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/README.md b/README.md index 16bc16d6..58f5f4af 100644 --- a/README.md +++ b/README.md @@ -66,60 +66,50 @@ along with a `Vagrantfile` that does default settings for the provider-specific configuration for this provider. ## Networking -By default 'vagrant-parallels' uses the basic Vagrant networking approach. By default VM has one adapter assigned to the 'Shared' network in Parallels Desktop. -But you can also add one ore more `:private_network` adapters, as described below: - -### Private Network -It is fully compatible with basic Vagrant [Private Networks](http://docs.vagrantup.com/v2/networking/private_network.html). -#### Available arguments: -- `type` - IP configuration way: `:static` or `:dhcp` (exactly Symbol object). Default is `:static`. If `:dchp` is set, such interface will get an IP dynamically from default subnet "10.37.129.1/255.255.255.0". -- `mac` - MAC address which will be assigned to this network adapter. If omitted, MAC will be automatically generated at the first `up` of VM. -- `ip` - IP address which will be assigned to this network adapter. It is required only if type is `:static`. -- `netmask` - network mask. Default is `"255.255.255.0"`. It is required only if type is `:static`. -- `nic_type` - Unnecessary argument, means the type of network adapter. Can be any of `"virtio"`, `"e1000"` or `"rtl"`. Default is `"e1000"`. - -#### Example: -```ruby -Vagrant.configure("2") do |config| - config.vm.network :private_network, ip: "33.33.33.50", netmask: "255.255.0.0" - config.vm.network :private_network, type: :dhcp, nic_type: "rtl" -end -``` -It means that two private network adapters will be configured: -1) The first will have static ip '33.33.33.50' and mask '255.255.0.0'. It will be represented as device `"e1000"` by default (e.g. 'Intel(R) PRO/1000 MT'). -2) The second adapter will be configured as `"rtl"` ('Realtek RTL8029AS') and get an IP from internal DHCP server, which is working on the default network "10.37.129.1/255.255.255.0". - -### Public Network -It is fully compatible with basic Vagrant [Public Networks](http://docs.vagrantup.com/v2/networking/public_network.html). -#### Available arguments (unnecessary, but provider specific): -- `bridge` - target host's interface for bridged network. You can specify full (ex: `Wi-Fi`) or short (ex: `en0`) name of interface. If omitted, you will be asked to choose the interface during the VM boot (or if only one interface exists, it will be chosen automatically). -_Hint:_ Full names of network interfaces are displayed in _System Preferences -> Network_ window, and short names - in the `ifconfig` command output on your Mac. -- `mac` - MAC address which will be assigned to this network adapter. If omitted, MAC will be automatically generated at the first `up` of VM. -- `ip` - IP address which will be assigned to this network adapter. Use it, if you want to configure adapter manually. -- `netmask` - network mask. Default is `"255.255.255.0"`. It is used only in pair with `ip` -- `type` - IP configuration way, only `:dhcp` is available. Use it only if your public network has a valid DHCP server. Otherwise, omit this attribute or use an `ip` and `netmask` described above. -- `nic_type` - type of network adapter. Can be any of `"virtio"`, `"e1000"` or `"rtl"`. Default is `"e1000"`. - -#### Example: +By default Vagrant Parallels provider uses the basic Vagrant networking +approach. Initially VM has one adapter assigned to the 'Shared' network +in Parallels Desktop. + +But you can also add `:private_network` and `:public_network` adapters. +These features are working by the same way as in the basic Vagrant: +- [Private Networks] +(http://docs.vagrantup.com/v2/networking/private_network.html) +- [Public Networks] +(http://docs.vagrantup.com/v2/networking/public_network.html) + +## Provider Specific Configuration + +Parallels Desktop has a `prlctl` utility that can be used to make modifications +to Parallels virtual machines from the command line. + + +Parallels provider exposes a way to call any command against *prlctl* just prior +to booting the machine: + ```ruby -Vagrant.configure("2") do |config| - config.vm.network :public_network, bridge: "Wi-Fi", mac: "001C425FC3AB", type: :dhcp - config.vm.network :public_network, bridge: "en4", ip: "10.3.1.18", netmask: "255.255.252.0" +config.vm.provider "parallels" do |v| + v.customize ["set", :id, "--device-set", "cdrom0", "--image", + "/path/to/disk.iso", "--connect"] end ``` -It means that two public network adapters will be configured: -1) The first will be bridged to the 'Wi-Fi' host machine's interface and will have the specified MAC address. After the VM boot it will be automatically configured to get an IP from the DHCP server, which is accessible in the 'Wi-Fi' network). -2) The second adapter will be bridged to the interface 'en4' and will have static ip '10.3.1.18' and mask '255.255.252.0'. -## Provider Specific Configuration +In the example above, the VM is modified to have a specified iso image attached +to it's virtual media device (cdrom). Some details: -Provider allows to define directives for `prlctl set` through `Vagrantfile` in same way as this done in VirtualBox provider. +* The `:id` special parameter is replaced with the ID of the virtual + machine being created, so when a *prlctl* command requires an ID, you + can pass this special parameter. + +* Multiple `customize` directives can be used. They will be executed in the + order given. + +There are some convenience shortcuts for memory and CPU settings: -#### Example: ```ruby - config.vm.provider :parallels do |parallels| - parallels.name = "HipHop VM" - parallels.customize ["set", :id, "--memsize", "1024"] +config.vm.provider "parallels" do |v| + v.memory = 1024 + v.cpus = 2 +end ``` ## Development @@ -177,35 +167,12 @@ So, now that you have your own plugin installed, check it with the command `vagr A great thanks to the people who helping this project stand on its feet, thank you -* Kevin Kaland `@wizonesolutions` +* Youssef Shahin `@yshahin` - plugin's author * Mikhail Zholobov `@legal90` +* Kevin Kaland `@wizonesolutions` +* Konstantin Nazarov `@racktear` * Dmytro Vasylenko `@odi-um` * Thomas Koschate `@koschate` -and to all the people who are using and testing this tool - -## Copyright & license - -The MIT License (MIT) - -Copyright (c) 2014 Youssef Shahin - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +and to all the people who are using and testing this provider 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/lib/vagrant-parallels/action.rb b/lib/vagrant-parallels/action.rb index f3c60aa7..4f9ddcb3 100644 --- a/lib/vagrant-parallels/action.rb +++ b/lib/vagrant-parallels/action.rb @@ -261,11 +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 b.use action_start @@ -288,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__) @@ -298,13 +294,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/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/action/check_guest_tools.rb b/lib/vagrant-parallels/action/check_guest_tools.rb index 7c256d69..ac313dd7 100644 --- a/lib/vagrant-parallels/action/check_guest_tools.rb +++ b/lib/vagrant-parallels/action/check_guest_tools.rb @@ -4,9 +4,17 @@ 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].info(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 +22,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/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/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/export.rb b/lib/vagrant-parallels/action/export.rb index 81f49476..88fb7715 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?(tpl_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 942ae2e1..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_all_names.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] - - 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/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/network.rb b/lib/vagrant-parallels/action/network.rb index b5555484..41b20878 100644 --- a/lib/vagrant-parallels/action/network.rb +++ b/lib/vagrant-parallels/action/network.rb @@ -24,11 +24,15 @@ 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 + # 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 @@ -56,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! @@ -69,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] @@ -84,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].info(I18n.t("vagrant.actions.vm.network.preparing")) + adapters.each do |adapter| + env[:ui].info(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 @@ -175,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 @@ -229,14 +274,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 @@ -245,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 @@ -273,35 +319,78 @@ 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 => interface[:dhcp], - :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 } 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/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/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/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/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 deleted file mode 100644 index 542d7f11..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_all_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/config.rb b/lib/vagrant-parallels/config.rb index d1ad8663..8ac49169 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 :check_guest_additions= :check_guest_tools= + def initialize + @check_guest_tools = UNSET_VALUE @customizations = [] @destroy_unused_network_interfaces = UNSET_VALUE @network_adapters = {} @@ -21,8 +26,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 @@ -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 diff --git a/lib/vagrant-parallels/driver/base.rb b/lib/vagrant-parallels/driver/base.rb new file mode 100644 index 00000000..120f0887 --- /dev/null +++ b/lib/vagrant-parallels/driver/base.rb @@ -0,0 +1,281 @@ +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 list of required CLI utils + @cli_paths = { + :prlctl => "prlctl", + :prlsrvctl => "prlsrvctl", + :prl_disk_tool => "prl_disk_tool", + :ifconfig => "ifconfig" + } + + @cli_paths.each do |name, path| + @logger.info("CLI utility '#{name}' path: #{path}") + end + 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`, `:bound_to`, `:ip`, `:netmask` and `:dhcp` + def create_host_only_network(options) + end + + # Deletes the virtual machine references by this driver. + 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 + + # Enables network adapters on the VM. + # + # The format of each adapter specification should be like so: + # + # { + # :type => :hostonly, + # :hostonly => "vagrant-vnet0", + # :bound_to => "vnic2", + # :nic_type => "virtio" + # } + # + # 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 '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 prlctl 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 by cloning from registered template. + # + # @param [String] template_uuid Registered template UUID. + # @return [String] UUID of the imported VM. + def import(template_uuid) + 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. + 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 registered + # virtual machines and templates. + # + # @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. + 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. + # + def start + 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 + + # 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 + command << { :notify => [:stdout, :stderr] } + + # 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) + 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..1d2b3e2b --- /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_8, + "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, + :compact, + :create_host_only_network, + :delete, + :delete_disabled_adapters, + :delete_unused_host_only_networks, + :enable_adapters, + :execute_command, + :export, + #:forward_ports, + :halt, + :import, + :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_settings, + :read_state, + :read_used_ports, + :read_virtual_networks, + :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_8.rb similarity index 56% rename from lib/vagrant-parallels/driver/prl_ctl.rb rename to lib/vagrant-parallels/driver/pd_8.rb index ece2b93d..30eef64d 100644 --- a/lib/vagrant-parallels/driver/prl_ctl.rb +++ b/lib/vagrant-parallels/driver/pd_8.rb @@ -1,54 +1,41 @@ 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 8. + class PD_8 < 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_8") @uuid = uuid + end - # 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}") + 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 compact(uuid=nil) - uuid ||= @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 - # regular expression to find what we're looking for. - if lines.last =~ /.+?(\d{,3}) ?%/ - yield $1.to_i if block_given? - 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 @@ -72,27 +59,19 @@ 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 - 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) @@ -101,19 +80,19 @@ 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. 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)} + networks.delete_if { |net| net['Bound To'] == net_params.fetch('iface', nil) } end end @@ -152,18 +131,10 @@ 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 - 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 @@ -176,17 +147,20 @@ 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 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 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) @@ -195,7 +169,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 @@ -204,71 +181,23 @@ def import(template_uuid, vm_name) yield $1.to_i if block_given? end end - @uuid = read_settings(vm_name).fetch('ID', vm_name) - end - - def ip - 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 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', '') - end - - valid_mac = mac.upcase.tr('^A-F0-9', '') - - all_macs_in_use.include?(valid_mac) - 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') - end - - list - end - - # Returns a hash of all UUIDs assigned to VMs and templates currently - # known by Parallels. Keys are 'Home' directories - # - # @return [Hash] - def read_all_paths - list = {} - read_all_info.each do |item| - if Dir.exists? item.fetch('Home') - list[File.realpath item.fetch('Home')] = item.fetch('ID') - end - end - - list + read_vms[vm_name] 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'] @@ -298,7 +227,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 = [] @@ -325,7 +254,17 @@ def read_host_only_interfaces end hostonly_ifaces << info end - hostonly_ifaces + 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 @@ -361,46 +300,77 @@ def read_network_interfaces nics end - # Returns the current state of this VM. - # - # @return [Symbol] + def read_settings + vm = json { execute('list', @uuid, '--info', '--json', retryable: true).gsub(/^INFO/, '') } + vm.last + end + def read_state - read_settings(@uuid).fetch('State', 'inaccessible').to_sym + vm = json { execute('list', @uuid, '--json', retryable: true).gsub(/^INFO/, '') } + return nil if !vm.last + vm.last.fetch('status').to_sym end def read_virtual_networks json { execute(:prlsrvctl, 'net', 'list', '--json', retryable: true) } end - def ready? - !!guest_execute('uname') rescue false + 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?(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) + 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 - # apply custom vm setting via set parameter - def set_vm_settings(command) - raw(@prlctl_path, *command) + def set_name(name) + execute('set', @uuid, '--name', name, :retryable => true) end def share_folders(folders) @@ -411,7 +381,7 @@ def share_folders(folders) end def ssh_port(expected_port) - 22 + expected_port end def start @@ -426,9 +396,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 @@ -441,115 +408,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/driver/pd_9.rb b/lib/vagrant-parallels/driver/pd_9.rb new file mode 100644 index 00000000..ee24a904 --- /dev/null +++ b/lib/vagrant-parallels/driver/pd_9.rb @@ -0,0 +1,417 @@ +require 'log4r' + +require 'vagrant/util/platform' + +require File.expand_path("../base", __FILE__) + +module VagrantPlugins + module Parallels + module Driver + # Driver for Parallels Desktop 9. + class PD_9 < Base + def initialize(uuid) + super() + + @logger = Log4r::Logger.new("vagrant::provider::parallels::pd_9") + @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[:type] == :shared + args.concat(["--type", "shared"]) + 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 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) } + vm.last + end + + def read_state + vm = json { execute('list', @uuid, '--json', retryable: true) } + return nil if !vm.last + 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) + end + templates_arr = json([]) do + execute('list', '--all', '--json', '--template', retryable: true) + 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) + end + templates_arr = json([]) do + execute('list', '--all','--info', '--json', '--template', retryable: true) + 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/errors.rb b/lib/vagrant-parallels/errors.rb index 4876c798..8ff63ee5 100644 --- a/lib/vagrant-parallels/errors.rb +++ b/lib/vagrant-parallels/errors.rb @@ -7,25 +7,33 @@ 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 + + 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/plugin.rb b/lib/vagrant-parallels/plugin.rb index ad915ff3..a4c5946d 100644 --- a/lib/vagrant-parallels/plugin.rb +++ b/lib/vagrant-parallels/plugin.rb @@ -47,14 +47,14 @@ class Plugin < Vagrant.plugin("2") end - module Driver - autoload :PrlCtl, File.expand_path("../driver/prl_ctl", __FILE__) - end + autoload :Action, File.expand_path("../action", __FILE__) - module Util - def generate_name(path, suffix='') - "#{path.basename.to_s.gsub(/[^-a-z0-9_]/i, '')}#{suffix}_#{Time.now.to_i}" - end + # Drop some autoloads in here to optimize the performance of loading + # our drivers only when they are needed. + module Driver + autoload :Meta, File.expand_path("../driver/meta", __FILE__) + autoload :PD_8, File.expand_path("../driver/pd_8", __FILE__) + autoload :PD_9, File.expand_path("../driver/pd_9", __FILE__) end end end \ No newline at end of file diff --git a/lib/vagrant-parallels/provider.rb b/lib/vagrant-parallels/provider.rb index c150645f..2c018c5b 100644 --- a/lib/vagrant-parallels/provider.rb +++ b/lib/vagrant-parallels/provider.rb @@ -9,7 +9,14 @@ 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) + + 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 end # @see Vagrant::Plugin::V2::Provider#action @@ -22,15 +29,38 @@ 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 # 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.ip, + :host => detected_ip, :port => @driver.ssh_port(@machine.config.ssh.guest_port) } end @@ -65,4 +95,4 @@ def to_s end end end -end \ No newline at end of file +end diff --git a/lib/vagrant-parallels/version.rb b/lib/vagrant-parallels/version.rb index 75154aae..ae1917f2 100644 --- a/lib/vagrant-parallels/version.rb +++ b/lib/vagrant-parallels/version.rb @@ -1,5 +1,5 @@ module VagrantPlugins module Parallels - VERSION = "0.2.1" + VERSION = "0.2.2.rc1" end end diff --git a/locales/en.yml b/locales/en.yml index 1962a74f..7e27c6d2 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -1,9 +1,16 @@ en: vagrant_parallels: + parallels: + checking_guest_tools: |- + Checking for Parallels Tools installed on the VM... + network_adapter: |- + Adapter %{adapter}: %{type}%{extra} #------------------------------------------------------------------------------- # 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. @@ -11,18 +18,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" @@ -30,6 +34,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 #------------------------------------------------------------------------------- @@ -69,7 +88,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: @@ -92,9 +111,4 @@ en: Parallels Tools Version: %{tools_version} Parallels Desktop Version: %{parallels_version} export: - compacting: Compacting exported HDD... - 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... + compacting: Compacting exported HDDs... 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 8d4e7f42..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') @@ -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 diff --git a/test/unit/driver/pd_8_test.rb b/test/unit/driver/pd_8_test.rb new file mode 100644 index 00000000..3ee45f07 --- /dev/null +++ b/test/unit/driver/pd_8_test.rb @@ -0,0 +1,196 @@ +require_relative "../base" + +describe VagrantPlugins::Parallels::Driver::PD_8 do + include_context "parallels" + let(:parallels_version) { "8" } + + let(:vm_name) {'VM_Name'} + + let(:tpl_uuid) {'1234-some-template-uuid-5678'} + let(:tpl_name) {'Some_Template_Name'} + + 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": "/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 + + # 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": "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 + + # 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/driver/pd_9_test.rb b/test/unit/driver/pd_9_test.rb new file mode 100644 index 00000000..b3b81770 --- /dev/null +++ b/test/unit/driver/pd_9_test.rb @@ -0,0 +1,196 @@ +require_relative "../base" + +describe VagrantPlugins::Parallels::Driver::PD_9 do + include_context "parallels" + let(:parallels_version) { "9" } + + let(:vm_name) {'VM_Name'} + + let(:tpl_uuid) {'1234-some-template-uuid-5678'} + let(:tpl_name) {'Some_Template_Name'} + + 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 +[ + { + "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 +[ + { + "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 +[ + { + "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 + + # 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 +[ + { + "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 + + # 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/driver/prl_ctl_test.rb b/test/unit/driver/prl_ctl_test.rb deleted file mode 100644 index ec5d4067..00000000 --- a/test/unit/driver/prl_ctl_test.rb +++ /dev/null @@ -1,148 +0,0 @@ -require_relative "../base" - -describe VagrantPlugins::Parallels::Driver::PrlCtl do - include_context "parallels" - - subject { VagrantPlugins::Parallels::Driver::PrlCtl.new(uuid) } - - describe "compact" do - it "compacts the VM disk images" do - pending "Should have possibility to compact more than one hdd" - 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_settings).with(tpl_name). - and_return({"ID" => 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 - 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_all_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\.]+)/) - 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) - end - end -end 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/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..16d13058 --- /dev/null +++ b/test/unit/support/shared/pd_driver_examples.rb @@ -0,0 +1,243 @@ +shared_examples "parallels desktop driver" do |options| + before do + 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 + 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 + 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(exit_code: 0)) + 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 + vnic_opts = { + :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_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_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_opts) + + 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 + + it "creates host-only NIC without dhcp" 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 "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 + 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 + 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 "halt" do + it "stops the VM" do + subprocess.should_receive(:execute). + with("prlctl", "stop", uuid, an_instance_of(Hash)). + 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(exit_code: 0)) + subject.halt(force=true) + 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 = driver.send(:read_settings) + subject.read_vms_info.should include(vm_settings) + 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(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(exit_code: 0)) + 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(exit_code: 0)) + 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(exit_code: 0)) + 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(exit_code: 0)) + 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 new file mode 100644 index 00000000..cbe240ef --- /dev/null +++ b/test/unit/synced_folder_test.rb @@ -0,0 +1,37 @@ +require "vagrant" +require_relative "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 diff --git a/vagrant-parallels.gemspec b/vagrant-parallels.gemspec index 71326d59..6cb4cf52 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" @@ -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