From 969fd3bd2d4ae13aeb77cea84bd7e22413f2fe56 Mon Sep 17 00:00:00 2001 From: Mikhail Zholobov Date: Wed, 29 Jan 2014 23:59:47 +0400 Subject: [PATCH 01/33] Action 'CheckGuestTools': text notice added --- lib/vagrant-parallels/action/check_guest_tools.rb | 6 ++++-- locales/en.yml | 5 ++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/vagrant-parallels/action/check_guest_tools.rb b/lib/vagrant-parallels/action/check_guest_tools.rb index 7c256d69..84e80988 100644 --- a/lib/vagrant-parallels/action/check_guest_tools.rb +++ b/lib/vagrant-parallels/action/check_guest_tools.rb @@ -7,6 +7,8 @@ def initialize(app, env) end def call(env) + env[:ui].output(I18n.t("vagrant_parallels.parallels.checking_guest_tools")) + tools_version = env[:machine].provider.driver.read_guest_tools_version if !tools_version env[:ui].warn I18n.t("vagrant_parallels.actions.vm.check_guest_tools.not_detected") @@ -14,8 +16,8 @@ def call(env) pd_version = env[:machine].provider.driver.version unless pd_version.start_with? tools_version env[:ui].warn(I18n.t("vagrant_parallels.actions.vm.check_guest_tools.version_mismatch", - tools_version: tools_version, - parallels_version: pd_version)) + :tools_version => tools_version, + :parallels_version => pd_version)) end end diff --git a/locales/en.yml b/locales/en.yml index 1962a74f..4ce76be3 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -1,5 +1,8 @@ en: vagrant_parallels: + parallels: + checking_guest_tools: |- + Checking for Parallels Tools installed on the VM... #------------------------------------------------------------------------------- # Translations for exception classes #------------------------------------------------------------------------------- @@ -69,7 +72,7 @@ en: suspend the virtual machine. In either case, to restart it again, simply run `vagrant up`. #------------------------------------------------------------------------------- -# Translations for Vagrant middleware acions +# Translations for Vagrant middleware actions #------------------------------------------------------------------------------- actions: vm: From 47931329fc101d10dd96455a5069511903ccc6a6 Mon Sep 17 00:00:00 2001 From: Mikhail Zholobov Date: Thu, 30 Jan 2014 00:04:15 +0400 Subject: [PATCH 02/33] 'synced_folder': output methods changed --- lib/vagrant-parallels/synced_folder.rb | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/vagrant-parallels/synced_folder.rb b/lib/vagrant-parallels/synced_folder.rb index d15b33a8..6f2c465b 100644 --- a/lib/vagrant-parallels/synced_folder.rb +++ b/lib/vagrant-parallels/synced_folder.rb @@ -36,14 +36,15 @@ def enable(machine, folders, _opts) end # Go through each folder and mount - machine.ui.info(I18n.t("vagrant.actions.vm.share_folders.mounting")) + machine.ui.output(I18n.t("vagrant.actions.vm.share_folders.mounting")) folders.each do |id, data| if data[:guestpath] id = Pathname.new(id).to_s.split('/').drop_while{|i| i.empty?}.join('_') # Guest path specified, so mount the folder to specified point - machine.ui.info(I18n.t("vagrant.actions.vm.share_folders.mounting_entry", - :guest_path => data[:guestpath])) + machine.ui.detail(I18n.t("vagrant.actions.vm.share_folders.mounting_entry", + guestpath: data[:guestpath], + hostpath: data[:hostpath])) # Dup the data so we can pass it to the guest API data = data.dup @@ -58,8 +59,8 @@ def enable(machine, folders, _opts) :mount_parallels_shared_folder, id, data[:guestpath], data) else # If no guest path is specified, then automounting is disabled - machine.ui.info(I18n.t("vagrant.actions.vm.share_folders.nomount_entry", - :host_path => data[:hostpath])) + machine.ui.detail(I18n.t("vagrant.actions.vm.share_folders.nomount_entry", + :hostpath => data[:hostpath])) end end end From 7c3f21e533475ad5c1b3204de92b05b5723c0dbe Mon Sep 17 00:00:00 2001 From: Mikhail Zholobov Date: Mon, 10 Feb 2014 23:45:18 +0400 Subject: [PATCH 03/33] 'action.rb': Replace HandleBoxUrl with HandleBox middleware [GH-76] --- lib/vagrant-parallels/action.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/vagrant-parallels/action.rb b/lib/vagrant-parallels/action.rb index f3c60aa7..7b8bb74f 100644 --- a/lib/vagrant-parallels/action.rb +++ b/lib/vagrant-parallels/action.rb @@ -252,7 +252,7 @@ def self.action_up # works fine. b.use Call, Created do |env, b2| if !env[:result] - b2.use HandleBoxUrl + b2.use HandleBox end end From ab657b63b488d6031ca7aacaa638c0ac5f80ead7 Mon Sep 17 00:00:00 2001 From: Mikhail Zholobov Date: Wed, 12 Feb 2014 00:44:06 +0400 Subject: [PATCH 04/33] config.rb: network_adapter option uses proper format [mitchellh/vagrant#2854] --- lib/vagrant-parallels/config.rb | 4 ++-- test/unit/config_test.rb | 13 ++++++++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/vagrant-parallels/config.rb b/lib/vagrant-parallels/config.rb index d1ad8663..d55d09ed 100644 --- a/lib/vagrant-parallels/config.rb +++ b/lib/vagrant-parallels/config.rb @@ -21,8 +21,8 @@ def customize(*command) @customizations << [event, command] end - def network_adapter(slot, type, *args) - @network_adapters[slot] = [type, args] + def network_adapter(slot, type, **opts) + @network_adapters[slot] = [type, opts] end # @param size [Integer, String] the memory size in MB diff --git a/test/unit/config_test.rb b/test/unit/config_test.rb index 8d4e7f42..b55ca48b 100644 --- a/test/unit/config_test.rb +++ b/test/unit/config_test.rb @@ -7,9 +7,12 @@ context "defaults" do before { subject.finalize! } + its(:check_guest_tools) { should be_true } + its(:name) { should be_nil } + it "should have one Shared adapter" do expect(subject.network_adapters).to eql({ - 0 => [:shared, []], + 0 => [:shared, {}], }) end end @@ -27,4 +30,12 @@ expect(subject.customizations).to include(["pre-boot", ["set", :id, "--cpus", 4]]) end end + + describe "#network_adapter" do + it "configures additional adapters" do + subject.network_adapter(2, :bridged, auto_config: true) + expect(subject.network_adapters[2]).to eql( + [:bridged, auto_config: true]) + end + end end From c10da82b38dde3eb6667a109ca2c01b6db16fbec Mon Sep 17 00:00:00 2001 From: Mikhail Zholobov Date: Wed, 12 Feb 2014 00:58:16 +0400 Subject: [PATCH 05/33] Test for 'synced_folder' added --- test/unit/synced_folder_test.rb | 37 +++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 test/unit/synced_folder_test.rb diff --git a/test/unit/synced_folder_test.rb b/test/unit/synced_folder_test.rb new file mode 100644 index 00000000..00fd92ed --- /dev/null +++ b/test/unit/synced_folder_test.rb @@ -0,0 +1,37 @@ +require "vagrant" +require_relative "../unit/base" + +require VagrantPlugins::Parallels.source_root.join('lib/vagrant-parallels/synced_folder') + +describe VagrantPlugins::Parallels::SyncedFolder do + let(:machine) do + double("machine").tap do |m| + end + end + + subject { described_class.new } + + describe "usable" do + it "should be with parallels provider" do + machine.stub(provider_name: :parallels) + subject.should be_usable(machine) + end + + it "should not be with another provider" do + machine.stub(provider_name: :virtualbox) + subject.should_not be_usable(machine) + end + end + + describe "prepare" do + let(:driver) { double("driver") } + + before do + machine.stub(driver: driver) + end + + it "should share the folders" do + pending + end + end +end From 16f8ef6999f2107e3c84b2eb280d0a6cebe794ee Mon Sep 17 00:00:00 2001 From: Mikhail Zholobov Date: Wed, 12 Feb 2014 01:10:33 +0400 Subject: [PATCH 06/33] config.rb: possibility to skip guest tools check --- lib/vagrant-parallels/action/check_guest_tools.rb | 6 ++++++ lib/vagrant-parallels/config.rb | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/lib/vagrant-parallels/action/check_guest_tools.rb b/lib/vagrant-parallels/action/check_guest_tools.rb index 84e80988..5ff15433 100644 --- a/lib/vagrant-parallels/action/check_guest_tools.rb +++ b/lib/vagrant-parallels/action/check_guest_tools.rb @@ -4,9 +4,15 @@ module Action class CheckGuestTools def initialize(app, env) @app = app + @logger = Log4r::Logger.new("vagrant::plugins::parallels::check_guest_tools") end def call(env) + if !env[:machine].provider_config.check_guest_tools + @logger.info("Not checking guest tools because configuration") + return @app.call(env) + end + env[:ui].output(I18n.t("vagrant_parallels.parallels.checking_guest_tools")) tools_version = env[:machine].provider.driver.read_guest_tools_version diff --git a/lib/vagrant-parallels/config.rb b/lib/vagrant-parallels/config.rb index d55d09ed..36872f96 100644 --- a/lib/vagrant-parallels/config.rb +++ b/lib/vagrant-parallels/config.rb @@ -1,12 +1,17 @@ module VagrantPlugins module Parallels class Config < Vagrant.plugin("2", :config) + attr_accessor :check_guest_tools attr_reader :customizations attr_accessor :destroy_unused_network_interfaces attr_reader :network_adapters attr_accessor :name + # Compatibility with virtualbox provider's syntax + alias_attribute :check_guest_additions, :check_guest_tools + def initialize + @check_guest_tools = UNSET_VALUE @customizations = [] @destroy_unused_network_interfaces = UNSET_VALUE @network_adapters = {} @@ -35,6 +40,10 @@ def cpus=(count) end def finalize! + if @check_guest_tools == UNSET_VALUE + @check_guest_tools = true + end + if @destroy_unused_network_interfaces == UNSET_VALUE @destroy_unused_network_interfaces = true end From 20d56a37ea3f66eccd5e86b4185d1a80edf5bc7a Mon Sep 17 00:00:00 2001 From: Mikhail Zholobov Date: Wed, 12 Feb 2014 01:37:18 +0400 Subject: [PATCH 07/33] driver global refactoring [GH-75] --- lib/vagrant-parallels/action/customize.rb | 10 +- lib/vagrant-parallels/action/import.rb | 4 +- .../action/prepare_nfs_valid_ids.rb | 2 +- lib/vagrant-parallels/action/set_name.rb | 4 +- .../action/unregister_template.rb | 2 +- lib/vagrant-parallels/driver/base.rb | 267 ++++++++++++++++++ lib/vagrant-parallels/driver/meta.rb | 138 +++++++++ .../driver/{prl_ctl.rb => pd_9.rb} | 238 ++++------------ lib/vagrant-parallels/plugin.rb | 8 +- lib/vagrant-parallels/provider.rb | 26 +- .../driver/{prl_ctl_test.rb => pd_9_test.rb} | 6 +- 11 files changed, 512 insertions(+), 193 deletions(-) create mode 100644 lib/vagrant-parallels/driver/base.rb create mode 100644 lib/vagrant-parallels/driver/meta.rb rename lib/vagrant-parallels/driver/{prl_ctl.rb => pd_9.rb} (70%) rename test/unit/driver/{prl_ctl_test.rb => pd_9_test.rb} (96%) diff --git a/lib/vagrant-parallels/action/customize.rb b/lib/vagrant-parallels/action/customize.rb index 8662fa8a..7283dd11 100644 --- a/lib/vagrant-parallels/action/customize.rb +++ b/lib/vagrant-parallels/action/customize.rb @@ -25,11 +25,13 @@ def call(env) arg.to_s end - result = env[:machine].provider.driver.set_vm_settings(processed_command) - if result.exit_code != 0 + begin + env[:machine].provider.driver.execute_command( + processed_command + [retryable: true]) + rescue VagrantPlugins::Parallels::Errors::PrlCtlError => e raise Vagrant::Errors::VMCustomizationFailed, { - :command => processed_command.inspect, - :error => result.stderr + :command => command, + :error => e.inspect } end end diff --git a/lib/vagrant-parallels/action/import.rb b/lib/vagrant-parallels/action/import.rb index 942ae2e1..998da4a2 100644 --- a/lib/vagrant-parallels/action/import.rb +++ b/lib/vagrant-parallels/action/import.rb @@ -17,13 +17,13 @@ def call(env) vm_name = generate_name(env[:root_path]) # Verify the name is not taken - if env[:machine].provider.driver.read_all_names.has_key?(vm_name) + if env[:machine].provider.driver.read_vms.has_key?(vm_name) raise Vagrant::Errors::VMNameExists, :name => vm_name end # Import the virtual machine template_path = File.realpath(Pathname.glob(env[:machine].box.directory.join('*.pvm')).first) - template_uuid = env[:machine].provider.driver.read_all_paths[template_path] + template_uuid = env[:machine].provider.driver.read_vms_paths[template_path] env[:machine].id = env[:machine].provider.driver.import(template_uuid, vm_name) do |progress| env[:ui].clear_line diff --git a/lib/vagrant-parallels/action/prepare_nfs_valid_ids.rb b/lib/vagrant-parallels/action/prepare_nfs_valid_ids.rb index 02786285..bb0885b7 100644 --- a/lib/vagrant-parallels/action/prepare_nfs_valid_ids.rb +++ b/lib/vagrant-parallels/action/prepare_nfs_valid_ids.rb @@ -8,7 +8,7 @@ def initialize(app, env) end def call(env) - env[:nfs_valid_ids] = env[:machine].provider.driver.read_all_names.values + env[:nfs_valid_ids] = env[:machine].provider.driver.read_vms.values @app.call(env) end end diff --git a/lib/vagrant-parallels/action/set_name.rb b/lib/vagrant-parallels/action/set_name.rb index baa0a5b8..198db5c8 100644 --- a/lib/vagrant-parallels/action/set_name.rb +++ b/lib/vagrant-parallels/action/set_name.rb @@ -28,8 +28,8 @@ def call(env) end # Verify the name is not taken - vms_names = env[:machine].provider.driver.read_all_names - raise Vagrant::Errors::VMNameExists, :name => name if \ + vms_names = env[:machine].provider.driver.read_vms + raise VagrantPlugins::Parallels::Errors::VMNameExists, :name => name if \ vms_names.has_key?(name) && vms_names[name] != env[:machine].id if vms_names.has_key?(name) diff --git a/lib/vagrant-parallels/action/unregister_template.rb b/lib/vagrant-parallels/action/unregister_template.rb index 542d7f11..1a18c5a1 100644 --- a/lib/vagrant-parallels/action/unregister_template.rb +++ b/lib/vagrant-parallels/action/unregister_template.rb @@ -11,7 +11,7 @@ def call(env) env[:machine].box.directory.join('*.pvm') ).first) - template_uuid = env[:machine].provider.driver.read_all_paths[template_path] + template_uuid = env[:machine].provider.driver.read_vms_paths[template_path] if env[:machine].provider.driver.registered?(template_path) env[:machine].provider.driver.unregister(template_uuid) diff --git a/lib/vagrant-parallels/driver/base.rb b/lib/vagrant-parallels/driver/base.rb new file mode 100644 index 00000000..be94ba99 --- /dev/null +++ b/lib/vagrant-parallels/driver/base.rb @@ -0,0 +1,267 @@ +require 'log4r' + +require 'vagrant/util/busy' +require 'vagrant/util/network_ip' +require 'vagrant/util/platform' +require 'vagrant/util/retryable' +require 'vagrant/util/subprocess' + +module VagrantPlugins + module Parallels + module Driver + # Base class for all Parallels drivers. + # + # This class provides useful tools for things such as executing + # PrlCtl and handling SIGINTs and so on. + class Base + # Include this so we can use `Subprocess` more easily. + include Vagrant::Util::Retryable + include Vagrant::Util::NetworkIP + + def initialize + @logger = Log4r::Logger.new("vagrant::provider::parallels::base") + + # This flag is used to keep track of interrupted state (SIGINT) + @interrupted = false + + # Set the path to prlctl + @prlctl_path = "prlctl" + @prlsrvctl_path = "prlsrvctl" + + @logger.info("CLI prlctl path: #{@prlctl_path}") + @logger.info("CLI prlsrvctl path: #{@prlsrvctl_path}") + end + + # Clears the shared folders that have been set on the virtual machine. + def clear_shared_folders + end + + # Creates a host only network with the given options. + # + # @param [Hash] options Options to create the host only network. + # @return [Hash] The details of the host only network, including + # keys `:name`, `:ip`, and `:netmask` + def create_host_only_network(options) + end + + # Deletes the virtual machine references by this driver. + def delete + end + + # Deletes any host only networks that aren't being used for anything. + def delete_unused_host_only_networks + end + + # Enables network adapters on the VM. + # + # The format of each adapter specification should be like so: + # + # { + # :type => :hostonly, + # :hostonly => "vboxnet0", + # :mac_address => "tubes" + # } + # + # This must support setting up both host only and bridged networks. + # + # @param [Array] adapters Array of adapters to enable. + def enable_adapters(adapters) + end + + # Execute a raw command straight through to VBoxManage. + # + # Accepts a :retryable => true option if the command should be retried + # upon failure. + # + # Raises a VBoxManage error if it fails. + # + # @param [Array] command Command to execute. + def execute_command(command) + end + + # Exports the virtual machine to the given path. + # + # @param [String] path Path to the OVF file. + # @yield [progress] Yields the block with the progress of the export. + def export(path) + end + + # Halts the virtual machine (pulls the plug). + def halt + end + + # Imports the VM from an OVF file. + # + # @param [String] ovf Path to the OVF file. + # @return [String] UUID of the imported VM. + def import(ovf) + end + + # Parses given block (JSON string) to object + def json(default=nil) + data = yield + JSON.parse(data) rescue default + end + + # Returns the maximum number of network adapters. + # TODO: Implement the usage! + def max_network_adapters + 16 + end + + # Returns a list of bridged interfaces. + # + # @return [Hash] + def read_bridged_interfaces + end + + # Returns the guest tools version that is installed on this VM. + # + # @return [String] + def read_guest_tools_version + end + + # Returns a list of available host only interfaces. + # + # @return [Hash] + def read_host_only_interfaces + end + + # Returns the MAC address of the first network interface. + # + # @return [String] + def read_mac_address + end + + # Returns a list of network interfaces of the VM. + # + # @return [Hash] + def read_network_interfaces + end + + # Returns the current state of this VM. + # + # @return [Symbol] + def read_state + end + + # Returns a list of all UUIDs of virtual machines currently + # known by VirtualBox. + # + # @return [Array] + def read_vms + end + + # Sets the MAC address of the first network adapter. + # + # @param [String] mac MAC address without any spaces/hyphens. + def set_mac_address(mac) + end + + # Sets the VM name. + # + # @param [String] name New VM name. + def set_name(name) + end + + # Share a set of folders on this VM. + # + # @param [Array] folders + def share_folders(folders) + end + + # Reads the SSH port of this VM. + # + # @param [Integer] expected Expected guest port of SSH. + def ssh_port(expected) + end + + # Starts the virtual machine. + # + # @param [String] mode Mode to boot the VM. Either "headless" + # or "gui" + def start(mode) + end + + # Suspend the virtual machine. + def suspend + end + + # Verifies that the driver is ready to accept work. + # + # This should raise a VagrantError if things are not ready. + def verify! + end + + # Checks if a VM with the given UUID exists. + # + # @return [Boolean] + def vm_exists?(uuid) + end + + # Execute the given subcommand for PrlCtl and return the output. + def execute(*command, &block) + # Get the options hash if it exists + opts = {} + opts = command.pop if command.last.is_a?(Hash) + + tries = opts[:retryable] ? 3 : 0 + + # Variable to store our execution result + r = nil + + retryable(:on => VagrantPlugins::Parallels::Errors::PrlCtlError, :tries => tries, :sleep => 1) do + # If there is an error with PrlCtl, this gets set to true + errored = false + + # Execute the command + r = raw(*command, &block) + + # If the command was a failure, then raise an exception that is + # nicely handled by Vagrant. + if r.exit_code != 0 + if @interrupted + @logger.info("Exit code != 0, but interrupted. Ignoring.") + else + errored = true + end + end + + # If there was an error running prlctl, show the error and the + # output. + if errored + raise VagrantPlugins::Parallels::Errors::PrlCtlError, + :command => command.inspect, + :stderr => r.stderr + end + end + r.stdout + end + + # Executes a command and returns the raw result object. + def raw(*command, &block) + int_callback = lambda do + @interrupted = true + @logger.info("Interrupted.") + end + + # Append in the options for subprocess + command << { :notify => [:stdout, :stderr] } + + # Get the utility to execute: + # 'prlctl' by default and 'prlsrvctl' if it set as a first argument in command + if command.first == :prlsrvctl + cli = @prlsrvctl_path + command.delete_at(0) + else + cli = @prlctl_path + end + + Vagrant::Util::Busy.busy(int_callback) do + Vagrant::Util::Subprocess.execute(cli, *command, &block) + end + end + end + end + end +end diff --git a/lib/vagrant-parallels/driver/meta.rb b/lib/vagrant-parallels/driver/meta.rb new file mode 100644 index 00000000..b5c5fc28 --- /dev/null +++ b/lib/vagrant-parallels/driver/meta.rb @@ -0,0 +1,138 @@ +require "forwardable" + +require "log4r" + +require File.expand_path("../base", __FILE__) + +module VagrantPlugins + module Parallels + module Driver + class Meta < Base + # This is raised if the VM is not found when initializing a driver + # with a UUID. + class VMNotFound < StandardError; end + + # We use forwardable to do all our driver forwarding + extend Forwardable + + # The UUID of the virtual machine we represent + attr_reader :uuid + + # The version of Parallels Desktop that is running. + attr_reader :version + + def initialize(uuid=nil) + # Setup the base + super() + + @logger = Log4r::Logger.new("vagrant::provider::parallels::meta") + @uuid = uuid + + # Read and assign the version of Parallels Desktop we know which + # specific driver to instantiate. + begin + @version = read_version || "" + rescue Vagrant::Errors::CommandUnavailable, + Vagrant::Errors::CommandUnavailableWindows + # This means that Parallels Desktop was not found, so we raise this + # error here. + raise VagrantPlugins::Parallels::Errors::ParallelsNotDetected + end + + # Instantiate the proper version driver for VirtualBox + @logger.debug("Finding driver for Parallels Desktop version: #{@version}") + driver_map = { + #TODO: Use customized class for each version + "8" => PD_9, + "9" => PD_9, + "10" => PD_9 + } + + driver_klass = nil + driver_map.each do |key, klass| + if @version.start_with?(key) + driver_klass = klass + break + end + end + + if !driver_klass + supported_versions = driver_map.keys.sort.join(", ") + raise VagrantPlugins::Parallels::Errors::ParallelsInvalidVersion, + supported_versions: supported_versions + end + + @logger.info("Using Parallels driver: #{driver_klass}") + @driver = driver_klass.new(@uuid) + + if @uuid + # Verify the VM exists, and if it doesn't, then don't worry + # about it (mark the UUID as nil) + raise VMNotFound if !@driver.vm_exists?(@uuid) + end + end + + def_delegators :@driver, + #:clear_forwarded_ports, + :clear_shared_folders, + #:create_dhcp_server, + :create_host_only_network, + :delete, + :delete_adapters, + :delete_unused_host_only_networks, + #:discard_saved_state, + :enable_adapters, + :execute_command, + :export, + #:forward_ports, + :halt, + :import, + :mac_in_use?, + :read_ip_dhcp, + #:read_forwarded_ports, + :read_bridged_interfaces, + :read_guest_tools_version, + :read_guest_ip, + :read_guest_property, + :read_host_only_interfaces, + :read_mac_address, + :read_network_interfaces, + :read_state, + :read_used_ports, + :read_vms, + :read_vms_info, + :read_vms_paths, + :register, + :registered?, + :resume, + :set_mac_address, + :set_name, + :share_folders, + :ssh_port, + :start, + :suspend, + :unregister, + :verify!, + :vm_exists? + + protected + + # This returns the version of Parallels Desktop that is running. + # + # @return [String] + def read_version + # The version string is usually in one of the following formats: + # + # * 8.0.12345.123456 + # * 9.0.12345.123456 + + if execute('--version', retryable: true) =~ /prlctl version ([\d\.]+)/ + return $1.downcase + else + return nil + end + end + end + end + end +end diff --git a/lib/vagrant-parallels/driver/prl_ctl.rb b/lib/vagrant-parallels/driver/pd_9.rb similarity index 70% rename from lib/vagrant-parallels/driver/prl_ctl.rb rename to lib/vagrant-parallels/driver/pd_9.rb index ece2b93d..7791e240 100644 --- a/lib/vagrant-parallels/driver/prl_ctl.rb +++ b/lib/vagrant-parallels/driver/pd_9.rb @@ -1,43 +1,22 @@ require 'log4r' -require 'json' -require 'vagrant/util/busy' -require "vagrant/util/network_ip" require 'vagrant/util/platform' -require 'vagrant/util/retryable' -require 'vagrant/util/subprocess' + +require File.expand_path("../base", __FILE__) module VagrantPlugins module Parallels module Driver - # Base class for all Parallels drivers. - # - # This class provides useful tools for things such as executing - # PrlCtl and handling SIGINTs and so on. - class PrlCtl - # Include this so we can use `Subprocess` more easily. - include Vagrant::Util::Retryable - include Vagrant::Util::NetworkIP - - attr_reader :uuid - + # Driver for Parallels Desktop 9. + class PD_9 < Base def initialize(uuid) - @logger = Log4r::Logger.new("vagrant::provider::parallels::prlctl") + super() - # This flag is used to keep track of interrupted state (SIGINT) - @interrupted = false - - # Store machine id + @logger = Log4r::Logger.new("vagrant::provider::parallels::pd_9") @uuid = uuid - - # Set the path to prlctl - @prlctl_path = "prlctl" - @prlsrvctl_path = "prlsrvctl" - - @logger.info("CLI prlctl path: #{@prlctl_path}") - @logger.info("CLI prlsrvctl path: #{@prlsrvctl_path}") end + def compact(uuid=nil) uuid ||= @uuid # TODO: VM can have more than one hdd! @@ -72,11 +51,11 @@ def create_host_only_network(options) # Return the details return { - :name => options[:name], - :bound_to => bound_to, - :ip => options[:adapter_ip], - :netmask => options[:netmask], - :dhcp => options[:dhcp] + :name => options[:name], + :bound_to => bound_to, + :ip => options[:adapter_ip], + :netmask => options[:netmask], + :dhcp => options[:dhcp] } end @@ -107,10 +86,10 @@ def delete_unused_host_only_networks # They should not be deleted anyway. networks.keep_if do |net| net['Type'] == "host-only" && - net['Bound To'].match(/^(?>vnic|Parallels Host-Only #)(\d+)$/)[1].to_i >= 2 + net['Bound To'].match(/^(?>vnic|Parallels Host-Only #)(\d+)$/)[1].to_i >= 2 end - read_all_info.each do |vm| + read_vms_info.each do |vm| used_nets = vm.fetch('Hardware', {}).select { |name, _| name.start_with? 'net' } used_nets.each_value do |net_params| networks.delete_if { |net| net['Bound To'] == net_params.fetch('iface', nil)} @@ -207,7 +186,18 @@ def import(template_uuid, vm_name) @uuid = read_settings(vm_name).fetch('ID', vm_name) end - def ip + def mac_in_use?(mac) + all_macs_in_use = [] + read_vms_info.each do |vm| + all_macs_in_use << vm.fetch('Hardware', {}).fetch('net0',{}).fetch('mac', '') + end + + valid_mac = mac.upcase.tr('^A-F0-9', '') + + all_macs_in_use.include?(valid_mac) + end + + def read_ip_dhcp mac_addr = read_mac_address.downcase File.foreach("/Library/Preferences/Parallels/parallels_dhcp_leases") do |line| if line.include? mac_addr @@ -217,38 +207,40 @@ def ip end end - - def mac_in_use?(mac) - all_macs_in_use = [] - read_all_info.each do |vm| - all_macs_in_use << vm.fetch('Hardware', {}).fetch('net0',{}).fetch('mac', '') + def read_vms + results = {} + vms_arr = json({}) do + execute('list', '--all', '--json', retryable: true).gsub(/^(INFO)?/, '') + end + templates_arr = json({}) do + execute('list', '--all', '--json', '--template', retryable: true).gsub(/^(INFO)?/, '') + end + vms = vms_arr | templates_arr + vms.each do |item| + results[item.fetch('name')] = item.fetch('uuid') end - valid_mac = mac.upcase.tr('^A-F0-9', '') - - all_macs_in_use.include?(valid_mac) + results end - # Returns a hash of all UUIDs assigned to VMs and templates currently - # known by Parallels. Keys are 'name' values - # - # @return [Hash] - def read_all_names - list = {} - read_all_info.each do |item| - list[item.fetch('Name')] = item.fetch('ID') + # Parse the JSON from *all* VMs and templates. Then return an array of objects (without duplicates) + def read_vms_info + vms_arr = json({}) do + execute('list', '--all','--info', '--json', retryable: true).gsub(/^(INFO)?/, '') end - - list + templates_arr = json({}) do + execute('list', '--all','--info', '--json', '--template', retryable: true).gsub(/^(INFO)?/, '') + end + vms_arr | templates_arr end # Returns a hash of all UUIDs assigned to VMs and templates currently - # known by Parallels. Keys are 'Home' directories + # known by Parallels Desktop. Keys are 'Home' directories # # @return [Hash] - def read_all_paths + def read_vms_paths list = {} - read_all_info.each do |item| + read_vms_info.each do |item| if Dir.exists? item.fetch('Home') list[File.realpath item.fetch('Home')] = item.fetch('ID') end @@ -325,7 +317,7 @@ def read_host_only_interfaces end hostonly_ifaces << info end - hostonly_ifaces + hostonly_ifaces end def read_mac_address @@ -361,21 +353,23 @@ def read_network_interfaces nics end + def read_settings(uuid=nil) + uuid ||= @uuid + json({}) { execute('list', uuid, '--info', '--json', retryable: true).gsub(/^(INFO)?\[/, '').gsub(/\]$/, '') } + end + # Returns the current state of this VM. # # @return [Symbol] def read_state - read_settings(@uuid).fetch('State', 'inaccessible').to_sym + vm = json({}) { execute('list', @uuid, '--json', retryable: true).gsub(/^(INFO)?\[/, '').gsub(/\]$/, '') } + vm.fetch('status', 'TROLOLO').to_sym end def read_virtual_networks json { execute(:prlsrvctl, 'net', 'list', '--json', retryable: true) } end - def ready? - !!guest_execute('uname') rescue false - end - def register(pvm_file) execute("register", pvm_file) end @@ -383,7 +377,7 @@ def register(pvm_file) def registered?(path) # TODO: Make this take UUID and have callers pass that instead # Need a way to get the UUID from unregistered templates though (config.pvs XML parsing/regex?) - read_all_paths.has_key?(path) + read_vms_paths.has_key?(path) end def resume @@ -398,9 +392,8 @@ def set_mac_address(mac) execute('set', @uuid, '--device-set', 'net0', '--type', 'shared', '--mac', mac) end - # apply custom vm setting via set parameter - def set_vm_settings(command) - raw(@prlctl_path, *command) + def execute_command(command) + execute(*command) end def share_folders(folders) @@ -441,115 +434,8 @@ def version end end - private - - def guest_execute(*command) - execute('exec', @uuid, *command) - end - - def json(default=nil) - data = yield - JSON.parse(data) rescue default - end - - # Parse the JSON from *all* VMs and templates. Then return an array of objects (without duplicates) - def read_all_info - vms_arr = json({}) do - execute('list', '--info', '--json', retryable: true).gsub(/^(INFO)?/, '') - end - templates_arr = json({}) do - execute('list', '--info', '--json', '--template', retryable: true).gsub(/^(INFO)?/, '') - end - vms_arr | templates_arr - end - - def read_settings(uuid=nil) - uuid ||= @uuid - json({}) { execute('list', uuid, '--info', '--json', retryable: true).gsub(/^(INFO)?\[/, '').gsub(/\]$/, '') } - end - - def error_detection(command_response) - errored = false - # If the command was a failure, then raise an exception that is - # nicely handled by Vagrant. - if command_response.exit_code != 0 - if @interrupted - @logger.info("Exit code != 0, but interrupted. Ignoring.") - elsif command_response.exit_code == 126 - # This exit code happens if PrlCtl is on the PATH, - # but another executable it tries to execute is missing. - # This is usually indicative of a corrupted Parallels install. - raise VagrantPlugins::Parallels::Errors::ParallelsErrorNotFoundError - else - errored = true - end - elsif command_response.stderr =~ /failed to open \/dev\/prlctl/i - # This catches an error message that only shows when kernel - # drivers aren't properly installed. - @logger.error("Error message about unable to open prlctl") - raise VagrantPlugins::Parallels::Errors::ParallelsErrorKernelModuleNotLoaded - elsif command_response.stderr =~ /Unable to perform/i - @logger.info("VM not running for command to work.") - errored = true - elsif command_response.stderr =~ /Invalid usage/i - @logger.info("PrlCtl error text found, assuming error.") - errored = true - end - errored - end - - # Execute the given subcommand for PrlCtl and return the output. - def execute(*command, &block) - # Get the utility to execute: 'prlctl' by default and 'prlsrvctl' if it set as a first argument in command - if command.first == :prlsrvctl - cli = @prlsrvctl_path - command.delete_at(0) - else - cli = @prlctl_path - end - - # Get the options hash if it exists - opts = {} - opts = command.pop if command.last.is_a?(Hash) - - tries = opts[:retryable] ? 3 : 0 - - # Variable to store our execution result - r = nil - - # If there is an error with PrlCtl, this gets set to true - errored = false - - retryable(on: VagrantPlugins::Parallels::Errors::ParallelsError, tries: tries, sleep: 1) do - # Execute the command - r = raw(cli, *command, &block) - errored = error_detection(r) - end - - # If there was an error running PrlCtl, show the error and the - # output. - if errored - raise VagrantPlugins::Parallels::Errors::ParallelsError, - command: command.inspect, - stderr: r.stderr - end - - r.stdout - end - - # Executes a command and returns the raw result object. - def raw(cli, *command, &block) - int_callback = lambda do - @interrupted = true - @logger.info("Interrupted.") - end - - # Append in the options for subprocess - command << { notify: [:stdout, :stderr] } - - Vagrant::Util::Busy.busy(int_callback) do - Vagrant::Util::Subprocess.execute(cli, *command, &block) - end + def vm_exists?(uuid) + raw("list", uuid).exit_code == 0 end end end diff --git a/lib/vagrant-parallels/plugin.rb b/lib/vagrant-parallels/plugin.rb index ad915ff3..fed896bf 100644 --- a/lib/vagrant-parallels/plugin.rb +++ b/lib/vagrant-parallels/plugin.rb @@ -47,8 +47,14 @@ class Plugin < Vagrant.plugin("2") end + autoload :Action, File.expand_path("../action", __FILE__) + + # Drop some autoloads in here to optimize the performance of loading + # our drivers only when they are needed. module Driver - autoload :PrlCtl, File.expand_path("../driver/prl_ctl", __FILE__) + autoload :Meta, File.expand_path("../driver/meta", __FILE__) + autoload :PD_8, File.expand_path("../driver/pd_8", __FILE__) + autoload :PD_9, File.expand_path("../driver/pd_9", __FILE__) end module Util diff --git a/lib/vagrant-parallels/provider.rb b/lib/vagrant-parallels/provider.rb index c150645f..aa0b63be 100644 --- a/lib/vagrant-parallels/provider.rb +++ b/lib/vagrant-parallels/provider.rb @@ -9,7 +9,10 @@ class Provider < Vagrant.plugin("2", :provider) def initialize(machine) @logger = Log4r::Logger.new("vagrant::provider::parallels") @machine = machine - @driver = Parallels::Driver::PrlCtl.new(machine.id) + + # This method will load in our driver, so we call it now to + # initialize it. + machine_id_changed end # @see Vagrant::Plugin::V2::Provider#action @@ -22,6 +25,23 @@ def action(name) nil end + # If the machine ID changed, then we need to rebuild our underlying + # driver. + def machine_id_changed + id = @machine.id + + begin + @logger.debug("Instantiating the driver for machine ID: #{@machine.id.inspect}") + @driver = VagrantPlugins::Parallels::Driver::Meta.new(id) + rescue VagrantPlugins::Parallels::Driver::Meta::VMNotFound + # The virtual machine doesn't exist, so we probably have a stale + # ID. Just clear the id out of the machine and reload it. + @logger.debug("VM not found! Clearing saved machine ID and reloading.") + id = nil + retry + end + end + # Returns the SSH info for accessing the Parallels VM. def ssh_info # If the VM is not created then we cannot possibly SSH into it, so @@ -30,7 +50,7 @@ def ssh_info # Return ip from running machine, use ip from config if available return { - :host => @machine.config.ssh.host || @driver.ip, + :host => @machine.config.ssh.host || @driver.read_ip_dhcp, :port => @driver.ssh_port(@machine.config.ssh.guest_port) } end @@ -65,4 +85,4 @@ def to_s end end end -end \ No newline at end of file +end diff --git a/test/unit/driver/prl_ctl_test.rb b/test/unit/driver/pd_9_test.rb similarity index 96% rename from test/unit/driver/prl_ctl_test.rb rename to test/unit/driver/pd_9_test.rb index ec5d4067..e7f36f64 100644 --- a/test/unit/driver/prl_ctl_test.rb +++ b/test/unit/driver/pd_9_test.rb @@ -1,9 +1,9 @@ require_relative "../base" -describe VagrantPlugins::Parallels::Driver::PrlCtl do +describe VagrantPlugins::Parallels::Driver::PD_9 do include_context "parallels" - subject { VagrantPlugins::Parallels::Driver::PrlCtl.new(uuid) } + subject { VagrantPlugins::Parallels::Driver::PD_9.new(uuid) } describe "compact" do it "compacts the VM disk images" do @@ -74,7 +74,7 @@ } it "checks the MAC address is already in use" do - subject.stub(:read_all_info).and_return([vm_1, vm_2]) + subject.stub(:read_vms_info).and_return([vm_1, vm_2]) subject.mac_in_use?('00:1c:42:bb:59:01').should be_true subject.mac_in_use?('00:1c:42:bb:59:02').should be_false From 1013cb44dbcb4e9bea0d5dca80e73003553e7be9 Mon Sep 17 00:00:00 2001 From: Mikhail Zholobov Date: Wed, 12 Feb 2014 11:05:55 +0400 Subject: [PATCH 08/33] 'error.rb': added new and deleted unused error messages. --- .../action/check_accessible.rb | 2 +- lib/vagrant-parallels/errors.rb | 18 ++++++----- locales/en.yml | 30 +++++++++++++------ 3 files changed, 33 insertions(+), 17 deletions(-) diff --git a/lib/vagrant-parallels/action/check_accessible.rb b/lib/vagrant-parallels/action/check_accessible.rb index a53cabc1..d11e8bfa 100644 --- a/lib/vagrant-parallels/action/check_accessible.rb +++ b/lib/vagrant-parallels/action/check_accessible.rb @@ -12,7 +12,7 @@ def call(env) # is a very bad situation and can only be fixed by the user. It # also prohibits us from actually doing anything with the virtual # machine, so we raise an error. - raise Vagrant::Errors::VMInaccessible + raise VagrantPlugins::Parallels::Errors::VMInaccessible end @app.call(env) diff --git a/lib/vagrant-parallels/errors.rb b/lib/vagrant-parallels/errors.rb index 4876c798..538e784b 100644 --- a/lib/vagrant-parallels/errors.rb +++ b/lib/vagrant-parallels/errors.rb @@ -7,25 +7,29 @@ class VagrantParallelsError < Vagrant::Errors::VagrantError error_namespace("vagrant_parallels.errors") end - class ParallelsError < VagrantParallelsError + class PrlCtlError < VagrantParallelsError error_key(:prlctl_error) end - class ParallelsErrorNotFoundError < VagrantParallelsError - error_key(:prlctl_not_found_error) + class ParallelsInstallIncomplete < VagrantParallelsError + error_key(:parallels_install_incomplete) end - class ParallelsErrorKernelModuleNotLoaded < VagrantParallelsError - error_key(:parallels_kernel_module_not_loaded) + class ParallelsInvalidVersion < VagrantParallelsError + error_key(:parallels_invalid_version) end - class ParallelsInstallIncomplete < VagrantParallelsError - error_key(:parallels_install_incomplete) + class ParallelsNotDetected < VagrantParallelsError + error_key(:parallels_not_detected) end class ParallelsNoRoomForHighLevelNetwork < VagrantParallelsError error_key(:parallels_no_room_for_high_level_network) end + + class VMInaccessible < VagrantParallelsError + error_key(:vm_inaccessible) + end end end end \ No newline at end of file diff --git a/locales/en.yml b/locales/en.yml index 4ce76be3..a9bf473f 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -14,18 +14,15 @@ en: Command: %{command} Stderr: %{stderr} - prlctl_not_found_error: |- - The "prlctl" command or one of its dependencies could not - be found. Please verify Parallels Desktop is properly installed. You can verify - everything is okay by running "prlctl --version" and verifying - that the Parallels Desktop version is outputted. - parallels_kernel_module_not_loaded: |- - Parallels Desktop is complaining that the kernel module is not loaded. Please - run `prlctl --version` or open the Parallels Desktop GUI to see the error - message which should contain instructions on how to fix this error. parallels_install_incomplete: |- Parallels Desktop is complaining that the installation is incomplete. Try to reinstall Parallels Desktop or contact Parallels support. + parallels_invalid_version: |- + Vagrant has detected that you have a version of Parallels Desktop installed + that is not supported. Please install or upgrade to one of the supported + versions listed below to use Vagrant: + + %{supported_versions} parallels_no_room_for_high_level_network: |- There is no available slots on the Parallels Desktop VM for the configured high-level network interfaces. "private_network" and "public_network" @@ -33,6 +30,21 @@ en: Parallels Desktop VM. Parallels Desktop limits the number of slots to 8, and it appears that every slot is in use. Please lower the number of used network adapters. + parallels_not_detected: |- + Vagrant could not detect Parallels Desktop! Make sure it is properly installed. + Vagrant uses the `prlctl` binary that ships with Parallels Desktop, and requires + this to be available on the PATH. If Parallels Desktop is installed, please find + the `prlctl` binary and add it to the PATH environmental variable. + vm_inaccessible: |- + Your VM has become "inaccessible." Unfortunately, this is a critical error + with Parallels Desktop that Vagrant can not cleanly recover from. + Please open VirtualBox and clear out your inaccessible virtual machines + or find a way to fix them. + vm_name_exists: |- + Parallels Desktop virtual machine with the name '%{name}' already exists. + Please use another name or delete the machine with the existing + name, and try again. + #------------------------------------------------------------------------------- # Translations for config validation errors #------------------------------------------------------------------------------- From b9d67d2a4a741fc3e1043f8054b102bc300c949f Mon Sep 17 00:00:00 2001 From: Mikhail Zholobov Date: Wed, 12 Feb 2014 11:36:47 +0400 Subject: [PATCH 09/33] Gemfile: plugin moved to 'plugins' group .gitignore: modified to be developer-frendly --- .gitignore | 34 +++++++++++++++++++++------------- Gemfile | 8 +++++--- vagrant-parallels.gemspec | 8 ++++---- 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/.gitignore b/.gitignore index 88bf3347..202482cd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,25 +1,33 @@ # OS-specific .DS_Store -#rbenv -.ruby-version +# Vagrant stuff +acceptance_config.yml +boxes/* +/Vagrantfile +/.vagrant +/vagrant-spec.config.rb # Bundler/Rubygems *.gem .bundle pkg/* tags -Gemfile.lock +/Gemfile.lock test/tmp/ -# Vagrant - -# Vagrant stuff -acceptance_config.yml -boxes/* -Vagrantfile -.vagrant -vagrant-spec.config.rb -*.box +# Python *.pyc -sandi_meter \ No newline at end of file + +# Rubinius +*.rbc + +# IDE junk +.idea/* +*.iml + +# Ruby Managers +.rbenv +.ruby-gemset +.ruby-version +.rvmrc diff --git a/Gemfile b/Gemfile index 5ebdb1bd..096cf3a5 100644 --- a/Gemfile +++ b/Gemfile @@ -1,11 +1,13 @@ source 'http://rubygems.org' -# Specify your gem's dependencies in vagrant-parallels.gemspec -gemspec +group :plugins do + # Specify your gem's dependencies in vagrant-parallels.gemspec + gemspec +end group :development do # We depend on Vagrant for development, but we don't add it as a # gem dependency because we expect to be installed within the # Vagrant environment itself using `vagrant plugin`. - gem 'vagrant', :git => 'git://github.com/mitchellh/vagrant.git', :tag => 'v1.4.3' + gem 'vagrant', :git => 'git://github.com/mitchellh/vagrant.git' end diff --git a/vagrant-parallels.gemspec b/vagrant-parallels.gemspec index 71326d59..99650b23 100644 --- a/vagrant-parallels.gemspec +++ b/vagrant-parallels.gemspec @@ -6,10 +6,10 @@ Gem::Specification.new do |spec| spec.name = "vagrant-parallels" spec.version = VagrantPlugins::Parallels::VERSION spec.platform = Gem::Platform::RUBY - spec.authors = ["Youssef Shahin"] - spec.email = ["yshahin@gmail.com"] - spec.description = %q{Enables Vagrant to manage Parallels machines.} - spec.summary = %q{Enables Vagrant to manage Parallels machines.} + spec.authors = ["Mikhail Zholobov", "Youssef Shahin"] + spec.email = ["mzholobov@parallels.com", "yshahin@gmail.com"] + spec.summary = %q{Parallels provider for Vagrant.} + spec.description = %q{Enables Vagrant to manage Parallels virtual machines.} spec.homepage = "http://github.com/Parallels/vagrant-parallels" spec.license = "MIT" From 154397fdf4d940c062790b90f9b3d8d3018b25c4 Mon Sep 17 00:00:00 2001 From: Mikhail Zholobov Date: Fri, 14 Feb 2014 15:55:59 +0400 Subject: [PATCH 10/33] alias 'check_guest_additions' fixed --- lib/vagrant-parallels/config.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/vagrant-parallels/config.rb b/lib/vagrant-parallels/config.rb index 36872f96..8ac49169 100644 --- a/lib/vagrant-parallels/config.rb +++ b/lib/vagrant-parallels/config.rb @@ -8,7 +8,7 @@ class Config < Vagrant.plugin("2", :config) attr_accessor :name # Compatibility with virtualbox provider's syntax - alias_attribute :check_guest_additions, :check_guest_tools + alias :check_guest_additions= :check_guest_tools= def initialize @check_guest_tools = UNSET_VALUE From a682b60132131ae95564ef5ed41df723a2a44624 Mon Sep 17 00:00:00 2001 From: Mikhail Zholobov Date: Sun, 16 Feb 2014 01:12:15 +0400 Subject: [PATCH 11/33] Driver refactoring (minor) --- lib/vagrant-parallels/driver/base.rb | 36 +++++++++++++---------- lib/vagrant-parallels/driver/meta.rb | 2 ++ lib/vagrant-parallels/driver/pd_9.rb | 44 ++++++++++------------------ test/unit/driver/pd_9_test.rb | 6 ++-- 4 files changed, 41 insertions(+), 47 deletions(-) diff --git a/lib/vagrant-parallels/driver/base.rb b/lib/vagrant-parallels/driver/base.rb index be94ba99..4a9c51d1 100644 --- a/lib/vagrant-parallels/driver/base.rb +++ b/lib/vagrant-parallels/driver/base.rb @@ -40,7 +40,7 @@ def clear_shared_folders # # @param [Hash] options Options to create the host only network. # @return [Hash] The details of the host only network, including - # keys `:name`, `:ip`, and `:netmask` + # keys `:name`, `:bound_to`, `:ip`, `:netmask` and `:dhcp` def create_host_only_network(options) end @@ -58,8 +58,9 @@ def delete_unused_host_only_networks # # { # :type => :hostonly, - # :hostonly => "vboxnet0", - # :mac_address => "tubes" + # :hostonly => "vagrant-vnet0", + # :bound_to => "vnic2", + # :nic_type => "virtio" # } # # This must support setting up both host only and bridged networks. @@ -68,12 +69,15 @@ def delete_unused_host_only_networks def enable_adapters(adapters) end - # Execute a raw command straight through to VBoxManage. + # Execute a raw command straight through to 'prlctl' utility + # + # Accepts a :prlsrvctl as a first element of command if the command + # should be executed through to 'prlsrvctl' utility # # Accepts a :retryable => true option if the command should be retried # upon failure. # - # Raises a VBoxManage error if it fails. + # Raises a prlctl error if it fails. # # @param [Array] command Command to execute. def execute_command(command) @@ -90,11 +94,11 @@ def export(path) def halt end - # Imports the VM from an OVF file. + # Imports the VM by cloning from registered template. # - # @param [String] ovf Path to the OVF file. + # @param [String] template_uuid Registered template UUID. # @return [String] UUID of the imported VM. - def import(ovf) + def import(template_uuid) end # Parses given block (JSON string) to object @@ -104,7 +108,6 @@ def json(default=nil) end # Returns the maximum number of network adapters. - # TODO: Implement the usage! def max_network_adapters 16 end @@ -145,13 +148,18 @@ def read_network_interfaces def read_state end - # Returns a list of all UUIDs of virtual machines currently - # known by VirtualBox. + # Returns a list of all registered + # virtual machines and templates. # - # @return [Array] + # @return [Hash] def read_vms end + # Resumes the virtual machine. + # + def resume(mac) + end + # Sets the MAC address of the first network adapter. # # @param [String] mac MAC address without any spaces/hyphens. @@ -178,9 +186,7 @@ def ssh_port(expected) # Starts the virtual machine. # - # @param [String] mode Mode to boot the VM. Either "headless" - # or "gui" - def start(mode) + def start end # Suspend the virtual machine. diff --git a/lib/vagrant-parallels/driver/meta.rb b/lib/vagrant-parallels/driver/meta.rb index b5c5fc28..a1083176 100644 --- a/lib/vagrant-parallels/driver/meta.rb +++ b/lib/vagrant-parallels/driver/meta.rb @@ -75,6 +75,7 @@ def initialize(uuid=nil) def_delegators :@driver, #:clear_forwarded_ports, :clear_shared_folders, + :compact, #:create_dhcp_server, :create_host_only_network, :delete, @@ -99,6 +100,7 @@ def initialize(uuid=nil) :read_network_interfaces, :read_state, :read_used_ports, + :read_virtual_networks, :read_vms, :read_vms_info, :read_vms_paths, diff --git a/lib/vagrant-parallels/driver/pd_9.rb b/lib/vagrant-parallels/driver/pd_9.rb index 7791e240..55e5df4e 100644 --- a/lib/vagrant-parallels/driver/pd_9.rb +++ b/lib/vagrant-parallels/driver/pd_9.rb @@ -17,13 +17,12 @@ def initialize(uuid) end - def compact(uuid=nil) - uuid ||= @uuid + def compact(uuid) # TODO: VM can have more than one hdd! path_to_hdd = read_settings(uuid).fetch("Hardware", {}).fetch("hdd0", {}).fetch("image", nil) raw('prl_disk_tool', 'compact', '--hdd', path_to_hdd) do |type, data| lines = data.split("\r") - # The progress of the import will be in the last line. Do a greedy + # The progress of the compact will be in the last line. Do a greedy # regular expression to find what we're looking for. if lines.last =~ /.+?(\d{,3}) ?%/ yield $1.to_i if block_given? @@ -80,7 +79,7 @@ def delete_adapters end def delete_unused_host_only_networks - networks = read_virtual_networks() + networks = read_virtual_networks # 'Shared'(vnic0) and 'Host-Only'(vnic1) are default in Parallels Desktop # They should not be deleted anyway. @@ -92,7 +91,7 @@ def delete_unused_host_only_networks read_vms_info.each do |vm| used_nets = vm.fetch('Hardware', {}).select { |name, _| name.start_with? 'net' } used_nets.each_value do |net_params| - networks.delete_if { |net| net['Bound To'] == net_params.fetch('iface', nil)} + networks.delete_if { |net| net['Bound To'] == net_params.fetch('iface', nil) } end end @@ -155,17 +154,16 @@ def enable_adapters(adapters) end end - def export(path, vm_name) - execute("clone", @uuid, "--name", vm_name, "--template", "--dst", path.to_s) do |type, data| + def export(path, tpl_name) + execute("clone", @uuid, "--name", tpl_name, "--template", "--dst", path.to_s) do |type, data| lines = data.split("\r") - # The progress of the import will be in the last line. Do a greedy + # The progress of the export will be in the last line. Do a greedy # regular expression to find what we're looking for. if lines.last =~ /.+?(\d{,3}) ?%/ yield $1.to_i if block_given? end end - - read_settings(vm_name).fetch('ID', vm_name) + read_vms[tpl_name] end def halt(force=false) @@ -183,7 +181,7 @@ def import(template_uuid, vm_name) yield $1.to_i if block_given? end end - @uuid = read_settings(vm_name).fetch('ID', vm_name) + read_vms[vm_name] end def mac_in_use?(mac) @@ -234,10 +232,6 @@ def read_vms_info vms_arr | templates_arr end - # Returns a hash of all UUIDs assigned to VMs and templates currently - # known by Parallels Desktop. Keys are 'Home' directories - # - # @return [Hash] def read_vms_paths list = {} read_vms_info.each do |item| @@ -358,9 +352,6 @@ def read_settings(uuid=nil) json({}) { execute('list', uuid, '--info', '--json', retryable: true).gsub(/^(INFO)?\[/, '').gsub(/\]$/, '') } end - # Returns the current state of this VM. - # - # @return [Symbol] def read_state vm = json({}) { execute('list', @uuid, '--json', retryable: true).gsub(/^(INFO)?\[/, '').gsub(/\]$/, '') } vm.fetch('status', 'TROLOLO').to_sym @@ -374,24 +365,22 @@ def register(pvm_file) execute("register", pvm_file) end - def registered?(path) - # TODO: Make this take UUID and have callers pass that instead - # Need a way to get the UUID from unregistered templates though (config.pvs XML parsing/regex?) - read_vms_paths.has_key?(path) + def registered?(uuid) + read_vms.has_value?(uuid) end def resume execute('resume', @uuid) end - def set_name(name) - execute('set', @uuid, '--name', name, :retryable => true) - end - def set_mac_address(mac) execute('set', @uuid, '--device-set', 'net0', '--type', 'shared', '--mac', mac) end + def set_name(name) + execute('set', @uuid, '--name', name, :retryable => true) + end + def execute_command(command) execute(*command) end @@ -419,9 +408,6 @@ def unregister(uuid) execute("unregister", uuid) end - # Verifies that the driver is ready to accept work. - # - # This should raise a VagrantError if things are not ready. def verify! version end diff --git a/test/unit/driver/pd_9_test.rb b/test/unit/driver/pd_9_test.rb index e7f36f64..89c2f978 100644 --- a/test/unit/driver/pd_9_test.rb +++ b/test/unit/driver/pd_9_test.rb @@ -2,8 +2,9 @@ describe VagrantPlugins::Parallels::Driver::PD_9 do include_context "parallels" + let(:parallels_version) { "9" } - subject { VagrantPlugins::Parallels::Driver::PD_9.new(uuid) } + subject { described_class.new(uuid) } describe "compact" do it "compacts the VM disk images" do @@ -20,8 +21,7 @@ tpl_uuid = "12345-hfgs-3456-hste" it "exports VM to template" do - subject.stub(:read_settings).with(tpl_name). - and_return({"ID" => tpl_uuid}) + subject.stub(:read_vms).and_return({tpl_name => tpl_uuid}) subprocess.should_receive(:execute). with("prlctl", "clone", uuid, "--name", an_instance_of(String), "--template", "--dst", From 23b26bfdc0cc4a77118ea06f5897f32ca314a14c Mon Sep 17 00:00:00 2001 From: Mikhail Zholobov Date: Sun, 16 Feb 2014 01:17:52 +0400 Subject: [PATCH 12/33] 'import' and 'export' action refactoring [GH-36] - register/unregister template actios has been moved to 'import' and 'export' - added interrupt handling - 'generate_name' util was deprecated --- lib/vagrant-parallels/action.rb | 5 -- lib/vagrant-parallels/action/export.rb | 68 +++++++++++++--- lib/vagrant-parallels/action/import.rb | 79 ++++++++++++------- .../action/register_template.rb | 24 ------ .../action/unregister_template.rb | 26 ------ lib/vagrant-parallels/driver/pd_9.rb | 5 +- lib/vagrant-parallels/plugin.rb | 6 -- 7 files changed, 109 insertions(+), 104 deletions(-) delete mode 100644 lib/vagrant-parallels/action/register_template.rb delete mode 100644 lib/vagrant-parallels/action/unregister_template.rb diff --git a/lib/vagrant-parallels/action.rb b/lib/vagrant-parallels/action.rb index 7b8bb74f..027c2bfb 100644 --- a/lib/vagrant-parallels/action.rb +++ b/lib/vagrant-parallels/action.rb @@ -261,10 +261,8 @@ def self.action_up # If the VM is NOT created yet, then do the setup steps if !env[:result] b2.use CheckAccessible - b2.use RegisterTemplate b2.use Customize, "pre-import" b2.use Import - b2.use UnregisterTemplate b2.use MatchMACAddress end end @@ -298,13 +296,10 @@ def self.action_up autoload :PackageConfigFiles, File.expand_path("../action/package_config_files", __FILE__) autoload :PrepareNFSSettings, File.expand_path("../action/prepare_nfs_settings", __FILE__) autoload :PrepareNFSValidIds, File.expand_path("../action/prepare_nfs_valid_ids", __FILE__) - autoload :RegisterTemplate, File.expand_path("../action/register_template", __FILE__) autoload :Resume, File.expand_path("../action/resume", __FILE__) autoload :SetupPackageFiles, File.expand_path("../action/setup_package_files", __FILE__) autoload :SetName, File.expand_path("../action/set_name", __FILE__) autoload :Suspend, File.expand_path("../action/suspend", __FILE__) - autoload :UnregisterTemplate, File.expand_path("../action/unregister_template", __FILE__) - end end end diff --git a/lib/vagrant-parallels/action/export.rb b/lib/vagrant-parallels/action/export.rb index 81f49476..245f5ad8 100644 --- a/lib/vagrant-parallels/action/export.rb +++ b/lib/vagrant-parallels/action/export.rb @@ -2,50 +2,94 @@ module VagrantPlugins module Parallels module Action class Export - include Util - def initialize(app, env) @app = app + @logger = Log4r::Logger.new("vagrant::plugins::parallels::export") end def call(env) @env = env + if env[:machine].provider.state.id != :stopped + raise Vagrant::Errors::VMPowerOffToPackage + end + + name = "#{env[:root_path].basename.to_s}_#{env[:machine].name}" + name.gsub!(/[^-a-z0-9_]/i, "") + + # Check the name is not in use + if @env[:machine].provider.driver.read_vms.has_key?(@template_name) + @template_name << rand(100000).to_s + end - raise Vagrant::Errors::VMPowerOffToPackage if \ - @env[:machine].provider.state.id != :stopped - export - compact + @template_name = gen_template_name + @template_uuid = export + compact_template + unregister_template @app.call(env) end - def export - temp_vm_name = generate_name(@env[:root_path], '_export') + def recover(env) + @env = env + unregister_template + end + + private + + def gen_template_name + # Use configured name if it is specified, or generate the new one + name = @env[:machine].provider_config.name + if !name + name = "#{@env[:root_path].basename.to_s}_#{@env[:machine].name}" + name.gsub!(/[^-a-z0-9_]/i, "") + end + tpl_name = "#{name}_box" + # Ensure that the name is not in use + if @env[:machine].provider.driver.read_vms.has_key?(name) + tpl_name << "_#{rand(1000)}" + end + + tpl_name + end + + def export @env[:ui].info I18n.t("vagrant.actions.vm.export.exporting") - @temp_vm_uuid = @env[:machine].provider.driver.export(@env["export.temp_dir"], temp_vm_name) do |progress| + tpl_uuid = @env[:machine].provider.driver.export(@env["export.temp_dir"], @template_name) do |progress| @env[:ui].clear_line @env[:ui].report_progress(progress, 100, false) + + # # If we got interrupted, then the import could have been interrupted. + # Just rise an exception and then 'recover' will be called to cleanup. + raise Vagrant::Errors::VagrantInterrupt if @env[:interrupted] end # Clear the line a final time so the next data can appear # alone on the line. @env[:ui].clear_line + + tpl_uuid end - def compact + def compact_template @env[:ui].info I18n.t("vagrant_parallels.actions.vm.export.compacting") - @env[:machine].provider.driver.compact(@temp_vm_uuid) do |progress| + @env[:machine].provider.driver.compact(@template_uuid) do |progress| @env[:ui].clear_line @env[:ui].report_progress(progress, 100, false) end - @env[:machine].provider.driver.unregister(@temp_vm_uuid) # Clear the line a final time so the next data can appear # alone on the line. @env[:ui].clear_line end + + def unregister_template + if @env[:machine].provider.driver.registered?(@template_uuid) + @logger.info("Unregister the box template: '#{@template_uuid}'") + @env[:machine].provider.driver.unregister(@template_uuid) + end + end end end end diff --git a/lib/vagrant-parallels/action/import.rb b/lib/vagrant-parallels/action/import.rb index 998da4a2..8494520a 100644 --- a/lib/vagrant-parallels/action/import.rb +++ b/lib/vagrant-parallels/action/import.rb @@ -2,41 +2,17 @@ module VagrantPlugins module Parallels module Action class Import - - include Util - def initialize(app, env) @app = app + @logger = Log4r::Logger.new("vagrant::plugins::parallels::import") end - #TODO: Clean up registered VM on interupt def call(env) - env[:ui].info I18n.t("vagrant.actions.vm.import.importing", - :name => env[:machine].box.name) - - vm_name = generate_name(env[:root_path]) - - # Verify the name is not taken - if env[:machine].provider.driver.read_vms.has_key?(vm_name) - raise Vagrant::Errors::VMNameExists, :name => vm_name - end - - # Import the virtual machine - template_path = File.realpath(Pathname.glob(env[:machine].box.directory.join('*.pvm')).first) - template_uuid = env[:machine].provider.driver.read_vms_paths[template_path] - - env[:machine].id = env[:machine].provider.driver.import(template_uuid, vm_name) do |progress| - env[:ui].clear_line - env[:ui].report_progress(progress, 100, false) - end - - # Clear the line one last time since the progress meter doesn't disappear - # immediately. - env[:ui].clear_line - - # If we got interrupted, then the import could have been - # interrupted and its not a big deal. Just return out. - return if env[:interrupted] + @env = env + @template_path = File.realpath(Pathname.glob(env[:machine].box.directory.join('*.pvm')).first) + @template_uuid = register_template + import + unregister_template # Flag as erroneous and return if import failed raise Vagrant::Errors::VMImportFailure if !env[:machine].id @@ -46,8 +22,13 @@ def call(env) end def recover(env) + @env = env + # We should to unregister template + unregister_template + if env[:machine].provider.state.id != :not_created return if env["vagrant.error"].is_a?(Vagrant::Errors::VagrantError) + return if env["vagrant_parallels.error"].is_a?(VagrantPlugins::Parallels::Errors::VagrantParallelsError) # If we're not supposed to destroy on error then just return return if !env[:destroy_on_error] @@ -61,6 +42,44 @@ def recover(env) env[:action_runner].run(Action.action_destroy, destroy_env) end end + + protected + + def register_template + if !@env[:machine].provider.driver.read_vms_paths.has_key?(@template_path) + @logger.info("Register the box template: '#{@template_path}'") + @env[:machine].provider.driver.register(@template_path) + end + + # Return the uuid of registered template + @env[:machine].provider.driver.read_vms_paths[@template_path] + end + + def import + @env[:ui].info I18n.t("vagrant.actions.vm.import.importing", + :name => @env[:machine].box.name) + + # Import the virtual machine + @env[:machine].id = @env[:machine].provider.driver.import(@template_uuid) do |progress| + @env[:ui].clear_line + @env[:ui].report_progress(progress, 100, false) + + # # If we got interrupted, then the import could have been interrupted. + # Just rise an exception and then 'recover' will be called to cleanup. + raise Vagrant::Errors::VagrantInterrupt if @env[:interrupted] + end + + # Clear the line one last time since the progress meter doesn't disappear + # immediately. + @env[:ui].clear_line + end + + def unregister_template + if @env[:machine].provider.driver.registered?(@template_uuid) + @logger.info("Unregister the box template: '#{@template_uuid}'") + @env[:machine].provider.driver.unregister(@template_uuid) + end + end end end end diff --git a/lib/vagrant-parallels/action/register_template.rb b/lib/vagrant-parallels/action/register_template.rb deleted file mode 100644 index 79dbd25a..00000000 --- a/lib/vagrant-parallels/action/register_template.rb +++ /dev/null @@ -1,24 +0,0 @@ -module VagrantPlugins - module Parallels - module Action - class RegisterTemplate - def initialize(app, env) - @app = app - end - - def call(env) - pvm_glob = Pathname.glob(env[:machine].box.directory.join('*.pvm')).first - # TODO: Handle error cases better, throw a Vagrant error and not a stack trace etc. - pvm_file = File.realpath pvm_glob.to_s - - unless env[:machine].provider.driver.registered?(pvm_file) - env[:machine].provider.driver.register(pvm_file.to_s) - end - # Call the next if we have one (but we shouldn't, since this - # middleware is built to run with the Call-type middlewares) - @app.call(env) - end - end - end - end -end diff --git a/lib/vagrant-parallels/action/unregister_template.rb b/lib/vagrant-parallels/action/unregister_template.rb deleted file mode 100644 index 1a18c5a1..00000000 --- a/lib/vagrant-parallels/action/unregister_template.rb +++ /dev/null @@ -1,26 +0,0 @@ -module VagrantPlugins - module Parallels - module Action - class UnregisterTemplate - def initialize(app, env) - @app = app - end - - def call(env) - template_path = File.realpath(Pathname.glob( - env[:machine].box.directory.join('*.pvm') - ).first) - - template_uuid = env[:machine].provider.driver.read_vms_paths[template_path] - - if env[:machine].provider.driver.registered?(template_path) - env[:machine].provider.driver.unregister(template_uuid) - end - # Call the next if we have one (but we shouldn't, since this - # middleware is built to run with the Call-type middlewares) - @app.call(env) - end - end - end - end -end diff --git a/lib/vagrant-parallels/driver/pd_9.rb b/lib/vagrant-parallels/driver/pd_9.rb index 55e5df4e..ec58a1e2 100644 --- a/lib/vagrant-parallels/driver/pd_9.rb +++ b/lib/vagrant-parallels/driver/pd_9.rb @@ -172,7 +172,10 @@ def halt(force=false) execute(*args) end - def import(template_uuid, vm_name) + def import(template_uuid) + template_name = read_vms.key(template_uuid) + vm_name = "#{template_name}_#{(Time.now.to_f * 1000.0).to_i}_#{rand(100000)}" + execute("clone", template_uuid, '--name', vm_name) do |type, data| lines = data.split("\r") # The progress of the import will be in the last line. Do a greedy diff --git a/lib/vagrant-parallels/plugin.rb b/lib/vagrant-parallels/plugin.rb index fed896bf..a4c5946d 100644 --- a/lib/vagrant-parallels/plugin.rb +++ b/lib/vagrant-parallels/plugin.rb @@ -56,11 +56,5 @@ module Driver autoload :PD_8, File.expand_path("../driver/pd_8", __FILE__) autoload :PD_9, File.expand_path("../driver/pd_9", __FILE__) end - - module Util - def generate_name(path, suffix='') - "#{path.basename.to_s.gsub(/[^-a-z0-9_]/i, '')}#{suffix}_#{Time.now.to_i}" - end - end end end \ No newline at end of file From f7ad058434a329496a861d6ab2b41f42c61bc891 Mon Sep 17 00:00:00 2001 From: Mikhail Zholobov Date: Sun, 16 Feb 2014 13:06:47 +0400 Subject: [PATCH 13/33] 'action/network': max adapters number is defined by driver --- lib/vagrant-parallels/action/network.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/vagrant-parallels/action/network.rb b/lib/vagrant-parallels/action/network.rb index b5555484..0266b6d3 100644 --- a/lib/vagrant-parallels/action/network.rb +++ b/lib/vagrant-parallels/action/network.rb @@ -27,8 +27,11 @@ def call(env) # Get the list of network adapters from the configuration network_adapters_config = env[:machine].provider_config.network_adapters.dup + # Get maximum number of network adapters + max_adapters = env[:machine].provider.driver.max_network_adapters + # Assign the adapter slot for each high-level network - available_slots = Set.new(0..7) + available_slots = Set.new(0...max_adapters) network_adapters_config.each do |slot, _data| available_slots.delete(slot) end From 244d10d9b63884b7cd09f2ae43a3caaddef7868e Mon Sep 17 00:00:00 2001 From: Mikhail Zholobov Date: Sun, 16 Feb 2014 16:13:51 +0400 Subject: [PATCH 14/33] 'action/export': multi-hdd compacting fixed --- lib/vagrant-parallels/action/export.rb | 2 +- lib/vagrant-parallels/driver/base.rb | 28 +++++++++++++------------- lib/vagrant-parallels/driver/pd_9.rb | 17 ++++++++-------- locales/en.yml | 2 +- 4 files changed, 25 insertions(+), 24 deletions(-) diff --git a/lib/vagrant-parallels/action/export.rb b/lib/vagrant-parallels/action/export.rb index 245f5ad8..88fb7715 100644 --- a/lib/vagrant-parallels/action/export.rb +++ b/lib/vagrant-parallels/action/export.rb @@ -47,7 +47,7 @@ def gen_template_name tpl_name = "#{name}_box" # Ensure that the name is not in use - if @env[:machine].provider.driver.read_vms.has_key?(name) + if @env[:machine].provider.driver.read_vms.has_key?(tpl_name) tpl_name << "_#{rand(1000)}" end diff --git a/lib/vagrant-parallels/driver/base.rb b/lib/vagrant-parallels/driver/base.rb index 4a9c51d1..0cbdb1be 100644 --- a/lib/vagrant-parallels/driver/base.rb +++ b/lib/vagrant-parallels/driver/base.rb @@ -24,12 +24,16 @@ def initialize # This flag is used to keep track of interrupted state (SIGINT) @interrupted = false - # Set the path to prlctl - @prlctl_path = "prlctl" - @prlsrvctl_path = "prlsrvctl" - - @logger.info("CLI prlctl path: #{@prlctl_path}") - @logger.info("CLI prlsrvctl path: #{@prlsrvctl_path}") + # Set the path to CLI utils + @cli_paths = { + :prlctl => "prlctl", + :prlsrvctl => "prlsrvctl", + :prl_disk_tool => "prl_disk_tool" + } + + @cli_paths.each do |name, path| + @logger.info("CLI #{name} path: #{path}") + end end # Clears the shared folders that have been set on the virtual machine. @@ -254,14 +258,10 @@ def raw(*command, &block) # Append in the options for subprocess command << { :notify => [:stdout, :stderr] } - # Get the utility to execute: - # 'prlctl' by default and 'prlsrvctl' if it set as a first argument in command - if command.first == :prlsrvctl - cli = @prlsrvctl_path - command.delete_at(0) - else - cli = @prlctl_path - end + # Get the utility from the first argument: + # 'prlctl' by default + util = @cli_paths.has_key?(command.first) ? command.delete_at(0) : :prlctl + cli = @cli_paths[util] Vagrant::Util::Busy.busy(int_callback) do Vagrant::Util::Subprocess.execute(cli, *command, &block) diff --git a/lib/vagrant-parallels/driver/pd_9.rb b/lib/vagrant-parallels/driver/pd_9.rb index ec58a1e2..1575badb 100644 --- a/lib/vagrant-parallels/driver/pd_9.rb +++ b/lib/vagrant-parallels/driver/pd_9.rb @@ -18,14 +18,15 @@ def initialize(uuid) def compact(uuid) - # TODO: VM can have more than one hdd! - path_to_hdd = read_settings(uuid).fetch("Hardware", {}).fetch("hdd0", {}).fetch("image", nil) - raw('prl_disk_tool', 'compact', '--hdd', path_to_hdd) do |type, data| - lines = data.split("\r") - # The progress of the compact will be in the last line. Do a greedy - # regular expression to find what we're looking for. - if lines.last =~ /.+?(\d{,3}) ?%/ - yield $1.to_i if block_given? + used_drives = read_settings.fetch('Hardware', {}).select { |name, _| name.start_with? 'hdd' } + used_drives.each_value do |drive_params| + execute(:prl_disk_tool, 'compact', '--hdd', drive_params["image"]) do |type, data| + lines = data.split("\r") + # The progress of the compact will be in the last line. Do a greedy + # regular expression to find what we're looking for. + if lines.last =~ /.+?(\d{,3}) ?%/ + yield $1.to_i if block_given? + end end end end diff --git a/locales/en.yml b/locales/en.yml index a9bf473f..118689c7 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -107,7 +107,7 @@ en: Parallels Tools Version: %{tools_version} Parallels Desktop Version: %{parallels_version} export: - compacting: Compacting exported HDD... + compacting: Compacting exported HDDs... match_mac: generate: |- The specified base MAC is already in use. Generating a new unique MAC From 8c532aa7ba3af90ed142154453d1e349b3c8ce4e Mon Sep 17 00:00:00 2001 From: Mikhail Zholobov Date: Sun, 16 Feb 2014 18:43:07 +0400 Subject: [PATCH 15/33] 'driver/pd_9': 'read_bridged_interfaces' method fixed. --- lib/vagrant-parallels/driver/base.rb | 7 ++++--- lib/vagrant-parallels/driver/pd_9.rb | 10 ++++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/vagrant-parallels/driver/base.rb b/lib/vagrant-parallels/driver/base.rb index 0cbdb1be..9862ed2d 100644 --- a/lib/vagrant-parallels/driver/base.rb +++ b/lib/vagrant-parallels/driver/base.rb @@ -24,15 +24,16 @@ def initialize # This flag is used to keep track of interrupted state (SIGINT) @interrupted = false - # Set the path to CLI utils + # Set the list of required CLI utils @cli_paths = { :prlctl => "prlctl", :prlsrvctl => "prlsrvctl", - :prl_disk_tool => "prl_disk_tool" + :prl_disk_tool => "prl_disk_tool", + :ifconfig => "ifconfig" } @cli_paths.each do |name, path| - @logger.info("CLI #{name} path: #{path}") + @logger.info("CLI utility '#{name}' path: #{path}") end end diff --git a/lib/vagrant-parallels/driver/pd_9.rb b/lib/vagrant-parallels/driver/pd_9.rb index 1575badb..d886a70e 100644 --- a/lib/vagrant-parallels/driver/pd_9.rb +++ b/lib/vagrant-parallels/driver/pd_9.rb @@ -248,17 +248,19 @@ def read_vms_paths end def read_bridged_interfaces - net_list = read_virtual_networks() + net_list = read_virtual_networks # Skip 'vnicXXX' and 'Default' interfaces net_list.delete_if do |net| - net['Type'] != "bridged" or net['Bound To'] =~ /^(vnic(.+?)|Default)$/ + net['Type'] != "bridged" or + net['Bound To'] =~ /^(vnic(.+?))$/ or + net['Network ID'] == "Default" end bridged_ifaces = [] net_list.collect do |iface| info = {} - ifconfig = raw('ifconfig', iface['Bound To']).stdout + ifconfig = execute(:ifconfig, iface['Bound To']) # Assign default values info[:name] = iface['Network ID'].gsub(/\s\(.*?\)$/, '') info[:bound_to] = iface['Bound To'] @@ -288,7 +290,7 @@ def read_guest_tools_version end def read_host_only_interfaces - net_list = read_virtual_networks() + net_list = read_virtual_networks net_list.keep_if { |net| net['Type'] == "host-only" } hostonly_ifaces = [] From 5b1ceda3ebd16e5e9b6d4033ac0f10b1eedd51a2 Mon Sep 17 00:00:00 2001 From: Mikhail Zholobov Date: Mon, 17 Feb 2014 14:39:09 +0400 Subject: [PATCH 16/33] driver and unit-tests refactoring [GH-75] --- lib/vagrant-parallels/driver/meta.rb | 1 + lib/vagrant-parallels/driver/pd_9.rb | 27 +- test/support/isolated_environment.rb | 46 --- test/support/tempdir.rb | 43 --- test/unit/base.rb | 6 +- test/unit/config_test.rb | 2 +- test/unit/driver/pd_9_test.rb | 277 +++++++++--------- test/unit/support/shared/parallels_context.rb | 4 +- .../unit/support/shared/pd_driver_examples.rb | 162 ++++++++++ test/unit/synced_folder_test.rb | 2 +- 10 files changed, 325 insertions(+), 245 deletions(-) delete mode 100644 test/support/isolated_environment.rb delete mode 100644 test/support/tempdir.rb create mode 100644 test/unit/support/shared/pd_driver_examples.rb diff --git a/lib/vagrant-parallels/driver/meta.rb b/lib/vagrant-parallels/driver/meta.rb index a1083176..b998ad8e 100644 --- a/lib/vagrant-parallels/driver/meta.rb +++ b/lib/vagrant-parallels/driver/meta.rb @@ -98,6 +98,7 @@ def initialize(uuid=nil) :read_host_only_interfaces, :read_mac_address, :read_network_interfaces, + :read_settings, :read_state, :read_used_ports, :read_virtual_networks, diff --git a/lib/vagrant-parallels/driver/pd_9.rb b/lib/vagrant-parallels/driver/pd_9.rb index d886a70e..1b3297dd 100644 --- a/lib/vagrant-parallels/driver/pd_9.rb +++ b/lib/vagrant-parallels/driver/pd_9.rb @@ -189,14 +189,11 @@ def import(template_uuid) end def mac_in_use?(mac) - all_macs_in_use = [] + valid_mac = mac.upcase.tr('^A-F0-9', '') read_vms_info.each do |vm| - all_macs_in_use << vm.fetch('Hardware', {}).fetch('net0',{}).fetch('mac', '') + return true if valid_mac == vm.fetch('Hardware', {}).fetch('net0',{}).fetch('mac', '') end - - valid_mac = mac.upcase.tr('^A-F0-9', '') - - all_macs_in_use.include?(valid_mac) + false end def read_ip_dhcp @@ -211,10 +208,10 @@ def read_ip_dhcp def read_vms results = {} - vms_arr = json({}) do + vms_arr = json([]) do execute('list', '--all', '--json', retryable: true).gsub(/^(INFO)?/, '') end - templates_arr = json({}) do + templates_arr = json([]) do execute('list', '--all', '--json', '--template', retryable: true).gsub(/^(INFO)?/, '') end vms = vms_arr | templates_arr @@ -227,10 +224,10 @@ def read_vms # Parse the JSON from *all* VMs and templates. Then return an array of objects (without duplicates) def read_vms_info - vms_arr = json({}) do + vms_arr = json([]) do execute('list', '--all','--info', '--json', retryable: true).gsub(/^(INFO)?/, '') end - templates_arr = json({}) do + templates_arr = json([]) do execute('list', '--all','--info', '--json', '--template', retryable: true).gsub(/^(INFO)?/, '') end vms_arr | templates_arr @@ -353,14 +350,14 @@ def read_network_interfaces nics end - def read_settings(uuid=nil) - uuid ||= @uuid - json({}) { execute('list', uuid, '--info', '--json', retryable: true).gsub(/^(INFO)?\[/, '').gsub(/\]$/, '') } + def read_settings + vm = json { execute('list', @uuid, '--info', '--json', retryable: true).gsub(/^(INFO)?/, '') } + vm.last end def read_state - vm = json({}) { execute('list', @uuid, '--json', retryable: true).gsub(/^(INFO)?\[/, '').gsub(/\]$/, '') } - vm.fetch('status', 'TROLOLO').to_sym + vm = json { execute('list', @uuid, '--json', retryable: true).gsub(/^(INFO)?/, '') } + vm.last.fetch('status').to_sym end def read_virtual_networks diff --git a/test/support/isolated_environment.rb b/test/support/isolated_environment.rb deleted file mode 100644 index ec61b205..00000000 --- a/test/support/isolated_environment.rb +++ /dev/null @@ -1,46 +0,0 @@ -require "fileutils" -require "pathname" - -require "log4r" - -require "support/tempdir" - -# This class manages an isolated environment for Vagrant to -# run in. It creates a temporary directory to act as the -# working directory as well as sets a custom home directory. -# -# This class also provides various helpers to create Vagrantfiles, -# boxes, etc. -class IsolatedEnvironment - attr_reader :homedir - attr_reader :workdir - - # Initializes an isolated environment. You can pass in some - # options here to configure runing custom applications in place - # of others as well as specifying environmental variables. - # - # @param [Hash] apps A mapping of application name (such as "vagrant") - # to an alternate full path to the binary to run. - # @param [Hash] env Additional environmental variables to inject - # into the execution environments. - def initialize - @logger = Log4r::Logger.new("test::isolated_environment") - - # Create a temporary directory for our work - @tempdir = Tempdir.new("vagrant") - @logger.info("Initialize isolated environment: #{@tempdir.path}") - - # Setup the home and working directories - @homedir = Pathname.new(File.join(@tempdir.path, "home")) - @workdir = Pathname.new(File.join(@tempdir.path, "work")) - - @homedir.mkdir - @workdir.mkdir - end - - # This closes the environment by cleaning it up. - def close - @logger.info("Removing isolated environment: #{@tempdir.path}") - FileUtils.rm_rf(@tempdir.path) - end -end diff --git a/test/support/tempdir.rb b/test/support/tempdir.rb deleted file mode 100644 index 5c7d7dee..00000000 --- a/test/support/tempdir.rb +++ /dev/null @@ -1,43 +0,0 @@ -require 'fileutils' -require 'tempfile' - -# This class provides an easy way of creating a temporary -# directory and having it removed when the application exits. -class Tempdir - attr_reader :path - - def initialize(basename="vagrant") - @path = nil - - # Loop and attempt to create a temporary directory until - # it succeeds. - while @path.nil? - file = Tempfile.new(basename) - @path = file.path - file.unlink - - begin - Dir.mkdir(@path) - rescue - @path = nil - end - end - - # Setup a finalizer to delete the directory. This is the same way - # that Tempfile and friends do this... - @cleanup_proc = lambda do - FileUtils.rm_rf(@path) if File.directory?(@path) - end - - ObjectSpace.define_finalizer(self, @cleanup_proc) - end - - # This deletes the temporary directory. - def unlink - # Delete the directory - @cleanup_proc.call - - # Undefine the finalizer since we're all cleaned up - ObjectSpace.undefine_finalizer(self) - end -end diff --git a/test/unit/base.rb b/test/unit/base.rb index bb775014..a837036b 100644 --- a/test/unit/base.rb +++ b/test/unit/base.rb @@ -10,8 +10,8 @@ $:.unshift File.expand_path("../../", __FILE__) # Load in helpers -require "support/tempdir" require "unit/support/shared/parallels_context" +require "unit/support/shared/pd_driver_examples" # Do not buffer output $stdout.sync = true @@ -21,7 +21,3 @@ RSpec.configure do |c| c.expect_with :rspec, :stdlib end - -# Configure VAGRANT_CWD so that the tests never find an actual -# Vagrantfile anywhere, or at least this minimizes those chances. -ENV["VAGRANT_CWD"] = Tempdir.new.path diff --git a/test/unit/config_test.rb b/test/unit/config_test.rb index b55ca48b..54bec7eb 100644 --- a/test/unit/config_test.rb +++ b/test/unit/config_test.rb @@ -1,4 +1,4 @@ -require_relative "../unit/base" +require_relative "base" require VagrantPlugins::Parallels.source_root.join('lib/vagrant-parallels/config') diff --git a/test/unit/driver/pd_9_test.rb b/test/unit/driver/pd_9_test.rb index 89c2f978..eff4b548 100644 --- a/test/unit/driver/pd_9_test.rb +++ b/test/unit/driver/pd_9_test.rb @@ -4,145 +4,158 @@ include_context "parallels" let(:parallels_version) { "9" } - subject { described_class.new(uuid) } - - describe "compact" do - it "compacts the VM disk images" do - pending "Should have possibility to compact more than one hdd" + let(:vm_name) {'VM_Name'} + let(:vm_net0_mac) {'001C42B4B074'} + let(:vm_net1_mac) {'001C42EC0068'} + let(:vm_hdd) {'/path/to/disk1.hdd'} + + let(:tpl_uuid) {'1234-some-template-uuid-5678'} + let(:tpl_name) {'Some_Template_Name'} + let(:tpl_net0_mac) {'001C42F6E500'} + let(:tpl_net1_mac) {'001C42AB0071'} + + subject { VagrantPlugins::Parallels::Driver::Meta.new(uuid) } + + it_behaves_like "parallels desktop driver" + + before do + # Returns short info about all registered VMs + # `prlctl list --all --json` + subprocess.stub(:execute). + with("prlctl", "list", "--all", "--json", kind_of(Hash)) do + out = <<-eos + [ + { + "uuid": "#{uuid}", + "status": "stopped", + "name": "#{vm_name}" + } + ] + eos + subprocess_result(stdout: out) end - end - - describe "create_host_only_network" do - it "creates host-only NIC" - end - - describe "export" do - tpl_name = "new_template_name" - tpl_uuid = "12345-hfgs-3456-hste" - it "exports VM to template" do - subject.stub(:read_vms).and_return({tpl_name => tpl_uuid}) - - subprocess.should_receive(:execute). - with("prlctl", "clone", uuid, "--name", an_instance_of(String), "--template", "--dst", - an_instance_of(String), an_instance_of(Hash)). - and_return(subprocess_result(stdout: "The VM has been successfully cloned")) - subject.export("/path/to/template", tpl_name).should == tpl_uuid + # Returns short info about all registered templates + # `prlctl list --all --json --template` + subprocess.stub(:execute). + with("prlctl", "list", "--all", "--json", "--template", kind_of(Hash)) do + out = <<-eos + [ + { + "uuid": "1234-some-template-uuid-5678", + "name": "Some_Template_Name" + } + ] + eos + subprocess_result(stdout: out) end - end - - describe "clear_shared_folders" do - shf_hash = {"enabled" => true, "shf_name_1" => {}, "shf_name_2" => {}} - it "deletes every shared folder assigned to the VM" do - subject.stub(:read_settings).and_return({"Host Shared Folders" => shf_hash}) - subprocess.should_receive(:execute).exactly(2).times. - with("prlctl", "set", uuid, "--shf-host-del", an_instance_of(String), an_instance_of(Hash)). - and_return(subprocess_result(stdout: "Shared folder deleted")) - subject.clear_shared_folders - end - end - - describe "halt" do - it "stops the VM" do - subprocess.should_receive(:execute). - with("prlctl", "stop", uuid, an_instance_of(Hash)). - and_return(subprocess_result(stdout: "VM has been halted gracefully")) - subject.halt - end - - it "stops the VM force" do - subprocess.should_receive(:execute). - with("prlctl", "stop", uuid, "--kill", an_instance_of(Hash)). - and_return(subprocess_result(stdout: "VM has been halted forcibly")) - subject.halt(force=true) - end - end - - describe "mac_in_use?" do - vm_1 = { - 'Hardware' => { - 'net0' => {'mac' => '001C42BB5901'}, - 'net1' => {'mac' => '001C42BB5902'}, - } - } - vm_2 = { - 'Hardware' => { - 'net0' => {'mac' => '001C42BB5903'}, - 'net1' => {'mac' => '001C42BB5904'}, - } - } - - it "checks the MAC address is already in use" do - subject.stub(:read_vms_info).and_return([vm_1, vm_2]) - - subject.mac_in_use?('00:1c:42:bb:59:01').should be_true - subject.mac_in_use?('00:1c:42:bb:59:02').should be_false - subject.mac_in_use?('00:1c:42:bb:59:03').should be_true - subject.mac_in_use?('00:1c:42:bb:59:04').should be_false - end - end - - describe "set_name" do - it "sets new name for the VM" do - subprocess.should_receive(:execute). - with("prlctl", "set", uuid, '--name', an_instance_of(String), an_instance_of(Hash)). - and_return(subprocess_result(stdout: "Settings applied")) - - subject.set_name('new_vm_name') - end - end - - describe "set_mac_address" do - it "sets base MAC address to the Shared network adapter" do - subprocess.should_receive(:execute).exactly(2).times. - with("prlctl", "set", uuid, '--device-set', 'net0', '--type', 'shared', '--mac', - an_instance_of(String), an_instance_of(Hash)). - and_return(subprocess_result(stdout: "Settings applied")) - - subject.set_mac_address('001C42DD5902') - subject.set_mac_address('auto') - end - end - - describe "start" do - it "starts the VM" do - subprocess.should_receive(:execute). - with("prlctl", "start", uuid, an_instance_of(Hash)). - and_return(subprocess_result(stdout: "VM started")) - subject.start - end - end - - describe "suspend" do - it "suspends the VM" do - subprocess.should_receive(:execute). - with("prlctl", "suspend", uuid, an_instance_of(Hash)). - and_return(subprocess_result(stdout: "VM suspended")) - subject.suspend - end - end - - describe "unregister" do - it "suspends the VM" do - subprocess.should_receive(:execute). - with("prlctl", "unregister", an_instance_of(String), an_instance_of(Hash)). - and_return(subprocess_result(stdout: "Specified VM unregistered")) - subject.unregister("template_or_vm_uuid") - end - end - describe "version" do - it "parses the version from output" do - subject.version.should match(/(#{parallels_version}[\d\.]+)/) + # Returns detailed info about specified VM or all registered VMs + # `prlctl list SOME-VM-UUID --info --json` + # `prlctl list --all --info --json` + subprocess.stub(:execute). + with("prlctl", "list", kind_of(String), "--info", "--json", kind_of(Hash))do + out = <<-eos + [ + { + "ID": "#{uuid}", + "Name": "#{vm_name}", + "State": "stopped", + "Home": "/path/to/#{vm_name}.pvm/", + "GuestTools": { + "version": "9.0.23062" + }, + "Hardware": { + "cpu": { + "cpus": 1 + }, + "memory": { + "size": "512Mb" + }, + "hdd0": { + "enabled": true, + "image": "#{vm_hdd}" + }, + "net0": { + "enabled": true, + "type": "shared", + "mac": "#{vm_net0_mac}", + "card": "e1000", + "dhcp": "yes" + }, + "net1": { + "enabled": true, + "type": "bridged", + "iface": "vnic2", + "mac": "#{vm_net1_mac}", + "card": "e1000", + "ips": "33.33.33.5/255.255.255.0 " + } + }, + "Host Shared Folders": { + "enabled": true, + "shared_folder_1": { + "enabled": true, + "path": "/path/to/shared/folder/1" + }, + "shared_folder_2": { + "enabled": true, + "path": "/path/to/shared/folder/2" + } + } + } + ] + eos + subprocess_result(stdout: out) end - it "rises ParallelsInstallIncomplete exception when output is invalid" do - subprocess.should_receive(:execute). - with("prlctl", "--version", an_instance_of(Hash)). - and_return(subprocess_result(stdout: "Some incorrect value has been returned!")) - expect { subject.version }. - to raise_error(VagrantPlugins::Parallels::Errors::ParallelsInstallIncomplete) + # Returns detailed info about specified template or all registered templates + # `prlctl list some_vm_uuid --info --json --template` + # `prlctl list --all --info --json --template` + subprocess.stub(:execute). + with("prlctl", "list", kind_of(String), "--info", "--json", "--template", kind_of(Hash))do + out = <<-eos + [ + { + "ID": "#{tpl_uuid}", + "Name": "#{tpl_name}", + "State": "stopped", + "Home": "/path/to/#{tpl_name}.pvm/", + "GuestTools": { + "version": "9.0.24172" + }, + "Hardware": { + "cpu": { + "cpus": 1 + }, + "memory": { + "size": "512Mb" + }, + "hdd0": { + "enabled": true, + "image": "/path/to/harddisk.hdd" + }, + "net0": { + "enabled": true, + "type": "shared", + "mac": "#{tpl_net0_mac}", + "card": "e1000", + "dhcp": "yes" + }, + "net1": { + "enabled": true, + "type": "bridged", + "iface": "vnic4", + "mac": "#{tpl_net1_mac}", + "card": "e1000", + "ips": "33.33.33.10/255.255.255.0 " + } + } + } + ] + eos + subprocess_result(stdout: out) end end end diff --git a/test/unit/support/shared/parallels_context.rb b/test/unit/support/shared/parallels_context.rb index 1689464e..4c2bb2a4 100644 --- a/test/unit/support/shared/parallels_context.rb +++ b/test/unit/support/shared/parallels_context.rb @@ -1,6 +1,6 @@ shared_context "parallels" do let(:parallels_context) { true } - let(:uuid) { "9876-dcba-8765-hgfe" } + let(:uuid) { "1234-here-is-uuid-5678" } let(:parallels_version) { "9" } let(:subprocess) { double("Vagrant::Util::Subprocess") } @@ -24,7 +24,7 @@ def subprocess_result(options={}) # drivers also call vm_exists? during init; subprocess.stub(:execute). - with("prlctl", "list", kind_of(String), "--info", "--json", kind_of(Hash)). + with("prlctl", "list", uuid, kind_of(Hash)). and_return(subprocess_result(exit_code: 0)) end end diff --git a/test/unit/support/shared/pd_driver_examples.rb b/test/unit/support/shared/pd_driver_examples.rb new file mode 100644 index 00000000..3ef0778d --- /dev/null +++ b/test/unit/support/shared/pd_driver_examples.rb @@ -0,0 +1,162 @@ +shared_examples "parallels desktop driver" do |options| + before do + raise ArgumentError, "Need parallels context to use these shared examples." unless defined? parallels_context + end + + describe "compact" do + it "compacts the VM disk drives" do + subprocess.should_receive(:execute). + with("prl_disk_tool", 'compact', '--hdd', vm_hdd, an_instance_of(Hash)). + and_return(subprocess_result(exit_code: 0)) + subject.compact(uuid) + end + end + + describe "create_host_only_network" do + it "creates host-only NIC" + end + + describe "export" do + tpl_name = "Some_Template_Name" + tpl_uuid = "1234-some-template-uuid-5678" + + it "exports VM to template" do + subprocess.should_receive(:execute). + with("prlctl", "clone", uuid, "--name", an_instance_of(String), "--template", "--dst", + an_instance_of(String), an_instance_of(Hash)). + and_return(subprocess_result(exit_code: 0)) + subject.export("/path/to/template", tpl_name).should == tpl_uuid + end + end + + describe "clear_shared_folders" do + it "deletes every shared folder assigned to the VM" do + subprocess.should_receive(:execute).at_least(2).times. + with("prlctl", "set", uuid, "--shf-host-del", an_instance_of(String), an_instance_of(Hash)). + and_return(subprocess_result(stdout: "Shared folder deleted")) + subject.clear_shared_folders + end + end + + describe "halt" do + it "stops the VM" do + subprocess.should_receive(:execute). + with("prlctl", "stop", uuid, an_instance_of(Hash)). + and_return(subprocess_result(stdout: "VM has been halted gracefully")) + subject.halt + end + + it "stops the VM force" do + subprocess.should_receive(:execute). + with("prlctl", "stop", uuid, "--kill", an_instance_of(Hash)). + and_return(subprocess_result(stdout: "VM has been halted forcibly")) + subject.halt(force=true) + end + end + + describe "mac_in_use?" do + let(:vm_net0_mac) {'00AABBCC01'} + let(:vm_net1_mac) {'00AABBCC02'} + let(:tpl_net0_mac) {'00AABBCC03'} + let(:tpl_net1_mac) {'00AABBCC04'} + + it "checks the MAC address is already in use" do + + subject.mac_in_use?('00:AA:BB:CC:01').should be_true + subject.mac_in_use?('00:AA:BB:CC:02').should be_false + subject.mac_in_use?('00:AA:BB:CC:03').should be_true + subject.mac_in_use?('00:AA:BB:CC:04').should be_false + end + end + + describe "read_settings" do + it "returns a hash with detailed info about the VM" do + subject.read_settings.should be_kind_of(Hash) + subject.read_settings.should include("ID" => uuid) + subject.read_settings.should include("Hardware") + subject.read_settings.should include("GuestTools") + end + end + + describe "read_vms" do + it "returns the list of all registered VMs and templates" do + subject.read_vms.should be_kind_of(Hash) + subject.read_vms.should have_at_least(2).items + subject.read_vms.should include(vm_name => uuid) + end + end + + describe "read_vms_info" do + it "returns detailed info about all registered VMs and templates" do + subject.read_vms_info.should be_kind_of(Array) + subject.read_vms_info.should have_at_least(2).items + + # It should include info about current VM + vm_settings = subject.send(:read_settings) + subject.read_vms_info.should include(vm_settings) + end + end + + describe "set_name" do + it "sets new name for the VM" do + subprocess.should_receive(:execute). + with("prlctl", "set", uuid, '--name', an_instance_of(String), an_instance_of(Hash)). + and_return(subprocess_result(stdout: "Settings applied")) + + subject.set_name('new_vm_name') + end + end + + describe "set_mac_address" do + it "sets base MAC address to the Shared network adapter" do + subprocess.should_receive(:execute).exactly(2).times. + with("prlctl", "set", uuid, '--device-set', 'net0', '--type', 'shared', '--mac', + an_instance_of(String), an_instance_of(Hash)). + and_return(subprocess_result(stdout: "Settings applied")) + + subject.set_mac_address('001C42DD5902') + subject.set_mac_address('auto') + end + end + + describe "start" do + it "starts the VM" do + subprocess.should_receive(:execute). + with("prlctl", "start", uuid, an_instance_of(Hash)). + and_return(subprocess_result(stdout: "VM started")) + subject.start + end + end + + describe "suspend" do + it "suspends the VM" do + subprocess.should_receive(:execute). + with("prlctl", "suspend", uuid, an_instance_of(Hash)). + and_return(subprocess_result(stdout: "VM suspended")) + subject.suspend + end + end + + describe "unregister" do + it "suspends the VM" do + subprocess.should_receive(:execute). + with("prlctl", "unregister", an_instance_of(String), an_instance_of(Hash)). + and_return(subprocess_result(stdout: "Specified VM unregistered")) + subject.unregister("template_or_vm_uuid") + end + end + + describe "version" do + it "parses the version from output" do + subject.version.should match(/(#{parallels_version}[\d\.]+)/) + end + + it "rises ParallelsInstallIncomplete exception when output is invalid" do + subprocess.should_receive(:execute). + with("prlctl", "--version", an_instance_of(Hash)). + and_return(subprocess_result(stdout: "Some incorrect value has been returned!")) + expect { subject.version }. + to raise_error(VagrantPlugins::Parallels::Errors::ParallelsInvalidVersion) + end + end +end diff --git a/test/unit/synced_folder_test.rb b/test/unit/synced_folder_test.rb index 00fd92ed..cbe240ef 100644 --- a/test/unit/synced_folder_test.rb +++ b/test/unit/synced_folder_test.rb @@ -1,5 +1,5 @@ require "vagrant" -require_relative "../unit/base" +require_relative "base" require VagrantPlugins::Parallels.source_root.join('lib/vagrant-parallels/synced_folder') From 1825851eeb892211e38f0611fd981447d1937170 Mon Sep 17 00:00:00 2001 From: Mikhail Zholobov Date: Mon, 17 Feb 2014 21:07:50 +0400 Subject: [PATCH 17/33] unit test for 'create_host_only_network' driver method --- test/unit/driver/pd_9_test.rb | 41 ++++++++++++- .../unit/support/shared/pd_driver_examples.rb | 61 ++++++++++++++++++- 2 files changed, 99 insertions(+), 3 deletions(-) diff --git a/test/unit/driver/pd_9_test.rb b/test/unit/driver/pd_9_test.rb index eff4b548..d3af4afd 100644 --- a/test/unit/driver/pd_9_test.rb +++ b/test/unit/driver/pd_9_test.rb @@ -14,6 +14,19 @@ let(:tpl_net0_mac) {'001C42F6E500'} let(:tpl_net1_mac) {'001C42AB0071'} + let(:hostonly_iface) {'vnic10'} + + let(:vnic_options) do { + :name => 'vagrant_vnic6', + :adapter_ip => '11.11.11.11', + :netmask => '255.255.252.0', + :dhcp => { + :ip => '11.11.11.11', + :lower => '11.11.8.1', + :upper => '11.11.11.254' + } + } end + subject { VagrantPlugins::Parallels::Driver::Meta.new(uuid) } it_behaves_like "parallels desktop driver" @@ -52,7 +65,7 @@ # Returns detailed info about specified VM or all registered VMs - # `prlctl list SOME-VM-UUID --info --json` + # `prlctl list --info --json` # `prlctl list --all --info --json` subprocess.stub(:execute). with("prlctl", "list", kind_of(String), "--info", "--json", kind_of(Hash))do @@ -111,7 +124,7 @@ end # Returns detailed info about specified template or all registered templates - # `prlctl list some_vm_uuid --info --json --template` + # `prlctl list --info --json --template` # `prlctl list --all --info --json --template` subprocess.stub(:execute). with("prlctl", "list", kind_of(String), "--info", "--json", "--template", kind_of(Hash))do @@ -157,5 +170,29 @@ eos subprocess_result(stdout: out) end + + # Returns detailed info about virtual network interface + # `prlsrvctl net info , '--json', retryable: true) + subprocess.stub(:execute). + with("prlsrvctl", "net", "info", kind_of(String), "--json", kind_of(Hash))do + out = <<-eos + { + "Network ID": "#{vnic_options[:name]}", + "Type": "host-only", + "Bound To": "#{hostonly_iface}", + "Parallels adapter": { + "IP address": "#{vnic_options[:adapter_ip]}", + "Subnet mask": "#{vnic_options[:netmask]}" + }, + "DHCPv4 server": { + "Server address": "#{vnic_options[:dhcp][:ip] || "10.37.132.1"}", + "IP scope start address": "#{vnic_options[:dhcp][:lower] || "10.37.132.1"}", + "IP scope end address": "#{vnic_options[:dhcp][:upper] || "10.37.132.254"}" + } + } + eos + subprocess_result(stdout: out) + end + end end diff --git a/test/unit/support/shared/pd_driver_examples.rb b/test/unit/support/shared/pd_driver_examples.rb index 3ef0778d..b8244853 100644 --- a/test/unit/support/shared/pd_driver_examples.rb +++ b/test/unit/support/shared/pd_driver_examples.rb @@ -13,7 +13,66 @@ end describe "create_host_only_network" do - it "creates host-only NIC" + let(:hostonly_iface) {'vnic12'} + it "creates host-only NIC with dhcp server configured" do + vnic_options = { + :name => 'vagrant_vnic8', + :adapter_ip => '11.11.11.11', + :netmask => '255.255.252.0', + :dhcp => { + :ip => '11.11.11.11', + :lower => '11.11.8.1', + :upper => '11.11.11.254' + } + } + + subprocess.should_receive(:execute). + with("prlsrvctl", "net", "add", vnic_options[:name], "--type", "host-only", an_instance_of(Hash)). + and_return(subprocess_result(exit_code: 0)) + + subprocess.should_receive(:execute). + with("prlsrvctl", "net", "set", vnic_options[:name], + "--ip", "#{vnic_options[:adapter_ip]}/#{vnic_options[:netmask]}", + "--dhcp-ip", vnic_options[:dhcp][:ip], + "--ip-scope-start", vnic_options[:dhcp][:lower], + "--ip-scope-end", vnic_options[:dhcp][:upper], an_instance_of(Hash)). + and_return(subprocess_result(exit_code: 0)) + + interface = subject.create_host_only_network(vnic_options) + + interface.should include(:name => vnic_options[:name]) + interface.should include(:ip => vnic_options[:adapter_ip]) + interface.should include(:netmask => vnic_options[:netmask]) + interface.should include(:dhcp => vnic_options[:dhcp]) + interface.should include(:bound_to => hostonly_iface) + interface[:bound_to].should =~ /^(vnic(\d+))$/ + end + + it "creates host-only NIC" do + vnic_options = { + :name => 'vagrant_vnic3', + :adapter_ip => '22.22.22.22', + :netmask => '255.255.254.0', + } + + subprocess.should_receive(:execute). + with("prlsrvctl", "net", "add", vnic_options[:name], "--type", "host-only", an_instance_of(Hash)). + and_return(subprocess_result(exit_code: 0)) + + subprocess.should_receive(:execute). + with("prlsrvctl", "net", "set", vnic_options[:name], + "--ip", "#{vnic_options[:adapter_ip]}/#{vnic_options[:netmask]}", an_instance_of(Hash)). + and_return(subprocess_result(exit_code: 0)) + + interface = subject.create_host_only_network(vnic_options) + + interface.should include(:name => vnic_options[:name]) + interface.should include(:ip => vnic_options[:adapter_ip]) + interface.should include(:netmask => vnic_options[:netmask]) + interface.should include(:dhcp => nil) + interface.should include(:bound_to => hostonly_iface) + interface[:bound_to].should =~ /^(vnic(\d+))$/ + end end describe "export" do From ff879409b6ec862d7b9f2adcade50692dd9e0181 Mon Sep 17 00:00:00 2001 From: Mikhail Zholobov Date: Tue, 18 Feb 2014 23:14:59 +0400 Subject: [PATCH 18/33] driver and unit-tests refactoring --- .../action/clear_network_interfaces.rb | 2 +- lib/vagrant-parallels/driver/base.rb | 4 + lib/vagrant-parallels/driver/meta.rb | 4 +- lib/vagrant-parallels/driver/pd_9.rb | 124 +++++++++--------- test/unit/driver/pd_9_test.rb | 7 + .../unit/support/shared/pd_driver_examples.rb | 42 ++++-- 6 files changed, 105 insertions(+), 78 deletions(-) diff --git a/lib/vagrant-parallels/action/clear_network_interfaces.rb b/lib/vagrant-parallels/action/clear_network_interfaces.rb index f13e54fd..94f74e7f 100644 --- a/lib/vagrant-parallels/action/clear_network_interfaces.rb +++ b/lib/vagrant-parallels/action/clear_network_interfaces.rb @@ -9,7 +9,7 @@ def initialize(app, env) def call(env) # Delete all disabled network adapters env[:ui].info I18n.t("vagrant.actions.vm.clear_network_interfaces.deleting") - env[:machine].provider.driver.delete_adapters + env[:machine].provider.driver.delete_disabled_adapters @app.call(env) end diff --git a/lib/vagrant-parallels/driver/base.rb b/lib/vagrant-parallels/driver/base.rb index 9862ed2d..1637e410 100644 --- a/lib/vagrant-parallels/driver/base.rb +++ b/lib/vagrant-parallels/driver/base.rb @@ -53,6 +53,10 @@ def create_host_only_network(options) def delete end + # Deletes all disabled network adapters from the VM configuration + def delete_disabled_adapters + end + # Deletes any host only networks that aren't being used for anything. def delete_unused_host_only_networks end diff --git a/lib/vagrant-parallels/driver/meta.rb b/lib/vagrant-parallels/driver/meta.rb index b998ad8e..abf84f3b 100644 --- a/lib/vagrant-parallels/driver/meta.rb +++ b/lib/vagrant-parallels/driver/meta.rb @@ -76,12 +76,10 @@ def initialize(uuid=nil) #:clear_forwarded_ports, :clear_shared_folders, :compact, - #:create_dhcp_server, :create_host_only_network, :delete, - :delete_adapters, + :delete_disabled_adapters, :delete_unused_host_only_networks, - #:discard_saved_state, :enable_adapters, :execute_command, :export, diff --git a/lib/vagrant-parallels/driver/pd_9.rb b/lib/vagrant-parallels/driver/pd_9.rb index 1b3297dd..506d2cd6 100644 --- a/lib/vagrant-parallels/driver/pd_9.rb +++ b/lib/vagrant-parallels/driver/pd_9.rb @@ -31,6 +31,14 @@ def compact(uuid) end end + def clear_shared_folders + shf = read_settings.fetch("Host Shared Folders", {}).keys + shf.delete("enabled") + shf.each do |folder| + execute("set", @uuid, "--shf-host-del", folder) + end + end + def create_host_only_network(options) # Create the interface execute(:prlsrvctl, "net", "add", options[:name], "--type", "host-only") @@ -59,19 +67,11 @@ def create_host_only_network(options) } end - def clear_shared_folders - shf = read_settings.fetch("Host Shared Folders", {}).keys - shf.delete("enabled") - shf.each do |folder| - execute("set", @uuid, "--shf-host-del", folder) - end - end - def delete execute('delete', @uuid) end - def delete_adapters + def delete_disabled_adapters read_settings.fetch('Hardware', {}).each do |adapter, params| if adapter.start_with?('net') and !params.fetch("enabled", true) execute('set', @uuid, '--device-del', adapter) @@ -155,6 +155,10 @@ def enable_adapters(adapters) end end + def execute_command(command) + execute(*command) + end + def export(path, tpl_name) execute("clone", @uuid, "--name", tpl_name, "--template", "--dst", path.to_s) do |type, data| lines = data.split("\r") @@ -196,54 +200,6 @@ def mac_in_use?(mac) false end - def read_ip_dhcp - mac_addr = read_mac_address.downcase - File.foreach("/Library/Preferences/Parallels/parallels_dhcp_leases") do |line| - if line.include? mac_addr - ip = line[/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/] - return ip - end - end - end - - def read_vms - results = {} - vms_arr = json([]) do - execute('list', '--all', '--json', retryable: true).gsub(/^(INFO)?/, '') - end - templates_arr = json([]) do - execute('list', '--all', '--json', '--template', retryable: true).gsub(/^(INFO)?/, '') - end - vms = vms_arr | templates_arr - vms.each do |item| - results[item.fetch('name')] = item.fetch('uuid') - end - - results - end - - # Parse the JSON from *all* VMs and templates. Then return an array of objects (without duplicates) - def read_vms_info - vms_arr = json([]) do - execute('list', '--all','--info', '--json', retryable: true).gsub(/^(INFO)?/, '') - end - templates_arr = json([]) do - execute('list', '--all','--info', '--json', '--template', retryable: true).gsub(/^(INFO)?/, '') - end - vms_arr | templates_arr - end - - def read_vms_paths - list = {} - read_vms_info.each do |item| - if Dir.exists? item.fetch('Home') - list[File.realpath item.fetch('Home')] = item.fetch('ID') - end - end - - list - end - def read_bridged_interfaces net_list = read_virtual_networks @@ -317,6 +273,16 @@ def read_host_only_interfaces hostonly_ifaces end + def read_ip_dhcp + mac_addr = read_mac_address.downcase + File.foreach("/Library/Preferences/Parallels/parallels_dhcp_leases") do |line| + if line.include? mac_addr + ip = line[/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/] + return ip + end + end + end + def read_mac_address read_settings.fetch('Hardware', {}).fetch('net0', {}).fetch('mac', nil) end @@ -364,6 +330,44 @@ def read_virtual_networks json { execute(:prlsrvctl, 'net', 'list', '--json', retryable: true) } end + def read_vms + results = {} + vms_arr = json([]) do + execute('list', '--all', '--json', retryable: true).gsub(/^(INFO)?/, '') + end + templates_arr = json([]) do + execute('list', '--all', '--json', '--template', retryable: true).gsub(/^(INFO)?/, '') + end + vms = vms_arr | templates_arr + vms.each do |item| + results[item.fetch('name')] = item.fetch('uuid') + end + + results + end + + # Parse the JSON from *all* VMs and templates. Then return an array of objects (without duplicates) + def read_vms_info + vms_arr = json([]) do + execute('list', '--all','--info', '--json', retryable: true).gsub(/^(INFO)?/, '') + end + templates_arr = json([]) do + execute('list', '--all','--info', '--json', '--template', retryable: true).gsub(/^(INFO)?/, '') + end + vms_arr | templates_arr + end + + def read_vms_paths + list = {} + read_vms_info.each do |item| + if Dir.exists? item.fetch('Home') + list[File.realpath item.fetch('Home')] = item.fetch('ID') + end + end + + list + end + def register(pvm_file) execute("register", pvm_file) end @@ -384,10 +388,6 @@ def set_name(name) execute('set', @uuid, '--name', name, :retryable => true) end - def execute_command(command) - execute(*command) - end - def share_folders(folders) folders.each do |folder| # Add the shared folder @@ -396,7 +396,7 @@ def share_folders(folders) end def ssh_port(expected_port) - 22 + expected_port end def start diff --git a/test/unit/driver/pd_9_test.rb b/test/unit/driver/pd_9_test.rb index d3af4afd..f08c91c9 100644 --- a/test/unit/driver/pd_9_test.rb +++ b/test/unit/driver/pd_9_test.rb @@ -104,6 +104,13 @@ "mac": "#{vm_net1_mac}", "card": "e1000", "ips": "33.33.33.5/255.255.255.0 " + }, + "net2": { + "enabled": false, + "type": "bridged", + "iface": "vnic5", + "mac": "001C42FFFFFF", + "card": "virtio" } }, "Host Shared Folders": { diff --git a/test/unit/support/shared/pd_driver_examples.rb b/test/unit/support/shared/pd_driver_examples.rb index b8244853..6a8899de 100644 --- a/test/unit/support/shared/pd_driver_examples.rb +++ b/test/unit/support/shared/pd_driver_examples.rb @@ -12,6 +12,15 @@ end end + describe "clear_shared_folders" do + it "deletes every shared folder assigned to the VM" do + subprocess.should_receive(:execute).at_least(2).times. + with("prlctl", "set", uuid, "--shf-host-del", an_instance_of(String), an_instance_of(Hash)). + and_return(subprocess_result(stdout: "Shared folder deleted")) + subject.clear_shared_folders + end + end + describe "create_host_only_network" do let(:hostonly_iface) {'vnic12'} it "creates host-only NIC with dhcp server configured" do @@ -48,7 +57,7 @@ interface[:bound_to].should =~ /^(vnic(\d+))$/ end - it "creates host-only NIC" do + it "creates host-only NIC without dhcp" do vnic_options = { :name => 'vagrant_vnic3', :adapter_ip => '22.22.22.22', @@ -75,6 +84,24 @@ end end + describe "delete" do + it "deletes the VM" do + subprocess.should_receive(:execute). + with("prlctl", "delete", uuid, an_instance_of(Hash)). + and_return(subprocess_result(exit_code: 0)) + subject.delete + end + end + + describe "delete_disabled_adapters" do + it "deletes disabled networks adapters from VM config" do + subprocess.should_receive(:execute). + with("prlctl", "set", uuid, "--device-del", /^(net(\d+))$/, an_instance_of(Hash)). + and_return(subprocess_result(exit_code: 0)) + subject.delete_disabled_adapters + end + end + describe "export" do tpl_name = "Some_Template_Name" tpl_uuid = "1234-some-template-uuid-5678" @@ -88,27 +115,18 @@ end end - describe "clear_shared_folders" do - it "deletes every shared folder assigned to the VM" do - subprocess.should_receive(:execute).at_least(2).times. - with("prlctl", "set", uuid, "--shf-host-del", an_instance_of(String), an_instance_of(Hash)). - and_return(subprocess_result(stdout: "Shared folder deleted")) - subject.clear_shared_folders - end - end - describe "halt" do it "stops the VM" do subprocess.should_receive(:execute). with("prlctl", "stop", uuid, an_instance_of(Hash)). - and_return(subprocess_result(stdout: "VM has been halted gracefully")) + and_return(subprocess_result(exit_code: 0)) subject.halt end it "stops the VM force" do subprocess.should_receive(:execute). with("prlctl", "stop", uuid, "--kill", an_instance_of(Hash)). - and_return(subprocess_result(stdout: "VM has been halted forcibly")) + and_return(subprocess_result(exit_code: 0)) subject.halt(force=true) end end From 0c09e9c042d1f56e98839cb966e682dbd36b2006 Mon Sep 17 00:00:00 2001 From: Mikhail Zholobov Date: Thu, 20 Feb 2014 00:13:08 +0400 Subject: [PATCH 19/33] 'pd_8' and 'pd_9' drivers are separated from each other --- lib/vagrant-parallels/driver/meta.rb | 2 +- lib/vagrant-parallels/driver/pd_8.rb | 432 +++++++++++++++++++++++++++ lib/vagrant-parallels/driver/pd_9.rb | 12 +- test/unit/driver/pd_8_test.rb | 207 +++++++++++++ 4 files changed, 646 insertions(+), 7 deletions(-) create mode 100644 lib/vagrant-parallels/driver/pd_8.rb create mode 100644 test/unit/driver/pd_8_test.rb diff --git a/lib/vagrant-parallels/driver/meta.rb b/lib/vagrant-parallels/driver/meta.rb index abf84f3b..54e29807 100644 --- a/lib/vagrant-parallels/driver/meta.rb +++ b/lib/vagrant-parallels/driver/meta.rb @@ -43,7 +43,7 @@ def initialize(uuid=nil) @logger.debug("Finding driver for Parallels Desktop version: #{@version}") driver_map = { #TODO: Use customized class for each version - "8" => PD_9, + "8" => PD_8, "9" => PD_9, "10" => PD_9 } diff --git a/lib/vagrant-parallels/driver/pd_8.rb b/lib/vagrant-parallels/driver/pd_8.rb new file mode 100644 index 00000000..a240e68c --- /dev/null +++ b/lib/vagrant-parallels/driver/pd_8.rb @@ -0,0 +1,432 @@ +require 'log4r' + +require 'vagrant/util/platform' + +require File.expand_path("../base", __FILE__) + +module VagrantPlugins + module Parallels + module Driver + # Driver for Parallels Desktop 8. + class PD_8 < Base + def initialize(uuid) + super() + + @logger = Log4r::Logger.new("vagrant::provider::parallels::pd_8") + @uuid = uuid + end + + + def compact(uuid) + used_drives = read_settings.fetch('Hardware', {}).select { |name, _| name.start_with? 'hdd' } + used_drives.each_value do |drive_params| + execute(:prl_disk_tool, 'compact', '--hdd', drive_params["image"]) do |type, data| + lines = data.split("\r") + # The progress of the compact will be in the last line. Do a greedy + # regular expression to find what we're looking for. + if lines.last =~ /.+?(\d{,3}) ?%/ + yield $1.to_i if block_given? + end + end + end + end + + def clear_shared_folders + shf = read_settings.fetch("Host Shared Folders", {}).keys + shf.delete("enabled") + shf.each do |folder| + execute("set", @uuid, "--shf-host-del", folder) + end + end + + def create_host_only_network(options) + # Create the interface + execute(:prlsrvctl, "net", "add", options[:name], "--type", "host-only") + + # Configure it + args = ["--ip", "#{options[:adapter_ip]}/#{options[:netmask]}"] + if options[:dhcp] + args.concat(["--dhcp-ip", options[:dhcp][:ip], + "--ip-scope-start", options[:dhcp][:lower], + "--ip-scope-end", options[:dhcp][:upper]]) + end + + execute(:prlsrvctl, "net", "set", options[:name], *args) + + # Determine interface to which it has been bound + net_info = json { execute(:prlsrvctl, 'net', 'info', options[:name], '--json', retryable: true) } + bound_to = net_info['Bound To'] + + # Return the details + return { + :name => options[:name], + :bound_to => bound_to, + :ip => options[:adapter_ip], + :netmask => options[:netmask], + :dhcp => options[:dhcp] + } + end + + def delete + execute('delete', @uuid) + end + + def delete_disabled_adapters + read_settings.fetch('Hardware', {}).each do |adapter, params| + if adapter.start_with?('net') and !params.fetch("enabled", true) + execute('set', @uuid, '--device-del', adapter) + end + end + end + + def delete_unused_host_only_networks + networks = read_virtual_networks + + # 'Shared'(vnic0) and 'Host-Only'(vnic1) are default in Parallels Desktop + # They should not be deleted anyway. + networks.keep_if do |net| + net['Type'] == "host-only" && + net['Bound To'].match(/^(?>vnic|Parallels Host-Only #)(\d+)$/)[1].to_i >= 2 + end + + read_vms_info.each do |vm| + used_nets = vm.fetch('Hardware', {}).select { |name, _| name.start_with? 'net' } + used_nets.each_value do |net_params| + networks.delete_if { |net| net['Bound To'] == net_params.fetch('iface', nil) } + end + + end + + networks.each do |net| + # Delete the actual host only network interface. + execute(:prlsrvctl, "net", "del", net["Network ID"]) + end + end + + def enable_adapters(adapters) + # Get adapters which have already configured for this VM + # Such adapters will be just overridden + existing_adapters = read_settings.fetch('Hardware', {}).keys.select { |name| name.start_with? 'net' } + + # Disable all previously existing adapters (except shared 'vnet0') + existing_adapters.each do |adapter| + if adapter != 'vnet0' + execute('set', @uuid, '--device-set', adapter, '--disable') + end + end + + adapters.each do |adapter| + args = [] + if existing_adapters.include? "net#{adapter[:adapter]}" + args.concat(["--device-set","net#{adapter[:adapter]}", "--enable"]) + else + args.concat(["--device-add", "net"]) + end + + if adapter[:hostonly] or adapter[:bridge] + # Oddly enough, but there is a 'bridge' anyway. + # The only difference is the destination interface: + # - in host-only (private) network it will be bridged to the 'vnicX' device + # - in real bridge (public) network it will be bridged to the assigned device + args.concat(["--type", "bridged", "--iface", adapter[:bound_to]]) + end + + if adapter[:shared] + args.concat(["--type", "shared"]) + end + + if adapter[:dhcp] + args.concat(["--dhcp", "yes"]) + elsif adapter[:ip] + args.concat(["--ipdel", "all", "--ipadd", "#{adapter[:ip]}/#{adapter[:netmask]}"]) + else + args.concat(["--dhcp", "no"]) + end + + if adapter[:mac_address] + args.concat(["--mac", adapter[:mac_address]]) + end + + if adapter[:nic_type] + args.concat(["--adapter-type", adapter[:nic_type].to_s]) + end + + execute("set", @uuid, *args) + end + end + + def execute_command(command) + execute(*command) + end + + def export(path, tpl_name) + execute("clone", @uuid, "--name", tpl_name, "--template", "--dst", path.to_s) do |type, data| + lines = data.split("\r") + # The progress of the export will be in the last line. Do a greedy + # regular expression to find what we're looking for. + if lines.last =~ /.+?(\d{,3}) ?%/ + yield $1.to_i if block_given? + end + end + read_vms[tpl_name] + end + + def halt(force=false) + args = ['stop', @uuid] + args << '--kill' if force + execute(*args) + end + + def import(template_uuid) + template_name = read_vms.key(template_uuid) + vm_name = "#{template_name}_#{(Time.now.to_f * 1000.0).to_i}_#{rand(100000)}" + + execute("clone", template_uuid, '--name', vm_name) do |type, data| + lines = data.split("\r") + # The progress of the import will be in the last line. Do a greedy + # regular expression to find what we're looking for. + if lines.last =~ /.+?(\d{,3}) ?%/ + yield $1.to_i if block_given? + end + end + read_vms[vm_name] + end + + def mac_in_use?(mac) + valid_mac = mac.upcase.tr('^A-F0-9', '') + read_vms_info.each do |vm| + return true if valid_mac == vm.fetch('Hardware', {}).fetch('net0',{}).fetch('mac', '') + end + false + end + + def read_bridged_interfaces + net_list = read_virtual_networks + + # Skip 'vnicXXX' and 'Default' interfaces + net_list.delete_if do |net| + net['Type'] != "bridged" or + net['Bound To'] =~ /^(vnic(.+?))$/ or + net['Network ID'] == "Default" + end + + bridged_ifaces = [] + net_list.collect do |iface| + info = {} + ifconfig = execute(:ifconfig, iface['Bound To']) + # Assign default values + info[:name] = iface['Network ID'].gsub(/\s\(.*?\)$/, '') + info[:bound_to] = iface['Bound To'] + info[:ip] = "0.0.0.0" + info[:netmask] = "0.0.0.0" + info[:status] = "Down" + + if ifconfig =~ /(?<=inet\s)(\S*)/ + info[:ip] = $1.to_s + end + if ifconfig =~ /(?<=netmask\s)(\S*)/ + # Netmask will be converted from hex to dec: + # '0xffffff00' -> '255.255.255.0' + info[:netmask] = $1.hex.to_s(16).scan(/../).each.map{|octet| octet.hex}.join(".") + end + if ifconfig =~ /\W(UP)\W/ and ifconfig !~ /(?<=status:\s)inactive$/ + info[:status] = "Up" + end + + bridged_ifaces << info + end + bridged_ifaces + end + + def read_guest_tools_version + read_settings.fetch('GuestTools', {}).fetch('version', nil) + end + + def read_host_only_interfaces + net_list = read_virtual_networks + net_list.keep_if { |net| net['Type'] == "host-only" } + + hostonly_ifaces = [] + net_list.collect do |iface| + info = {} + net_info = json { execute(:prlsrvctl, 'net', 'info', iface['Network ID'], '--json') } + # Really we need to work with bounded virtual interface + info[:name] = net_info['Network ID'] + info[:bound_to] = net_info['Bound To'] + info[:ip] = net_info['Parallels adapter']['IP address'] + info[:netmask] = net_info['Parallels adapter']['Subnet mask'] + # Such interfaces are always in 'Up' + info[:status] = "Up" + + # There may be a fake DHCPv4 parameters + # We can trust them only if adapter IP and DHCP IP are in the same subnet + dhcp_ip = net_info['DHCPv4 server']['Server address'] + if network_address(info[:ip], info[:netmask]) == network_address(dhcp_ip, info[:netmask]) + info[:dhcp] = { + :ip => dhcp_ip, + :lower => net_info['DHCPv4 server']['IP scope start address'], + :upper => net_info['DHCPv4 server']['IP scope end address'] + } + end + hostonly_ifaces << info + end + hostonly_ifaces + end + + def read_ip_dhcp + mac_addr = read_mac_address.downcase + File.foreach("/Library/Preferences/Parallels/parallels_dhcp_leases") do |line| + if line.include? mac_addr + ip = line[/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/] + return ip + end + end + end + + def read_mac_address + read_settings.fetch('Hardware', {}).fetch('net0', {}).fetch('mac', nil) + end + + def read_network_interfaces + nics = {} + + # Get enabled VM's network interfaces + ifaces = read_settings.fetch('Hardware', {}).keep_if do |dev, params| + dev.start_with?('net') and params.fetch("enabled", true) + end + ifaces.each do |name, params| + adapter = name.match(/^net(\d+)$/)[1].to_i + nics[adapter] ||= {} + + if params['type'] == "shared" + nics[adapter][:type] = :shared + elsif params['type'] == "host" + # It is PD internal host-only network and it is bounded to 'vnic1' + nics[adapter][:type] = :hostonly + nics[adapter][:hostonly] = "vnic1" + elsif params['type'] == "bridged" and params.fetch('iface','').start_with?('vnic') + # Bridged to the 'vnicXX'? Then it is a host-only, actually. + nics[adapter][:type] = :hostonly + nics[adapter][:hostonly] = params.fetch('iface','') + elsif params['type'] == "bridged" + nics[adapter][:type] = :bridged + nics[adapter][:bridge] = params.fetch('iface','') + end + end + nics + end + + def read_settings + vm = json { execute('list', @uuid, '--info', '--json', retryable: true).gsub(/^INFO/, '') } + vm.last + end + + def read_state + vm = json { execute('list', @uuid, '--json', retryable: true).gsub(/^INFO/, '') } + vm.last.fetch('status').to_sym + end + + def read_virtual_networks + json { execute(:prlsrvctl, 'net', 'list', '--json', retryable: true) } + end + + def read_vms + results = {} + vms_arr = json([]) do + execute('list', '--all', '--json', retryable: true).gsub(/^INFO/, '') + end + templates_arr = json([]) do + execute('list', '--all', '--json', '--template', retryable: true).gsub(/^INFO/, '') + end + vms = vms_arr | templates_arr + vms.each do |item| + results[item.fetch('name')] = item.fetch('uuid') + end + + results + end + + # Parse the JSON from *all* VMs and templates. Then return an array of objects (without duplicates) + def read_vms_info + vms_arr = json([]) do + execute('list', '--all','--info', '--json', retryable: true).gsub(/^INFO/, '') + end + templates_arr = json([]) do + execute('list', '--all','--info', '--json', '--template', retryable: true).gsub(/^INFO/, '') + end + vms_arr | templates_arr + end + + def read_vms_paths + list = {} + read_vms_info.each do |item| + if Dir.exists? item.fetch('Home') + list[File.realpath item.fetch('Home')] = item.fetch('ID') + end + end + + list + end + + def register(pvm_file) + execute("register", pvm_file) + end + + def registered?(uuid) + read_vms.has_value?(uuid) + end + + def resume + execute('resume', @uuid) + end + + def set_mac_address(mac) + execute('set', @uuid, '--device-set', 'net0', '--type', 'shared', '--mac', mac) + end + + def set_name(name) + execute('set', @uuid, '--name', name, :retryable => true) + end + + def share_folders(folders) + folders.each do |folder| + # Add the shared folder + execute('set', @uuid, '--shf-host-add', folder[:name], '--path', folder[:hostpath]) + end + end + + def ssh_port(expected_port) + expected_port + end + + def start + execute('start', @uuid) + end + + def suspend + execute('suspend', @uuid) + end + + def unregister(uuid) + execute("unregister", uuid) + end + + def verify! + version + end + + def version + if execute('--version', retryable: true) =~ /prlctl version ([\d\.]+)/ + $1.downcase + else + raise VagrantPlugins::Parallels::Errors::ParallelsInstallIncomplete + end + end + + def vm_exists?(uuid) + raw("list", uuid).exit_code == 0 + end + end + end + end +end diff --git a/lib/vagrant-parallels/driver/pd_9.rb b/lib/vagrant-parallels/driver/pd_9.rb index 506d2cd6..40c4d809 100644 --- a/lib/vagrant-parallels/driver/pd_9.rb +++ b/lib/vagrant-parallels/driver/pd_9.rb @@ -317,12 +317,12 @@ def read_network_interfaces end def read_settings - vm = json { execute('list', @uuid, '--info', '--json', retryable: true).gsub(/^(INFO)?/, '') } + vm = json { execute('list', @uuid, '--info', '--json', retryable: true) } vm.last end def read_state - vm = json { execute('list', @uuid, '--json', retryable: true).gsub(/^(INFO)?/, '') } + vm = json { execute('list', @uuid, '--json', retryable: true) } vm.last.fetch('status').to_sym end @@ -333,10 +333,10 @@ def read_virtual_networks def read_vms results = {} vms_arr = json([]) do - execute('list', '--all', '--json', retryable: true).gsub(/^(INFO)?/, '') + execute('list', '--all', '--json', retryable: true) end templates_arr = json([]) do - execute('list', '--all', '--json', '--template', retryable: true).gsub(/^(INFO)?/, '') + execute('list', '--all', '--json', '--template', retryable: true) end vms = vms_arr | templates_arr vms.each do |item| @@ -349,10 +349,10 @@ def read_vms # Parse the JSON from *all* VMs and templates. Then return an array of objects (without duplicates) def read_vms_info vms_arr = json([]) do - execute('list', '--all','--info', '--json', retryable: true).gsub(/^(INFO)?/, '') + execute('list', '--all','--info', '--json', retryable: true) end templates_arr = json([]) do - execute('list', '--all','--info', '--json', '--template', retryable: true).gsub(/^(INFO)?/, '') + execute('list', '--all','--info', '--json', '--template', retryable: true) end vms_arr | templates_arr end diff --git a/test/unit/driver/pd_8_test.rb b/test/unit/driver/pd_8_test.rb new file mode 100644 index 00000000..efdefa34 --- /dev/null +++ b/test/unit/driver/pd_8_test.rb @@ -0,0 +1,207 @@ +require_relative "../base" + +describe VagrantPlugins::Parallels::Driver::PD_8 do + include_context "parallels" + let(:parallels_version) { "8" } + + let(:vm_name) {'VM_Name'} + let(:vm_net0_mac) {'001C42B4B074'} + let(:vm_net1_mac) {'001C42EC0068'} + let(:vm_hdd) {'/path/to/disk1.hdd'} + + let(:tpl_uuid) {'1234-some-template-uuid-5678'} + let(:tpl_name) {'Some_Template_Name'} + let(:tpl_net0_mac) {'001C42F6E500'} + let(:tpl_net1_mac) {'001C42AB0071'} + + let(:hostonly_iface) {'vnic10'} + + let(:vnic_options) do { + :name => 'vagrant_vnic6', + :adapter_ip => '11.11.11.11', + :netmask => '255.255.252.0', + :dhcp => { + :ip => '11.11.11.11', + :lower => '11.11.8.1', + :upper => '11.11.11.254' + } + } end + + subject { VagrantPlugins::Parallels::Driver::Meta.new(uuid) } + + it_behaves_like "parallels desktop driver" + + before do + # Returns short info about all registered VMs + # `prlctl list --all --json` + subprocess.stub(:execute). + with("prlctl", "list", "--all", "--json", kind_of(Hash)) do + out = <<-eos +INFO +[ + { + "uuid": "#{uuid}", + "status": "stopped", + "name": "#{vm_name}" + } +] + eos + subprocess_result(stdout: out) + end + + # Returns short info about all registered templates + # `prlctl list --all --json --template` + subprocess.stub(:execute). + with("prlctl", "list", "--all", "--json", "--template", kind_of(Hash)) do + out = <<-eos +INFO +[ + { + "uuid": "1234-some-template-uuid-5678", + "name": "Some_Template_Name" + } +] + eos + subprocess_result(stdout: out) + end + + + # Returns detailed info about specified VM or all registered VMs + # `prlctl list --info --json` + # `prlctl list --all --info --json` + subprocess.stub(:execute). + with("prlctl", "list", kind_of(String), "--info", "--json", kind_of(Hash))do + out = <<-eos +INFO[ + { + "ID": "#{uuid}", + "Name": "#{vm_name}", + "State": "stopped", + "Home": "/path/to/#{vm_name}.pvm/", + "GuestTools": { + "version": "8.0.18615" + }, + "Hardware": { + "cpu": { + "cpus": 1 + }, + "memory": { + "size": "512Mb" + }, + "hdd0": { + "enabled": true, + "image": "#{vm_hdd}" + }, + "net0": { + "enabled": true, + "type": "shared", + "mac": "#{vm_net0_mac}", + "card": "e1000", + "dhcp": "yes" + }, + "net1": { + "enabled": true, + "type": "bridged", + "iface": "vnic2", + "mac": "#{vm_net1_mac}", + "card": "e1000", + "ips": "33.33.33.5/255.255.255.0 " + }, + "net2": { + "enabled": false, + "type": "bridged", + "iface": "vnic5", + "mac": "001C42FFFFFF", + "card": "virtio" + } + }, + "Host Shared Folders": { + "enabled": true, + "shared_folder_1": { + "enabled": true, + "path": "/path/to/shared/folder/1" + }, + "shared_folder_2": { + "enabled": true, + "path": "/path/to/shared/folder/2" + } + } + } +] + eos + subprocess_result(stdout: out) + end + + # Returns detailed info about specified template or all registered templates + # `prlctl list --info --json --template` + # `prlctl list --all --info --json --template` + subprocess.stub(:execute). + with("prlctl", "list", kind_of(String), "--info", "--json", "--template", kind_of(Hash))do + out = <<-eos +INFO[ + { + "ID": "#{tpl_uuid}", + "Name": "#{tpl_name}", + "State": "stopped", + "Home": "/path/to/#{tpl_name}.pvm/", + "GuestTools": { + "version": "8.0.18615" + }, + "Hardware": { + "cpu": { + "cpus": 1 + }, + "memory": { + "size": "512Mb" + }, + "hdd0": { + "enabled": true, + "image": "/path/to/harddisk.hdd" + }, + "net0": { + "enabled": true, + "type": "shared", + "mac": "#{tpl_net0_mac}", + "card": "e1000", + "dhcp": "yes" + }, + "net1": { + "enabled": true, + "type": "bridged", + "iface": "vnic4", + "mac": "#{tpl_net1_mac}", + "card": "e1000", + "ips": "33.33.33.10/255.255.255.0 " + } + } + } +] + eos + subprocess_result(stdout: out) + end + + # Returns detailed info about virtual network interface + # `prlsrvctl net info , '--json', retryable: true) + subprocess.stub(:execute). + with("prlsrvctl", "net", "info", kind_of(String), "--json", kind_of(Hash))do + out = <<-eos + { + "Network ID": "#{vnic_options[:name]}", + "Type": "host-only", + "Bound To": "#{hostonly_iface}", + "Parallels adapter": { + "IP address": "#{vnic_options[:adapter_ip]}", + "Subnet mask": "#{vnic_options[:netmask]}" + }, + "DHCPv4 server": { + "Server address": "#{vnic_options[:dhcp][:ip] || "10.37.132.1"}", + "IP scope start address": "#{vnic_options[:dhcp][:lower] || "10.37.132.1"}", + "IP scope end address": "#{vnic_options[:dhcp][:upper] || "10.37.132.254"}" + } + } + eos + subprocess_result(stdout: out) + end + + end +end From 18e123dbcf1c5c7296cc0611611458695d91d471 Mon Sep 17 00:00:00 2001 From: Mikhail Zholobov Date: Thu, 20 Feb 2014 00:16:01 +0400 Subject: [PATCH 20/33] 'action/network' dhcp server's ip equals host's ip It is really so in PD: dhcp server is ruuning on the host addres. There is no reason to run dhcp server on dedicated IP. --- lib/vagrant-parallels/action/network.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/vagrant-parallels/action/network.rb b/lib/vagrant-parallels/action/network.rb index b5555484..78b4df8e 100644 --- a/lib/vagrant-parallels/action/network.rb +++ b/lib/vagrant-parallels/action/network.rb @@ -229,14 +229,14 @@ def hostonly_config(options) dhcp_options = {} if options[:type] == :dhcp # Calculate the DHCP server IP, which is the network address - # with the final octet + 2. So "172.28.0.0" turns into "172.28.0.2" + # with the final octet + 1. So "172.28.0.0" turns into "172.28.0.1" dhcp_ip = ip_parts.dup - dhcp_ip[3] += 2 + dhcp_ip[3] += 1 dhcp_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_lower[3] += 2 dhcp_options[:dhcp_lower] ||= dhcp_lower.join(".") dhcp_upper = ip_parts.dup From 9635d48c45539d6411d229c7b68b4204cde72332 Mon Sep 17 00:00:00 2001 From: Mikhail Zholobov Date: Fri, 21 Feb 2014 14:39:24 +0400 Subject: [PATCH 21/33] unit-tests refactoring: added stubs with 'driver' object --- test/unit/driver/pd_8_test.rb | 65 ++--- test/unit/driver/pd_9_test.rb | 249 +++++++++--------- .../unit/support/shared/pd_driver_examples.rb | 122 +++++---- 3 files changed, 218 insertions(+), 218 deletions(-) diff --git a/test/unit/driver/pd_8_test.rb b/test/unit/driver/pd_8_test.rb index efdefa34..3ee45f07 100644 --- a/test/unit/driver/pd_8_test.rb +++ b/test/unit/driver/pd_8_test.rb @@ -5,14 +5,9 @@ let(:parallels_version) { "8" } let(:vm_name) {'VM_Name'} - let(:vm_net0_mac) {'001C42B4B074'} - let(:vm_net1_mac) {'001C42EC0068'} - let(:vm_hdd) {'/path/to/disk1.hdd'} let(:tpl_uuid) {'1234-some-template-uuid-5678'} let(:tpl_name) {'Some_Template_Name'} - let(:tpl_net0_mac) {'001C42F6E500'} - let(:tpl_net1_mac) {'001C42AB0071'} let(:hostonly_iface) {'vnic10'} @@ -37,8 +32,7 @@ subprocess.stub(:execute). with("prlctl", "list", "--all", "--json", kind_of(Hash)) do out = <<-eos -INFO -[ +INFO[ { "uuid": "#{uuid}", "status": "stopped", @@ -54,8 +48,7 @@ subprocess.stub(:execute). with("prlctl", "list", "--all", "--json", "--template", kind_of(Hash)) do out = <<-eos -INFO -[ +INFO[ { "uuid": "1234-some-template-uuid-5678", "name": "Some_Template_Name" @@ -70,7 +63,8 @@ # `prlctl list --info --json` # `prlctl list --all --info --json` subprocess.stub(:execute). - with("prlctl", "list", kind_of(String), "--info", "--json", kind_of(Hash))do + with("prlctl", "list", kind_of(String), "--info", "--json", + kind_of(Hash)) do out = <<-eos INFO[ { @@ -90,12 +84,12 @@ }, "hdd0": { "enabled": true, - "image": "#{vm_hdd}" + "image": "/path/to/disk1.hdd" }, "net0": { "enabled": true, "type": "shared", - "mac": "#{vm_net0_mac}", + "mac": "001C42B4B074", "card": "e1000", "dhcp": "yes" }, @@ -103,16 +97,9 @@ "enabled": true, "type": "bridged", "iface": "vnic2", - "mac": "#{vm_net1_mac}", + "mac": "001C42EC0068", "card": "e1000", "ips": "33.33.33.5/255.255.255.0 " - }, - "net2": { - "enabled": false, - "type": "bridged", - "iface": "vnic5", - "mac": "001C42FFFFFF", - "card": "virtio" } }, "Host Shared Folders": { @@ -136,7 +123,8 @@ # `prlctl list --info --json --template` # `prlctl list --all --info --json --template` subprocess.stub(:execute). - with("prlctl", "list", kind_of(String), "--info", "--json", "--template", kind_of(Hash))do + with("prlctl", "list", kind_of(String), "--info", "--json", "--template", + kind_of(Hash)) do out = <<-eos INFO[ { @@ -161,7 +149,7 @@ "net0": { "enabled": true, "type": "shared", - "mac": "#{tpl_net0_mac}", + "mac": "001C42F6E500", "card": "e1000", "dhcp": "yes" }, @@ -169,7 +157,7 @@ "enabled": true, "type": "bridged", "iface": "vnic4", - "mac": "#{tpl_net1_mac}", + "mac": "001C42AB0071", "card": "e1000", "ips": "33.33.33.10/255.255.255.0 " } @@ -183,22 +171,23 @@ # Returns detailed info about virtual network interface # `prlsrvctl net info , '--json', retryable: true) subprocess.stub(:execute). - with("prlsrvctl", "net", "info", kind_of(String), "--json", kind_of(Hash))do + with("prlsrvctl", "net", "info", kind_of(String), "--json", + kind_of(Hash)) do out = <<-eos - { - "Network ID": "#{vnic_options[:name]}", - "Type": "host-only", - "Bound To": "#{hostonly_iface}", - "Parallels adapter": { - "IP address": "#{vnic_options[:adapter_ip]}", - "Subnet mask": "#{vnic_options[:netmask]}" - }, - "DHCPv4 server": { - "Server address": "#{vnic_options[:dhcp][:ip] || "10.37.132.1"}", - "IP scope start address": "#{vnic_options[:dhcp][:lower] || "10.37.132.1"}", - "IP scope end address": "#{vnic_options[:dhcp][:upper] || "10.37.132.254"}" - } - } +{ + "Network ID": "#{vnic_options[:name]}", + "Type": "host-only", + "Bound To": "#{hostonly_iface}", + "Parallels adapter": { + "IP address": "#{vnic_options[:adapter_ip]}", + "Subnet mask": "#{vnic_options[:netmask]}" + }, + "DHCPv4 server": { + "Server address": "#{vnic_options[:dhcp][:ip] || "10.37.132.1"}", + "IP scope start address": "#{vnic_options[:dhcp][:lower] || "10.37.132.1"}", + "IP scope end address": "#{vnic_options[:dhcp][:upper] || "10.37.132.254"}" + } +} eos subprocess_result(stdout: out) end diff --git a/test/unit/driver/pd_9_test.rb b/test/unit/driver/pd_9_test.rb index f08c91c9..b3b81770 100644 --- a/test/unit/driver/pd_9_test.rb +++ b/test/unit/driver/pd_9_test.rb @@ -5,14 +5,9 @@ let(:parallels_version) { "9" } let(:vm_name) {'VM_Name'} - let(:vm_net0_mac) {'001C42B4B074'} - let(:vm_net1_mac) {'001C42EC0068'} - let(:vm_hdd) {'/path/to/disk1.hdd'} let(:tpl_uuid) {'1234-some-template-uuid-5678'} let(:tpl_name) {'Some_Template_Name'} - let(:tpl_net0_mac) {'001C42F6E500'} - let(:tpl_net1_mac) {'001C42AB0071'} let(:hostonly_iface) {'vnic10'} @@ -37,13 +32,13 @@ subprocess.stub(:execute). with("prlctl", "list", "--all", "--json", kind_of(Hash)) do out = <<-eos - [ - { - "uuid": "#{uuid}", - "status": "stopped", - "name": "#{vm_name}" - } - ] +[ + { + "uuid": "#{uuid}", + "status": "stopped", + "name": "#{vm_name}" + } +] eos subprocess_result(stdout: out) end @@ -53,12 +48,12 @@ subprocess.stub(:execute). with("prlctl", "list", "--all", "--json", "--template", kind_of(Hash)) do out = <<-eos - [ - { - "uuid": "1234-some-template-uuid-5678", - "name": "Some_Template_Name" - } - ] +[ + { + "uuid": "1234-some-template-uuid-5678", + "name": "Some_Template_Name" + } +] eos subprocess_result(stdout: out) end @@ -68,64 +63,58 @@ # `prlctl list --info --json` # `prlctl list --all --info --json` subprocess.stub(:execute). - with("prlctl", "list", kind_of(String), "--info", "--json", kind_of(Hash))do + with("prlctl", "list", kind_of(String), "--info", "--json", + kind_of(Hash)) do out = <<-eos - [ - { - "ID": "#{uuid}", - "Name": "#{vm_name}", - "State": "stopped", - "Home": "/path/to/#{vm_name}.pvm/", - "GuestTools": { - "version": "9.0.23062" - }, - "Hardware": { - "cpu": { - "cpus": 1 - }, - "memory": { - "size": "512Mb" - }, - "hdd0": { - "enabled": true, - "image": "#{vm_hdd}" - }, - "net0": { - "enabled": true, - "type": "shared", - "mac": "#{vm_net0_mac}", - "card": "e1000", - "dhcp": "yes" - }, - "net1": { - "enabled": true, - "type": "bridged", - "iface": "vnic2", - "mac": "#{vm_net1_mac}", - "card": "e1000", - "ips": "33.33.33.5/255.255.255.0 " - }, - "net2": { - "enabled": false, - "type": "bridged", - "iface": "vnic5", - "mac": "001C42FFFFFF", - "card": "virtio" - } - }, - "Host Shared Folders": { - "enabled": true, - "shared_folder_1": { - "enabled": true, - "path": "/path/to/shared/folder/1" - }, - "shared_folder_2": { - "enabled": true, - "path": "/path/to/shared/folder/2" - } - } - } - ] +[ + { + "ID": "#{uuid}", + "Name": "#{vm_name}", + "State": "stopped", + "Home": "/path/to/#{vm_name}.pvm/", + "GuestTools": { + "version": "9.0.23062" + }, + "Hardware": { + "cpu": { + "cpus": 1 + }, + "memory": { + "size": "512Mb" + }, + "hdd0": { + "enabled": true, + "image": "/path/to/disk1.hdd" + }, + "net0": { + "enabled": true, + "type": "shared", + "mac": "001C42B4B074", + "card": "e1000", + "dhcp": "yes" + }, + "net1": { + "enabled": true, + "type": "bridged", + "iface": "vnic2", + "mac": "001C42EC0068", + "card": "e1000", + "ips": "33.33.33.5/255.255.255.0 " + } + }, + "Host Shared Folders": { + "enabled": true, + "shared_folder_1": { + "enabled": true, + "path": "/path/to/shared/folder/1" + }, + "shared_folder_2": { + "enabled": true, + "path": "/path/to/shared/folder/2" + } + } + } +] eos subprocess_result(stdout: out) end @@ -134,46 +123,47 @@ # `prlctl list --info --json --template` # `prlctl list --all --info --json --template` subprocess.stub(:execute). - with("prlctl", "list", kind_of(String), "--info", "--json", "--template", kind_of(Hash))do + with("prlctl", "list", kind_of(String), "--info", "--json", "--template", + kind_of(Hash)) do out = <<-eos - [ - { - "ID": "#{tpl_uuid}", - "Name": "#{tpl_name}", - "State": "stopped", - "Home": "/path/to/#{tpl_name}.pvm/", - "GuestTools": { - "version": "9.0.24172" - }, - "Hardware": { - "cpu": { - "cpus": 1 - }, - "memory": { - "size": "512Mb" - }, - "hdd0": { - "enabled": true, - "image": "/path/to/harddisk.hdd" - }, - "net0": { - "enabled": true, - "type": "shared", - "mac": "#{tpl_net0_mac}", - "card": "e1000", - "dhcp": "yes" - }, - "net1": { - "enabled": true, - "type": "bridged", - "iface": "vnic4", - "mac": "#{tpl_net1_mac}", - "card": "e1000", - "ips": "33.33.33.10/255.255.255.0 " - } - } - } - ] +[ + { + "ID": "#{tpl_uuid}", + "Name": "#{tpl_name}", + "State": "stopped", + "Home": "/path/to/#{tpl_name}.pvm/", + "GuestTools": { + "version": "9.0.24172" + }, + "Hardware": { + "cpu": { + "cpus": 1 + }, + "memory": { + "size": "512Mb" + }, + "hdd0": { + "enabled": true, + "image": "/path/to/harddisk.hdd" + }, + "net0": { + "enabled": true, + "type": "shared", + "mac": "001C42F6E500", + "card": "e1000", + "dhcp": "yes" + }, + "net1": { + "enabled": true, + "type": "bridged", + "iface": "vnic4", + "mac": "001C42AB0071", + "card": "e1000", + "ips": "33.33.33.10/255.255.255.0 " + } + } + } +] eos subprocess_result(stdout: out) end @@ -181,22 +171,23 @@ # Returns detailed info about virtual network interface # `prlsrvctl net info , '--json', retryable: true) subprocess.stub(:execute). - with("prlsrvctl", "net", "info", kind_of(String), "--json", kind_of(Hash))do + with("prlsrvctl", "net", "info", kind_of(String), "--json", + kind_of(Hash)) do out = <<-eos - { - "Network ID": "#{vnic_options[:name]}", - "Type": "host-only", - "Bound To": "#{hostonly_iface}", - "Parallels adapter": { - "IP address": "#{vnic_options[:adapter_ip]}", - "Subnet mask": "#{vnic_options[:netmask]}" - }, - "DHCPv4 server": { - "Server address": "#{vnic_options[:dhcp][:ip] || "10.37.132.1"}", - "IP scope start address": "#{vnic_options[:dhcp][:lower] || "10.37.132.1"}", - "IP scope end address": "#{vnic_options[:dhcp][:upper] || "10.37.132.254"}" - } - } +{ + "Network ID": "#{vnic_options[:name]}", + "Type": "host-only", + "Bound To": "#{hostonly_iface}", + "Parallels adapter": { + "IP address": "#{vnic_options[:adapter_ip]}", + "Subnet mask": "#{vnic_options[:netmask]}" + }, + "DHCPv4 server": { + "Server address": "#{vnic_options[:dhcp][:ip] || "10.37.132.1"}", + "IP scope start address": "#{vnic_options[:dhcp][:lower] || "10.37.132.1"}", + "IP scope end address": "#{vnic_options[:dhcp][:upper] || "10.37.132.254"}" + } +} eos subprocess_result(stdout: out) end diff --git a/test/unit/support/shared/pd_driver_examples.rb b/test/unit/support/shared/pd_driver_examples.rb index 6a8899de..52f07069 100644 --- a/test/unit/support/shared/pd_driver_examples.rb +++ b/test/unit/support/shared/pd_driver_examples.rb @@ -3,10 +3,18 @@ raise ArgumentError, "Need parallels context to use these shared examples." unless defined? parallels_context end + # Accessor to the delegate object + let(:driver) { subject.instance_variable_get("@driver") } + describe "compact" do + settings = {"Hardware" => {"hdd0" => {"image" => "/path/to/disk0.hdd"}, + "hdd1" => {"image" => "/path/to/disk1.hdd"}}} it "compacts the VM disk drives" do - subprocess.should_receive(:execute). - with("prl_disk_tool", 'compact', '--hdd', vm_hdd, an_instance_of(Hash)). + driver.should_receive(:read_settings).and_return(settings) + + subprocess.should_receive(:execute).exactly(2).times. + with("prl_disk_tool", 'compact', '--hdd', /^\/path\/to\/disk(0|1).hdd$/, + an_instance_of(Hash)). and_return(subprocess_result(exit_code: 0)) subject.compact(uuid) end @@ -15,8 +23,9 @@ describe "clear_shared_folders" do it "deletes every shared folder assigned to the VM" do subprocess.should_receive(:execute).at_least(2).times. - with("prlctl", "set", uuid, "--shf-host-del", an_instance_of(String), an_instance_of(Hash)). - and_return(subprocess_result(stdout: "Shared folder deleted")) + with("prlctl", "set", uuid, "--shf-host-del", an_instance_of(String), + an_instance_of(Hash)). + and_return(subprocess_result(exit_code: 0)) subject.clear_shared_folders end end @@ -24,7 +33,7 @@ describe "create_host_only_network" do let(:hostonly_iface) {'vnic12'} it "creates host-only NIC with dhcp server configured" do - vnic_options = { + vnic_opts = { :name => 'vagrant_vnic8', :adapter_ip => '11.11.11.11', :netmask => '255.255.252.0', @@ -36,23 +45,24 @@ } subprocess.should_receive(:execute). - with("prlsrvctl", "net", "add", vnic_options[:name], "--type", "host-only", an_instance_of(Hash)). + with("prlsrvctl", "net", "add", vnic_opts[:name], + "--type", "host-only", an_instance_of(Hash)). and_return(subprocess_result(exit_code: 0)) subprocess.should_receive(:execute). - with("prlsrvctl", "net", "set", vnic_options[:name], - "--ip", "#{vnic_options[:adapter_ip]}/#{vnic_options[:netmask]}", - "--dhcp-ip", vnic_options[:dhcp][:ip], - "--ip-scope-start", vnic_options[:dhcp][:lower], - "--ip-scope-end", vnic_options[:dhcp][:upper], an_instance_of(Hash)). + with("prlsrvctl", "net", "set", vnic_opts[:name], + "--ip", "#{vnic_opts[:adapter_ip]}/#{vnic_opts[:netmask]}", + "--dhcp-ip", vnic_opts[:dhcp][:ip], + "--ip-scope-start", vnic_opts[:dhcp][:lower], + "--ip-scope-end", vnic_opts[:dhcp][:upper], an_instance_of(Hash)). and_return(subprocess_result(exit_code: 0)) - interface = subject.create_host_only_network(vnic_options) + interface = subject.create_host_only_network(vnic_opts) - interface.should include(:name => vnic_options[:name]) - interface.should include(:ip => vnic_options[:adapter_ip]) - interface.should include(:netmask => vnic_options[:netmask]) - interface.should include(:dhcp => vnic_options[:dhcp]) + interface.should include(:name => vnic_opts[:name]) + interface.should include(:ip => vnic_opts[:adapter_ip]) + interface.should include(:netmask => vnic_opts[:netmask]) + interface.should include(:dhcp => vnic_opts[:dhcp]) interface.should include(:bound_to => hostonly_iface) interface[:bound_to].should =~ /^(vnic(\d+))$/ end @@ -65,12 +75,14 @@ } subprocess.should_receive(:execute). - with("prlsrvctl", "net", "add", vnic_options[:name], "--type", "host-only", an_instance_of(Hash)). + with("prlsrvctl", "net", "add", vnic_options[:name], + "--type", "host-only", an_instance_of(Hash)). and_return(subprocess_result(exit_code: 0)) subprocess.should_receive(:execute). with("prlsrvctl", "net", "set", vnic_options[:name], - "--ip", "#{vnic_options[:adapter_ip]}/#{vnic_options[:netmask]}", an_instance_of(Hash)). + "--ip", "#{vnic_options[:adapter_ip]}/#{vnic_options[:netmask]}", + an_instance_of(Hash)). and_return(subprocess_result(exit_code: 0)) interface = subject.create_host_only_network(vnic_options) @@ -95,8 +107,12 @@ describe "delete_disabled_adapters" do it "deletes disabled networks adapters from VM config" do - subprocess.should_receive(:execute). - with("prlctl", "set", uuid, "--device-del", /^(net(\d+))$/, an_instance_of(Hash)). + settings = {"Hardware" => {"net0" => {"enabled" => false}, + "net1" => {"enabled" => false}}} + driver.should_receive(:read_settings).and_return(settings) + subprocess.should_receive(:execute).exactly(2).times. + with("prlctl", "set", uuid, "--device-del", /^net(0|1)$/, + an_instance_of(Hash)). and_return(subprocess_result(exit_code: 0)) subject.delete_disabled_adapters end @@ -108,8 +124,9 @@ it "exports VM to template" do subprocess.should_receive(:execute). - with("prlctl", "clone", uuid, "--name", an_instance_of(String), "--template", "--dst", - an_instance_of(String), an_instance_of(Hash)). + with("prlctl", "clone", uuid, "--name", an_instance_of(String), + "--template", "--dst", an_instance_of(String), + an_instance_of(Hash)). and_return(subprocess_result(exit_code: 0)) subject.export("/path/to/template", tpl_name).should == tpl_uuid end @@ -132,18 +149,19 @@ end describe "mac_in_use?" do - let(:vm_net0_mac) {'00AABBCC01'} - let(:vm_net1_mac) {'00AABBCC02'} - let(:tpl_net0_mac) {'00AABBCC03'} - let(:tpl_net1_mac) {'00AABBCC04'} + before do + vms_info = [{"Hardware" => {"net0" => {"mac" => "00AABBCC01"}, + "net1" => {"mac" => "00AABBCC02"}}}, + {"Hardware" => {"net0" => {"mac" => "00AABBCC03"}, + "net1" => {"mac" => "00AABBCC04"}}}] + driver.should_receive(:read_vms_info).and_return(vms_info) + end - it "checks the MAC address is already in use" do + it { subject.mac_in_use?('00:AA:BB:CC:01').should be_true } + it { subject.mac_in_use?('00:AA:BB:CC:02').should be_false } + it { subject.mac_in_use?('00:AA:BB:CC:03').should be_true } + it { subject.mac_in_use?('00:AA:BB:CC:04').should be_false } - subject.mac_in_use?('00:AA:BB:CC:01').should be_true - subject.mac_in_use?('00:AA:BB:CC:02').should be_false - subject.mac_in_use?('00:AA:BB:CC:03').should be_true - subject.mac_in_use?('00:AA:BB:CC:04').should be_false - end end describe "read_settings" do @@ -169,38 +187,39 @@ subject.read_vms_info.should have_at_least(2).items # It should include info about current VM - vm_settings = subject.send(:read_settings) + vm_settings = driver.send(:read_settings) subject.read_vms_info.should include(vm_settings) end end - describe "set_name" do - it "sets new name for the VM" do - subprocess.should_receive(:execute). - with("prlctl", "set", uuid, '--name', an_instance_of(String), an_instance_of(Hash)). - and_return(subprocess_result(stdout: "Settings applied")) - - subject.set_name('new_vm_name') - end - end - describe "set_mac_address" do it "sets base MAC address to the Shared network adapter" do subprocess.should_receive(:execute).exactly(2).times. - with("prlctl", "set", uuid, '--device-set', 'net0', '--type', 'shared', '--mac', - an_instance_of(String), an_instance_of(Hash)). - and_return(subprocess_result(stdout: "Settings applied")) + with("prlctl", "set", uuid, '--device-set', 'net0', '--type', 'shared', + '--mac', an_instance_of(String), an_instance_of(Hash)). + and_return(subprocess_result(exit_code: 0)) subject.set_mac_address('001C42DD5902') subject.set_mac_address('auto') end end + describe "set_name" do + it "sets new name for the VM" do + subprocess.should_receive(:execute). + with("prlctl", "set", uuid, '--name', an_instance_of(String), + an_instance_of(Hash)). + and_return(subprocess_result(exit_code: 0)) + + subject.set_name('new_vm_name') + end + end + describe "start" do it "starts the VM" do subprocess.should_receive(:execute). with("prlctl", "start", uuid, an_instance_of(Hash)). - and_return(subprocess_result(stdout: "VM started")) + and_return(subprocess_result(exit_code: 0)) subject.start end end @@ -209,7 +228,7 @@ it "suspends the VM" do subprocess.should_receive(:execute). with("prlctl", "suspend", uuid, an_instance_of(Hash)). - and_return(subprocess_result(stdout: "VM suspended")) + and_return(subprocess_result(exit_code: 0)) subject.suspend end end @@ -217,8 +236,9 @@ describe "unregister" do it "suspends the VM" do subprocess.should_receive(:execute). - with("prlctl", "unregister", an_instance_of(String), an_instance_of(Hash)). - and_return(subprocess_result(stdout: "Specified VM unregistered")) + with("prlctl", "unregister", an_instance_of(String), + an_instance_of(Hash)). + and_return(subprocess_result(exit_code: 0)) subject.unregister("template_or_vm_uuid") end end @@ -231,7 +251,7 @@ it "rises ParallelsInstallIncomplete exception when output is invalid" do subprocess.should_receive(:execute). with("prlctl", "--version", an_instance_of(Hash)). - and_return(subprocess_result(stdout: "Some incorrect value has been returned!")) + and_return(subprocess_result(exit_code: 0)) expect { subject.version }. to raise_error(VagrantPlugins::Parallels::Errors::ParallelsInvalidVersion) end From ce40aa95698503983fd6076fc0fffd0997f3c9a0 Mon Sep 17 00:00:00 2001 From: Mikhail Zholobov Date: Sun, 23 Feb 2014 13:19:33 +0400 Subject: [PATCH 22/33] 'action/network': fixed "unnecessary dhcp" error in private networking Error description: if interface already exists and it's been configured with DHCP before, these dhcp-settings applied to VM, ingnoring the 'type: static' option. Fixed: added checking of the type. --- lib/vagrant-parallels/action/network.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/vagrant-parallels/action/network.rb b/lib/vagrant-parallels/action/network.rb index ab3a8bbe..90bcd601 100644 --- a/lib/vagrant-parallels/action/network.rb +++ b/lib/vagrant-parallels/action/network.rb @@ -282,7 +282,7 @@ def hostonly_adapter(config) :mac => config[:mac], :nic_type => config[:nic_type], :type => :hostonly, - :dhcp => interface[:dhcp], + :dhcp => config[:type] == :dhcp ? interface[:dhcp] : false, :ip => config[:ip], :netmask => config[:netmask], } From 97c362fa0a109f7ef71e509d07d8c9c51855af1e Mon Sep 17 00:00:00 2001 From: Mikhail Zholobov Date: Wed, 26 Feb 2014 00:36:50 +0400 Subject: [PATCH 23/33] Updated i18n-tasks gem to 0.2.21 --- config/i18n-tasks.yml.erb | 2 +- test/unit/locales/locales_test.rb | 2 +- vagrant-parallels.gemspec | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config/i18n-tasks.yml.erb b/config/i18n-tasks.yml.erb index 822653ae..b9933744 100644 --- a/config/i18n-tasks.yml.erb +++ b/config/i18n-tasks.yml.erb @@ -2,7 +2,7 @@ search: paths: - "lib/" data: - adapter: yaml + adapter: file_system read: - "locales/%{locale}.yml" - "<%= %x[bundle show vagrant].chomp %>/templates/locales/%{locale}.yml" diff --git a/test/unit/locales/locales_test.rb b/test/unit/locales/locales_test.rb index 43830cd6..af603287 100644 --- a/test/unit/locales/locales_test.rb +++ b/test/unit/locales/locales_test.rb @@ -9,6 +9,6 @@ end it 'are all present' do - i18n.untranslated_keys.should have(0).keys + i18n.missing_keys.should have(0).keys end end \ No newline at end of file diff --git a/vagrant-parallels.gemspec b/vagrant-parallels.gemspec index 99650b23..6cb4cf52 100644 --- a/vagrant-parallels.gemspec +++ b/vagrant-parallels.gemspec @@ -19,7 +19,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency "bundler", "~> 1.5.2" spec.add_development_dependency "rake" spec.add_development_dependency "rspec", "~> 2.14.0" - spec.add_development_dependency "i18n-tasks", "~> 0.2.14" + spec.add_development_dependency "i18n-tasks", "~> 0.2.21" # 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 From e894ace80cb092fa94da8aa7a06e89aa31793953 Mon Sep 17 00:00:00 2001 From: Mikhail Zholobov Date: Tue, 25 Feb 2014 23:10:55 +0400 Subject: [PATCH 24/33] driver/base: don't log in trap context porting the commit mitchellh/vagrant@3324756 --- lib/vagrant-parallels/driver/base.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/vagrant-parallels/driver/base.rb b/lib/vagrant-parallels/driver/base.rb index 1637e410..120f0887 100644 --- a/lib/vagrant-parallels/driver/base.rb +++ b/lib/vagrant-parallels/driver/base.rb @@ -257,7 +257,10 @@ def execute(*command, &block) def raw(*command, &block) int_callback = lambda do @interrupted = true - @logger.info("Interrupted.") + + # We have to execute this in a thread due to trap contexts + # and locks. + Thread.new { @logger.info("Interrupted.") } end # Append in the options for subprocess From 908a75b955dd365cf7d3b98b6b589ed9ac51bb81 Mon Sep 17 00:00:00 2001 From: Mikhail Zholobov Date: Wed, 26 Feb 2014 02:19:03 +0400 Subject: [PATCH 25/33] action/match_mac_address deleted [GH-79] We also don't need to pack Vagrantfile with 'base_mac' anymore --- lib/vagrant-parallels/action.rb | 2 -- .../action/match_mac_address.rb | 28 ------------------- .../action/package_config_files.rb | 12 -------- lib/vagrant-parallels/driver/meta.rb | 1 - lib/vagrant-parallels/driver/pd_8.rb | 8 ------ lib/vagrant-parallels/driver/pd_9.rb | 8 ------ locales/en.yml | 5 ---- .../unit/support/shared/pd_driver_examples.rb | 15 ---------- 8 files changed, 79 deletions(-) delete mode 100644 lib/vagrant-parallels/action/match_mac_address.rb diff --git a/lib/vagrant-parallels/action.rb b/lib/vagrant-parallels/action.rb index 027c2bfb..d4019117 100644 --- a/lib/vagrant-parallels/action.rb +++ b/lib/vagrant-parallels/action.rb @@ -263,7 +263,6 @@ def self.action_up b2.use CheckAccessible b2.use Customize, "pre-import" b2.use Import - b2.use MatchMACAddress end end b.use action_start @@ -286,7 +285,6 @@ def self.action_up autoload :Import, File.expand_path("../action/import", __FILE__) autoload :IsSuspended, File.expand_path("../action/is_suspended", __FILE__) autoload :IsRunning, File.expand_path("../action/is_running", __FILE__) - autoload :MatchMACAddress, File.expand_path("../action/match_mac_address", __FILE__) autoload :MessageAlreadyRunning, File.expand_path("../action/message_already_running", __FILE__) autoload :MessageNotCreated, File.expand_path("../action/message_not_created", __FILE__) autoload :MessageNotRunning, File.expand_path("../action/message_not_running", __FILE__) diff --git a/lib/vagrant-parallels/action/match_mac_address.rb b/lib/vagrant-parallels/action/match_mac_address.rb deleted file mode 100644 index d2f021ea..00000000 --- a/lib/vagrant-parallels/action/match_mac_address.rb +++ /dev/null @@ -1,28 +0,0 @@ -module VagrantPlugins - module Parallels - module Action - class MatchMACAddress - def initialize(app, env) - @app = app - end - - def call(env) - raise Vagrant::Errors::VMBaseMacNotSpecified if !env[:machine].config.vm.base_mac - - env[:ui].info I18n.t("vagrant_parallels.actions.vm.match_mac.matching") - - base_mac = env[:machine].config.vm.base_mac - # Generate new base mac if the specified address is already in use - if env[:machine].provider.driver.mac_in_use?(base_mac) - env[:ui].info I18n.t("vagrant_parallels.actions.vm.match_mac.generate") - env[:machine].provider.driver.set_mac_address('auto') - else - env[:machine].provider.driver.set_mac_address(base_mac) - end - - @app.call(env) - end - end - end - end -end diff --git a/lib/vagrant-parallels/action/package_config_files.rb b/lib/vagrant-parallels/action/package_config_files.rb index bb628927..4cd5f313 100644 --- a/lib/vagrant-parallels/action/package_config_files.rb +++ b/lib/vagrant-parallels/action/package_config_files.rb @@ -14,21 +14,9 @@ def initialize(app, env) def call(env) @env = env create_metadata - create_vagrantfile @app.call(env) end - # This method creates the auto-generated Vagrantfile at the root of the - # box. This Vagrantfile contains the MAC address so that the user doesn't - # have to worry about it. - def create_vagrantfile - File.open(File.join(@env["export.temp_dir"], "Vagrantfile"), "w") do |f| - f.write(TemplateRenderer.render("package_Vagrantfile", { - :base_mac => @env[:machine].provider.driver.read_mac_address - })) - end - end - def create_metadata File.open(File.join(@env["export.temp_dir"], "metadata.json"), "w") do |f| f.write(template_metadatafile) diff --git a/lib/vagrant-parallels/driver/meta.rb b/lib/vagrant-parallels/driver/meta.rb index 54e29807..1d2b3e2b 100644 --- a/lib/vagrant-parallels/driver/meta.rb +++ b/lib/vagrant-parallels/driver/meta.rb @@ -86,7 +86,6 @@ def initialize(uuid=nil) #:forward_ports, :halt, :import, - :mac_in_use?, :read_ip_dhcp, #:read_forwarded_ports, :read_bridged_interfaces, diff --git a/lib/vagrant-parallels/driver/pd_8.rb b/lib/vagrant-parallels/driver/pd_8.rb index a240e68c..c6cd4fd3 100644 --- a/lib/vagrant-parallels/driver/pd_8.rb +++ b/lib/vagrant-parallels/driver/pd_8.rb @@ -192,14 +192,6 @@ def import(template_uuid) read_vms[vm_name] end - def mac_in_use?(mac) - valid_mac = mac.upcase.tr('^A-F0-9', '') - read_vms_info.each do |vm| - return true if valid_mac == vm.fetch('Hardware', {}).fetch('net0',{}).fetch('mac', '') - end - false - end - def read_bridged_interfaces net_list = read_virtual_networks diff --git a/lib/vagrant-parallels/driver/pd_9.rb b/lib/vagrant-parallels/driver/pd_9.rb index 40c4d809..fa9ba2a7 100644 --- a/lib/vagrant-parallels/driver/pd_9.rb +++ b/lib/vagrant-parallels/driver/pd_9.rb @@ -192,14 +192,6 @@ def import(template_uuid) read_vms[vm_name] end - def mac_in_use?(mac) - valid_mac = mac.upcase.tr('^A-F0-9', '') - read_vms_info.each do |vm| - return true if valid_mac == vm.fetch('Hardware', {}).fetch('net0',{}).fetch('mac', '') - end - false - end - def read_bridged_interfaces net_list = read_virtual_networks diff --git a/locales/en.yml b/locales/en.yml index 118689c7..4cb2db2b 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -108,8 +108,3 @@ en: Parallels Desktop Version: %{parallels_version} export: compacting: Compacting exported HDDs... - match_mac: - generate: |- - The specified base MAC is already in use. Generating a new unique MAC - address for Shared network... - matching: Matching MAC address for Shared network... diff --git a/test/unit/support/shared/pd_driver_examples.rb b/test/unit/support/shared/pd_driver_examples.rb index 6a8899de..9b6ced6f 100644 --- a/test/unit/support/shared/pd_driver_examples.rb +++ b/test/unit/support/shared/pd_driver_examples.rb @@ -131,21 +131,6 @@ end end - describe "mac_in_use?" do - let(:vm_net0_mac) {'00AABBCC01'} - let(:vm_net1_mac) {'00AABBCC02'} - let(:tpl_net0_mac) {'00AABBCC03'} - let(:tpl_net1_mac) {'00AABBCC04'} - - it "checks the MAC address is already in use" do - - subject.mac_in_use?('00:AA:BB:CC:01').should be_true - subject.mac_in_use?('00:AA:BB:CC:02').should be_false - subject.mac_in_use?('00:AA:BB:CC:03').should be_true - subject.mac_in_use?('00:AA:BB:CC:04').should be_false - end - end - describe "read_settings" do it "returns a hash with detailed info about the VM" do subject.read_settings.should be_kind_of(Hash) From c4dbfc327a7a636e3aafd3d03ec3991c0b0c58d0 Mon Sep 17 00:00:00 2001 From: Mikhail Zholobov Date: Wed, 26 Feb 2014 11:34:38 +0400 Subject: [PATCH 26/33] 'ssh_info' will return `nil` until guest IP can be detected --- lib/vagrant-parallels/provider.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/vagrant-parallels/provider.rb b/lib/vagrant-parallels/provider.rb index aa0b63be..aa07f74f 100644 --- a/lib/vagrant-parallels/provider.rb +++ b/lib/vagrant-parallels/provider.rb @@ -48,9 +48,15 @@ def ssh_info # we return nil. return nil if state.id == :not_created + detected_ip = @machine.config.ssh.host || @driver.read_ip_dhcp + + # If ip couldn't be detected then we cannot possibly SSH into it, + # and should return nil too. + return nil if detected_ip.nil? + # Return ip from running machine, use ip from config if available return { - :host => @machine.config.ssh.host || @driver.read_ip_dhcp, + :host => detected_ip, :port => @driver.ssh_port(@machine.config.ssh.guest_port) } end From f0bc94f136a24df06ddf1b8f928710ffd530d702 Mon Sep 17 00:00:00 2001 From: Mikhail Zholobov Date: Thu, 27 Feb 2014 22:16:05 +0400 Subject: [PATCH 27/33] action.rb: added 'BoxCheckOutdated' --- lib/vagrant-parallels/action.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/vagrant-parallels/action.rb b/lib/vagrant-parallels/action.rb index d4019117..b9937557 100644 --- a/lib/vagrant-parallels/action.rb +++ b/lib/vagrant-parallels/action.rb @@ -203,6 +203,7 @@ def self.action_start Vagrant::Action::Builder.new.tap do |b| b.use CheckParallels b.use ConfigValidate + b.use BoxCheckOutdated b.use Call, IsRunning do |env, b2| # If the VM is running, then our work here is done, exit if env[:result] From 1fedbcaad1e67cb5eb19366d14a75eb93be43489 Mon Sep 17 00:00:00 2001 From: Mikhail Zholobov Date: Fri, 28 Feb 2014 12:24:47 +0400 Subject: [PATCH 28/33] Networking: use Vagrant guest capability instead Parallels Tools [GH-83] --- lib/vagrant-parallels/action/network.rb | 177 ++++++++++++++++++------ lib/vagrant-parallels/driver/pd_9.rb | 8 -- locales/en.yml | 2 + 3 files changed, 135 insertions(+), 52 deletions(-) diff --git a/lib/vagrant-parallels/action/network.rb b/lib/vagrant-parallels/action/network.rb index 90bcd601..0698a4e0 100644 --- a/lib/vagrant-parallels/action/network.rb +++ b/lib/vagrant-parallels/action/network.rb @@ -24,6 +24,7 @@ def initialize(app, env) def call(env) @env = env + # Get the list of network adapters from the configuration network_adapters_config = env[:machine].provider_config.network_adapters.dup @@ -59,10 +60,10 @@ def call(env) data = nil if type == :private_network # private_network = hostonly - data = [:hostonly, options] + data = [:hostonly, options] elsif type == :public_network # public_network = bridged - data = [:bridged, options] + data = [:bridged, options] end # Store it! @@ -72,6 +73,7 @@ def call(env) @logger.info("Determining adapters and compiling network configuration...") adapters = [] + networks = [] network_adapters_config.each do |slot, data| type = data[0] options = data[1] @@ -87,29 +89,53 @@ def call(env) adapter = send("#{type}_adapter", config) adapters << adapter @logger.debug("Adapter configuration: #{adapter.inspect}") + + # Get the network configuration + network = send("#{type}_network_config", config) + network[:auto_config] = config[:auto_config] + networks << network end if !adapters.empty? # Enable the adapters @logger.info("Enabling adapters...") - env[:ui].info I18n.t("vagrant.actions.vm.network.preparing") + env[:ui].output(I18n.t("vagrant.actions.vm.network.preparing")) + adapters.each do |adapter| + env[:ui].detail(I18n.t( + "vagrant_parallels.parallels.network_adapter", + adapter: adapter[:adapter].to_s, + type: adapter[:type].to_s, + extra: "", + )) + end + env[:machine].provider.driver.enable_adapters(adapters) end # Continue the middleware chain. @app.call(env) - end + # If we have networks to configure, then we configure it now, since + # that requires the machine to be up and running. + if !adapters.empty? && !networks.empty? + assign_interface_numbers(networks, adapters) - def bridged_config(options) - if options[:type] and options[:type].to_sym == :dhcp - options[:dhcp] = true + # Only configure the networks the user requested us to configure + networks_to_configure = networks.select { |n| n[:auto_config] } + if !networks_to_configure.empty? + env[:ui].info I18n.t("vagrant.actions.vm.network.configuring") + env[:machine].guest.capability(:configure_networks, networks_to_configure) + end end + end + def bridged_config(options) return { - :bridge => nil, - :mac => nil, - :nic_type => "e1000", + :auto_config => true, + :bridge => nil, + :mac => nil, + :nic_type => nil, + :use_dhcp_assigned_default_route => false }.merge(options || {}) end @@ -178,24 +204,40 @@ def bridged_adapter(config) # Given the choice we can now define the adapter we're using return { - :adapter => config[:adapter], - :type => :bridged, - :bridge => chosen_bridge[:name], - :bound_to => chosen_bridge[:bound_to], - :mac_address => config[:mac], - :dhcp => config[:dhcp], - :ip => config[:ip], - :netmask => config[:netmask], - :nic_type => config[:nic_type] + :adapter => config[:adapter], + :type => :bridged, + :bridge => chosen_bridge[:name], + :bound_to => chosen_bridge[:bound_to], + :mac_address => config[:mac], + :nic_type => config[:nic_type] + } + end + + def bridged_network_config(config) + if config[:ip] + options = { + :auto_config => true, + :mac => nil, + :netmask => "255.255.255.0", + :type => :static + }.merge(config) + options[:type] = options[:type].to_sym + return options + end + + return { + :type => :dhcp, + :use_dhcp_assigned_default_route => config[:use_dhcp_assigned_default_route] } end def hostonly_config(options) options = { - :mac => nil, - :nic_type => "e1000", - :netmask => "255.255.255.0", - :type => :static + :auto_config => true, + :mac => nil, + :nic_type => nil, + :netmask => "255.255.255.0", + :type => :static }.merge(options) # Make sure the type is a symbol @@ -248,12 +290,13 @@ def hostonly_config(options) end return { - :adapter_ip => options[:adapter_ip], - :ip => options[:ip], - :mac => options[:mac], - :netmask => options[:netmask], - :nic_type => options[:nic_type], - :type => options[:type], + :adapter_ip => options[:adapter_ip], + :auto_config => options[:auto_config], + :ip => options[:ip], + :mac => options[:mac], + :netmask => options[:netmask], + :nic_type => options[:nic_type], + :type => options[:type] }.merge(dhcp_options) end @@ -276,35 +319,81 @@ def hostonly_adapter(config) end return { - :adapter => config[:adapter], - :hostonly => interface[:name], - :bound_to => interface[:bound_to], - :mac => config[:mac], - :nic_type => config[:nic_type], - :type => :hostonly, - :dhcp => config[:type] == :dhcp ? interface[:dhcp] : false, - :ip => config[:ip], - :netmask => config[:netmask], + :adapter => config[:adapter], + :hostonly => interface[:name], + :bound_to => interface[:bound_to], + :mac => config[:mac], + :nic_type => config[:nic_type], + :type => :hostonly, + :dhcp => config[:type] == :dhcp ? interface[:dhcp] : false, + :ip => config[:ip], + :netmask => config[:netmask], } end + def hostonly_network_config(config) + return { + :type => config[:type], + :adapter_ip => config[:adapter_ip], + :ip => config[:ip], + :netmask => config[:netmask] + } + end + + def shared_config(options) - return {} + return { + :auto_config => false + } end def shared_adapter(config) return { - :adapter => config[:adapter], - :shared => "Shared", - :type => :shared, - :dhcp => true, - :nic_type => "e1000" + :adapter => config[:adapter], + :type => :shared } end + def shared_network_config(config) + return {} + end + #----------------------------------------------------------------- # Misc. helpers #----------------------------------------------------------------- + # Assigns the actual interface number of a network based on the + # enabled NICs on the virtual machine. + # + # This interface number is used by the guest to configure the + # NIC on the guest VM. + # + # The networks are modified in place by adding an ":interface" + # field to each. + def assign_interface_numbers(networks, adapters) + current = 0 + adapter_to_interface = {} + + # Make a first pass to assign interface numbers by adapter location + vm_adapters = @env[:machine].provider.driver.read_network_interfaces + vm_adapters.sort.each do |number, adapter| + if adapter[:type] != :none + # Not used, so assign the interface number and increment + adapter_to_interface[number] = current + current += 1 + end + end + + # Make a pass through the adapters to assign the :interface + # key to each network configuration. + adapters.each_index do |i| + adapter = adapters[i] + network = networks[i] + + # Figure out the interface number by simple lookup + network[:interface] = adapter_to_interface[adapter[:adapter]] + end + end + # This determines the next free network name def next_network_name # Get the list of numbers diff --git a/lib/vagrant-parallels/driver/pd_9.rb b/lib/vagrant-parallels/driver/pd_9.rb index fa9ba2a7..b148dfbb 100644 --- a/lib/vagrant-parallels/driver/pd_9.rb +++ b/lib/vagrant-parallels/driver/pd_9.rb @@ -135,14 +135,6 @@ def enable_adapters(adapters) args.concat(["--type", "shared"]) end - if adapter[:dhcp] - args.concat(["--dhcp", "yes"]) - elsif adapter[:ip] - args.concat(["--ipdel", "all", "--ipadd", "#{adapter[:ip]}/#{adapter[:netmask]}"]) - else - args.concat(["--dhcp", "no"]) - end - if adapter[:mac_address] args.concat(["--mac", adapter[:mac_address]]) end diff --git a/locales/en.yml b/locales/en.yml index 4cb2db2b..5b27e785 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -3,6 +3,8 @@ en: parallels: checking_guest_tools: |- Checking for Parallels Tools installed on the VM... + network_adapter: |- + Adapter %{adapter}: %{type}%{extra} #------------------------------------------------------------------------------- # Translations for exception classes #------------------------------------------------------------------------------- From ca0af6353f52d03caecfffb59572e314aeb2c7a6 Mon Sep 17 00:00:00 2001 From: Mikhail Zholobov Date: Fri, 28 Feb 2014 13:19:33 +0400 Subject: [PATCH 29/33] driver/pd_8: Disable usage of Parallels Tools for guest's network config [GH-83] --- lib/vagrant-parallels/driver/pd_8.rb | 8 -------- 1 file changed, 8 deletions(-) diff --git a/lib/vagrant-parallels/driver/pd_8.rb b/lib/vagrant-parallels/driver/pd_8.rb index c6cd4fd3..913bd6bc 100644 --- a/lib/vagrant-parallels/driver/pd_8.rb +++ b/lib/vagrant-parallels/driver/pd_8.rb @@ -135,14 +135,6 @@ def enable_adapters(adapters) args.concat(["--type", "shared"]) end - if adapter[:dhcp] - args.concat(["--dhcp", "yes"]) - elsif adapter[:ip] - args.concat(["--ipdel", "all", "--ipadd", "#{adapter[:ip]}/#{adapter[:netmask]}"]) - else - args.concat(["--dhcp", "no"]) - end - if adapter[:mac_address] args.concat(["--mac", adapter[:mac_address]]) end From f026dbff1f7abc29dad37c6c20d961ae6ddfd0fd Mon Sep 17 00:00:00 2001 From: Mikhail Zholobov Date: Sat, 1 Mar 2014 11:25:12 +0400 Subject: [PATCH 30/33] action/network: Deleted unused attributes of host-only adapter [GH-83] --- lib/vagrant-parallels/action/network.rb | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/vagrant-parallels/action/network.rb b/lib/vagrant-parallels/action/network.rb index 0698a4e0..34ba910d 100644 --- a/lib/vagrant-parallels/action/network.rb +++ b/lib/vagrant-parallels/action/network.rb @@ -324,10 +324,7 @@ def hostonly_adapter(config) :bound_to => interface[:bound_to], :mac => config[:mac], :nic_type => config[:nic_type], - :type => :hostonly, - :dhcp => config[:type] == :dhcp ? interface[:dhcp] : false, - :ip => config[:ip], - :netmask => config[:netmask], + :type => :hostonly } end From cd6195b6f6db5ceb8547ed2919ef50ea3b86e388 Mon Sep 17 00:00:00 2001 From: Mikhail Zholobov Date: Sat, 1 Mar 2014 11:31:31 +0400 Subject: [PATCH 31/33] Added requirement of Mac OS X platform. --- lib/vagrant-parallels/errors.rb | 4 ++++ lib/vagrant-parallels/provider.rb | 4 ++++ locales/en.yml | 2 ++ 3 files changed, 10 insertions(+) diff --git a/lib/vagrant-parallels/errors.rb b/lib/vagrant-parallels/errors.rb index 538e784b..8ff63ee5 100644 --- a/lib/vagrant-parallels/errors.rb +++ b/lib/vagrant-parallels/errors.rb @@ -30,6 +30,10 @@ class ParallelsNoRoomForHighLevelNetwork < VagrantParallelsError class VMInaccessible < VagrantParallelsError error_key(:vm_inaccessible) end + + class MacOSXRequired < VagrantParallelsError + error_key(:mac_os_x_required) + end end end end \ No newline at end of file diff --git a/lib/vagrant-parallels/provider.rb b/lib/vagrant-parallels/provider.rb index aa07f74f..2c018c5b 100644 --- a/lib/vagrant-parallels/provider.rb +++ b/lib/vagrant-parallels/provider.rb @@ -10,6 +10,10 @@ def initialize(machine) @logger = Log4r::Logger.new("vagrant::provider::parallels") @machine = machine + if !Vagrant::Util::Platform.darwin? + raise Errors::MacOSXRequired + end + # This method will load in our driver, so we call it now to # initialize it. machine_id_changed diff --git a/locales/en.yml b/locales/en.yml index 5b27e785..7e27c6d2 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -9,6 +9,8 @@ en: # Translations for exception classes #------------------------------------------------------------------------------- errors: + mac_os_x_required: |- + Parallels provider only works on OS X (or Mac OS X). prlctl_error: |- There was an error while executing `prlctl`, a CLI used by Vagrant for controlling Parallels Desktop. The command and stderr is shown below. From faa74956854ef9069a4967930a575f8a5d626cb9 Mon Sep 17 00:00:00 2001 From: Mikhail Zholobov Date: Sat, 1 Mar 2014 23:26:56 +0400 Subject: [PATCH 32/33] driver: fixed error in 'import' interruption --- lib/vagrant-parallels/driver/pd_8.rb | 1 + lib/vagrant-parallels/driver/pd_9.rb | 1 + 2 files changed, 2 insertions(+) diff --git a/lib/vagrant-parallels/driver/pd_8.rb b/lib/vagrant-parallels/driver/pd_8.rb index 913bd6bc..07cd07ca 100644 --- a/lib/vagrant-parallels/driver/pd_8.rb +++ b/lib/vagrant-parallels/driver/pd_8.rb @@ -307,6 +307,7 @@ def read_settings def read_state vm = json { execute('list', @uuid, '--json', retryable: true).gsub(/^INFO/, '') } + return nil if !vm.last vm.last.fetch('status').to_sym end diff --git a/lib/vagrant-parallels/driver/pd_9.rb b/lib/vagrant-parallels/driver/pd_9.rb index b148dfbb..b11f0dac 100644 --- a/lib/vagrant-parallels/driver/pd_9.rb +++ b/lib/vagrant-parallels/driver/pd_9.rb @@ -307,6 +307,7 @@ def read_settings def read_state vm = json { execute('list', @uuid, '--json', retryable: true) } + return nil if !vm.last vm.last.fetch('status').to_sym end From 078f58ad768a288b669f6cb3620717457553e37f Mon Sep 17 00:00:00 2001 From: Mikhail Zholobov Date: Sun, 2 Mar 2014 13:02:14 +0400 Subject: [PATCH 33/33] driver: fixed condition 'adapter type is shared' --- lib/vagrant-parallels/driver/pd_8.rb | 2 +- lib/vagrant-parallels/driver/pd_9.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/vagrant-parallels/driver/pd_8.rb b/lib/vagrant-parallels/driver/pd_8.rb index 07cd07ca..30eef64d 100644 --- a/lib/vagrant-parallels/driver/pd_8.rb +++ b/lib/vagrant-parallels/driver/pd_8.rb @@ -131,7 +131,7 @@ def enable_adapters(adapters) args.concat(["--type", "bridged", "--iface", adapter[:bound_to]]) end - if adapter[:shared] + if adapter[:type] == :shared args.concat(["--type", "shared"]) end diff --git a/lib/vagrant-parallels/driver/pd_9.rb b/lib/vagrant-parallels/driver/pd_9.rb index b11f0dac..ee24a904 100644 --- a/lib/vagrant-parallels/driver/pd_9.rb +++ b/lib/vagrant-parallels/driver/pd_9.rb @@ -131,7 +131,7 @@ def enable_adapters(adapters) args.concat(["--type", "bridged", "--iface", adapter[:bound_to]]) end - if adapter[:shared] + if adapter[:type] == :shared args.concat(["--type", "shared"]) end