vagrant-parallels 0.0.1.dev

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +23 -0
  3. data/.ruby-gemset +1 -0
  4. data/Gemfile +13 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +59 -0
  7. data/Rakefile +23 -0
  8. data/lib/vagrant-parallels.rb +18 -0
  9. data/lib/vagrant-parallels/action.rb +282 -0
  10. data/lib/vagrant-parallels/action/boot.rb +21 -0
  11. data/lib/vagrant-parallels/action/check_accessible.rb +23 -0
  12. data/lib/vagrant-parallels/action/check_created.rb +21 -0
  13. data/lib/vagrant-parallels/action/check_guest_tools.rb +32 -0
  14. data/lib/vagrant-parallels/action/check_parallels.rb +22 -0
  15. data/lib/vagrant-parallels/action/check_running.rb +21 -0
  16. data/lib/vagrant-parallels/action/clear_network_interfaces.rb +19 -0
  17. data/lib/vagrant-parallels/action/clear_shared_folders.rb +17 -0
  18. data/lib/vagrant-parallels/action/created.rb +20 -0
  19. data/lib/vagrant-parallels/action/destroy.rb +19 -0
  20. data/lib/vagrant-parallels/action/forced_halt.rb +20 -0
  21. data/lib/vagrant-parallels/action/import.rb +63 -0
  22. data/lib/vagrant-parallels/action/is_paused.rb +21 -0
  23. data/lib/vagrant-parallels/action/is_running.rb +20 -0
  24. data/lib/vagrant-parallels/action/is_saved.rb +21 -0
  25. data/lib/vagrant-parallels/action/match_mac_address.rb +21 -0
  26. data/lib/vagrant-parallels/action/message_already_running.rb +16 -0
  27. data/lib/vagrant-parallels/action/message_not_created.rb +16 -0
  28. data/lib/vagrant-parallels/action/message_not_running.rb +16 -0
  29. data/lib/vagrant-parallels/action/message_will_not_destroy.rb +17 -0
  30. data/lib/vagrant-parallels/action/prepare_nfs_settings.rb +64 -0
  31. data/lib/vagrant-parallels/action/prune_nfs_exports.rb +20 -0
  32. data/lib/vagrant-parallels/action/register_template.rb +22 -0
  33. data/lib/vagrant-parallels/action/resume.rb +25 -0
  34. data/lib/vagrant-parallels/action/share_folders.rb +121 -0
  35. data/lib/vagrant-parallels/action/suspend.rb +20 -0
  36. data/lib/vagrant-parallels/action/unregister_template.rb +24 -0
  37. data/lib/vagrant-parallels/driver/prl_ctl.rb +281 -0
  38. data/lib/vagrant-parallels/errors.rb +23 -0
  39. data/lib/vagrant-parallels/plugin.rb +79 -0
  40. data/lib/vagrant-parallels/provider.rb +68 -0
  41. data/lib/vagrant-parallels/version.rb +5 -0
  42. data/locales/en.yml +1137 -0
  43. data/spec/vagrant-parallels/setup_spec.rb +7 -0
  44. data/vagrant-parallels.gemspec +59 -0
  45. metadata +158 -0
@@ -0,0 +1,16 @@
1
+ module VagrantPlugins
2
+ module Parallels
3
+ module Action
4
+ class MessageNotRunning
5
+ def initialize(app, env)
6
+ @app = app
7
+ end
8
+
9
+ def call(env)
10
+ env[:ui].info I18n.t("vagrant.commands.common.vm_not_running")
11
+ @app.call(env)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,17 @@
1
+ module VagrantPlugins
2
+ module Parallels
3
+ module Action
4
+ class MessageWillNotDestroy
5
+ def initialize(app, env)
6
+ @app = app
7
+ end
8
+
9
+ def call(env)
10
+ env[:ui].info I18n.t("vagrant.commands.destroy.will_not_destroy",
11
+ :name => env[:machine].name)
12
+ @app.call(env)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,64 @@
1
+ module VagrantPlugins
2
+ module Parallels
3
+ module Action
4
+ class PrepareNFSSettings
5
+ def initialize(app,env)
6
+ @app = app
7
+ @logger = Log4r::Logger.new("vagrant::action::vm::nfs")
8
+ end
9
+
10
+ def call(env)
11
+ @app.call(env)
12
+
13
+ using_nfs = false
14
+ env[:machine].config.vm.synced_folders.each do |id, opts|
15
+ if opts[:nfs]
16
+ using_nfs = true
17
+ break
18
+ end
19
+ end
20
+
21
+ if using_nfs
22
+ @logger.info("Using NFS, preparing NFS settings by reading host IP and machine IP")
23
+ env[:nfs_host_ip] = read_host_ip(env[:machine])
24
+ env[:nfs_machine_ip] = read_machine_ip(env[:machine])
25
+
26
+ raise Vagrant::Errors::NFSNoHostonlyNetwork if !env[:nfs_machine_ip]
27
+ end
28
+ end
29
+
30
+ # Returns the IP address of the first host only network adapter
31
+ #
32
+ # @param [Machine] machine
33
+ # @return [String]
34
+ def read_host_ip(machine)
35
+ machine.provider.driver.read_network_interfaces.each do |adapter, opts|
36
+ if opts[:type] == :hostonly
37
+ machine.provider.driver.read_host_only_interfaces.each do |interface|
38
+ if interface[:name] == opts[:hostonly]
39
+ return interface[:ip]
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ nil
46
+ end
47
+
48
+ # Returns the IP address of the guest by looking at the first
49
+ # enabled host only network.
50
+ #
51
+ # @return [String]
52
+ def read_machine_ip(machine)
53
+ machine.config.vm.networks.each do |type, options|
54
+ if type == :private_network && options[:ip].is_a?(String)
55
+ return options[:ip]
56
+ end
57
+ end
58
+
59
+ nil
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,20 @@
1
+ module VagrantPlugins
2
+ module Parallels
3
+ module Action
4
+ class PruneNFSExports
5
+ def initialize(app, env)
6
+ @app = app
7
+ end
8
+
9
+ def call(env)
10
+ if env[:host]
11
+ vms = env[:machine].provider.driver.read_vms
12
+ env[:host].nfs_prune(vms.values)
13
+ end
14
+
15
+ @app.call(env)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,22 @@
1
+ module VagrantPlugins
2
+ module Parallels
3
+ module Action
4
+ class RegisterTemplate
5
+ def initialize(app, env)
6
+ @app = app
7
+ end
8
+
9
+ def call(env)
10
+ pvm_file = Pathname.glob(env[:machine].box.directory.join('*.pvm')).first
11
+
12
+ unless env[:machine].provider.driver.registered?(pvm_file.basename.to_s[0...-4])
13
+ env[:machine].provider.driver.register(pvm_file.to_s)
14
+ end
15
+ # Call the next if we have one (but we shouldn't, since this
16
+ # middleware is built to run with the Call-type middlewares)
17
+ @app.call(env)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,25 @@
1
+ module VagrantPlugins
2
+ module Parallels
3
+ module Action
4
+ class Resume
5
+ def initialize(app, env)
6
+ @app = app
7
+ end
8
+
9
+ def call(env)
10
+ current_state = env[:machine].provider.state.id
11
+
12
+ if current_state == :suspended
13
+ env[:ui].info I18n.t("vagrant.actions.vm.resume.unpausing")
14
+ env[:machine].provider.driver.resume
15
+ elsif current_state == :stopped
16
+ env[:ui].info I18n.t("vagrant.actions.vm.resume.resuming")
17
+ env[:action_runner].run(Boot, env)
18
+ end
19
+
20
+ @app.call(env)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,121 @@
1
+ require "pathname"
2
+
3
+ require "log4r"
4
+
5
+ require "vagrant/util/platform"
6
+ require "vagrant/util/scoped_hash_override"
7
+
8
+ module VagrantPlugins
9
+ module Parallels
10
+ module Action
11
+ class ShareFolders
12
+ include Vagrant::Util::ScopedHashOverride
13
+
14
+ def initialize(app, env)
15
+ @logger = Log4r::Logger.new("vagrant::action::vm::share_folders")
16
+ @app = app
17
+ end
18
+
19
+ def call(env)
20
+ @env = env
21
+
22
+ prepare_folders
23
+ create_metadata
24
+
25
+ @app.call(env)
26
+
27
+ mount_shared_folders
28
+ end
29
+
30
+ # This method returns an actual list of VirtualBox shared
31
+ # folders to create and their proper path.
32
+ def shared_folders
33
+ {}.tap do |result|
34
+ @env[:machine].config.vm.synced_folders.each do |id, data|
35
+ data = scoped_hash_override(data, :parallels)
36
+
37
+ # Ignore NFS shared folders
38
+ next if data[:nfs]
39
+
40
+ # Ignore disabled shared folders
41
+ next if data[:disabled]
42
+
43
+ # This to prevent overwriting the actual shared folders data
44
+ id = Pathname.new(id).to_s.split('/').drop_while{|i| i.empty?}.join('_')
45
+ result[id] = data.dup
46
+ end
47
+ end
48
+ end
49
+
50
+ # Prepares the shared folders by verifying they exist and creating them
51
+ # if they don't.
52
+ def prepare_folders
53
+ shared_folders.each do |id, options|
54
+ hostpath = Pathname.new(options[:hostpath]).expand_path(@env[:root_path])
55
+
56
+ if !hostpath.directory? && options[:create]
57
+ # Host path doesn't exist, so let's create it.
58
+ @logger.debug("Host path doesn't exist, creating: #{hostpath}")
59
+
60
+ begin
61
+ hostpath.mkpath
62
+ rescue Errno::EACCES
63
+ raise Vagrant::Errors::SharedFolderCreateFailed,
64
+ :path => hostpath.to_s
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ def create_metadata
71
+ @env[:ui].info I18n.t("vagrant.actions.vm.share_folders.creating")
72
+
73
+ folders = []
74
+ shared_folders.each do |id, data|
75
+ hostpath = File.expand_path(data[:hostpath], @env[:root_path])
76
+ hostpath = Vagrant::Util::Platform.cygwin_windows_path(hostpath)
77
+
78
+ folders << {
79
+ :name => id,
80
+ :hostpath => hostpath,
81
+ :transient => data[:transient]
82
+ }
83
+ end
84
+
85
+ @env[:machine].provider.driver.share_folders(folders)
86
+ end
87
+
88
+ # TODO: Fix this, doesn't execute at the correct time
89
+ def mount_shared_folders
90
+ @env[:ui].info I18n.t("vagrant.actions.vm.share_folders.mounting")
91
+
92
+ # short guestpaths first, so we don't step on ourselves
93
+ folders = shared_folders.sort_by do |id, data|
94
+ if data[:guestpath]
95
+ data[:guestpath].length
96
+ else
97
+ # A long enough path to just do this at the end.
98
+ 10000
99
+ end
100
+ end
101
+
102
+ # Go through each folder and mount
103
+ folders.each do |id, data|
104
+ if data[:guestpath]
105
+ # Guest path specified, so mount the folder to specified point
106
+ @env[:ui].info(I18n.t("vagrant.actions.vm.share_folders.mounting_entry",
107
+ :guest_path => data[:guestpath]))
108
+
109
+ # Symlink to mounted folder to guest path
110
+ @env[:machine].provider.driver.symlink(id, data[:guestpath])
111
+ else
112
+ # If no guest path is specified, then automounting is disabled
113
+ @env[:ui].info(I18n.t("vagrant.actions.vm.share_folders.nomount_entry",
114
+ :host_path => data[:hostpath]))
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,20 @@
1
+ module VagrantPlugins
2
+ module Parallels
3
+ module Action
4
+ class Suspend
5
+ def initialize(app, env)
6
+ @app = app
7
+ end
8
+
9
+ def call(env)
10
+ if env[:machine].provider.state.id == :running
11
+ env[:ui].info I18n.t("vagrant.actions.vm.suspend.suspending")
12
+ env[:machine].provider.driver.suspend
13
+ end
14
+
15
+ @app.call(env)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,24 @@
1
+ module VagrantPlugins
2
+ module Parallels
3
+ module Action
4
+ class UnregisterTemplate
5
+ def initialize(app, env)
6
+ @app = app
7
+ end
8
+
9
+ def call(env)
10
+ vm_name = Pathname.glob(
11
+ env[:machine].box.directory.join('*.pvm')
12
+ ).first.basename.to_s[0...-4]
13
+
14
+ if env[:machine].provider.driver.registered?(vm_name)
15
+ env[:machine].provider.driver.unregister(vm_name)
16
+ end
17
+ # Call the next if we have one (but we shouldn't, since this
18
+ # middleware is built to run with the Call-type middlewares)
19
+ @app.call(env)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,281 @@
1
+ require 'log4r'
2
+ require 'json'
3
+
4
+ require 'vagrant/util/busy'
5
+ require 'vagrant/util/platform'
6
+ require 'vagrant/util/retryable'
7
+ require 'vagrant/util/subprocess'
8
+
9
+ module VagrantPlugins
10
+ module Parallels
11
+ module Driver
12
+ # Base class for all Parallels drivers.
13
+ #
14
+ # This class provides useful tools for things such as executing
15
+ # PrlCtl and handling SIGINTs and so on.
16
+ class PrlCtl
17
+ # Include this so we can use `Subprocess` more easily.
18
+ include Vagrant::Util::Retryable
19
+
20
+ attr_reader :uuid
21
+
22
+ def initialize(uuid)
23
+ @logger = Log4r::Logger.new("vagrant::provider::parallels::prlctl")
24
+
25
+ # This flag is used to keep track of interrupted state (SIGINT)
26
+ @interrupted = false
27
+
28
+ # Store machine id
29
+ @uuid = uuid
30
+
31
+ # Set the path to prlctl
32
+ @manager_path = "prlctl"
33
+
34
+ @logger.info("Parallels path: #{@manager_path}")
35
+ end
36
+
37
+ # Returns the current state of this VM.
38
+ #
39
+ # @return [Symbol]
40
+ def read_state
41
+ read_settings(@uuid).fetch('State', 'inaccessible').to_sym
42
+ end
43
+
44
+ # Returns a hash of all UUIDs of virtual machines currently
45
+ # known by Parallels. Hash keys is VM names
46
+ #
47
+ # @return [Hash]
48
+ def read_vms
49
+ list = {}
50
+ json({}) { execute('list', '--all', '--json', retryable: true) }.each do |item|
51
+ list[item.fetch('name')] = item.fetch('uuid')
52
+ end
53
+
54
+ list
55
+ end
56
+
57
+ # Returns a hash of all UUIDs of VM templates currently
58
+ # known by Parallels. Hash keys is template names
59
+ #
60
+ # @return [Hash]
61
+ def read_templates
62
+ list = {}
63
+ json({}) { execute('list', '--template', '--json', retryable: true) }.each do |item|
64
+ list[item.fetch('name')] = item.fetch('uuid')
65
+ end
66
+
67
+ list
68
+ end
69
+
70
+ def read_mac_address
71
+ read_settings.fetch('Hardware', {}).fetch('net0', {}).fetch('mac', nil)
72
+ end
73
+
74
+ # Verifies that the driver is ready to accept work.
75
+ #
76
+ # This should raise a VagrantError if things are not ready.
77
+ def verify!
78
+ execute('--version')
79
+ end
80
+
81
+ def clear_shared_folders
82
+ read_settings.fetch("Host Shared Folders", {}).keys.drop(1).each do |folder|
83
+ execute("set", @uuid, "--shf-host-del", folder)
84
+ end
85
+ end
86
+
87
+ def import(template_name, vm_name)
88
+ last = 0
89
+ execute("clone", template_name, '--name', vm_name) do |type, data|
90
+ lines = data.split("\r")
91
+ # The progress of the import will be in the last line. Do a greedy
92
+ # regular expression to find what we're looking for.
93
+ if lines.last =~ /.+?(\d{,3})%/
94
+ current = $1.to_i
95
+ if current > last
96
+ last = current
97
+ yield current if block_given?
98
+ end
99
+ end
100
+ end
101
+ @uuid = read_settings(vm_name).fetch('ID', vm_name)
102
+ end
103
+
104
+ def delete_adapters
105
+ read_settings.fetch('Hardware').each do |k, _|
106
+ if k != 'net0' and k.start_with? 'net'
107
+ execute('set', @uuid, '--device-del', k)
108
+ end
109
+ end
110
+ end
111
+
112
+ def resume
113
+ execute('resume', @uuid)
114
+ end
115
+
116
+ def suspend
117
+ execute('suspend', @uuid)
118
+ end
119
+
120
+ def start
121
+ execute('start', @uuid)
122
+ end
123
+
124
+ def halt(force=false)
125
+ args = ['stop', @uuid]
126
+ args << '--kill' if force
127
+ execute(*args)
128
+ end
129
+
130
+ def delete
131
+ execute('delete', @uuid)
132
+ end
133
+
134
+ def register(pvm_file)
135
+ execute("register", pvm_file)
136
+ end
137
+
138
+ def unregister(uuid)
139
+ execute("unregister", uuid)
140
+ end
141
+
142
+ def registered?(name)
143
+ read_templates.has_key?(name) || read_vms.has_key?(name)
144
+ end
145
+
146
+ def set_mac_address(mac)
147
+ execute('set', @uuid, '--device-set', 'net0', '--type', 'shared', '--mac', mac)
148
+ end
149
+
150
+ def ssh_port(expected_port)
151
+ 22
152
+ end
153
+
154
+ def read_guest_tools_version
155
+ read_settings.fetch('GuestTools', {}).fetch('version', nil)
156
+ end
157
+
158
+ def share_folders(folders)
159
+ folders.each do |folder|
160
+ # Add the shared folder
161
+ execute('set', @uuid, '--shf-host-add', folder[:name], '--path', folder[:hostpath])
162
+ end
163
+ end
164
+
165
+ def symlink(id, path)
166
+ guest_execute('ln', '-sf', "/media/psf/#{id}", path)
167
+ end
168
+
169
+ def execute_command(command)
170
+ raw(*command)
171
+ end
172
+
173
+ def ready?
174
+ !!guest_execute('uname') rescue false
175
+ end
176
+
177
+ def ip
178
+ mac_addr = read_mac_address.downcase
179
+ File.foreach("/Library/Preferences/Parallels/parallels_dhcp_leases") do |line|
180
+ if line.include? mac_addr
181
+ ip = line[/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/]
182
+ return ip
183
+ end
184
+ end
185
+ end
186
+
187
+ private
188
+
189
+ def read_settings(uuid=nil)
190
+ uuid ||= @uuid
191
+ json({}) { execute('list', uuid, '--info', '--json', retryable: true).gsub(/^(INFO)?\[/, '').gsub(/\]$/, '') }
192
+ end
193
+
194
+ def json(default=nil)
195
+ data = yield
196
+ JSON.parse(data) rescue default
197
+ end
198
+
199
+ def guest_execute(*command)
200
+ execute('exec', @uuid, *command)
201
+ end
202
+
203
+ # Execute the given subcommand for PrlCtl and return the output.
204
+ def execute(*command, &block)
205
+ # Get the options hash if it exists
206
+ opts = {}
207
+ opts = command.pop if command.last.is_a?(Hash)
208
+
209
+ tries = 0
210
+ tries = 3 if opts[:retryable]
211
+
212
+ # Variable to store our execution result
213
+ r = nil
214
+
215
+ # If there is an error with PrlCtl, this gets set to true
216
+ errored = false
217
+
218
+ retryable(on: VagrantPlugins::Parallels::Errors::ParallelsError, tries: tries, sleep: 1) do
219
+ # Execute the command
220
+ r = raw(@manager_path, *command, &block)
221
+
222
+ # If the command was a failure, then raise an exception that is
223
+ # nicely handled by Vagrant.
224
+ if r.exit_code != 0
225
+ if @interrupted
226
+ @logger.info("Exit code != 0, but interrupted. Ignoring.")
227
+ elsif r.exit_code == 126
228
+ # This exit code happens if PrlCtl is on the PATH,
229
+ # but another executable it tries to execute is missing.
230
+ # This is usually indicative of a corrupted Parallels install.
231
+ raise VagrantPlugins::Parallels::Errors::ParallelsErrorNotFoundError
232
+ else
233
+ errored = true
234
+ end
235
+ else
236
+ if r.stderr =~ /failed to open \/dev\/prlctl/i
237
+ # This catches an error message that only shows when kernel
238
+ # drivers aren't properly installed.
239
+ @logger.error("Error message about unable to open prlctl")
240
+ raise VagrantPlugins::Parallels::Errors::ParallelsErrorKernelModuleNotLoaded
241
+ end
242
+
243
+ if r.stderr =~ /Unable to perform/i
244
+ @logger.info("VM not running for command to work.")
245
+ errored = true
246
+ elsif r.stderr =~ /Invalid usage/i
247
+ @logger.info("PrlCtl error text found, assuming error.")
248
+ errored = true
249
+ end
250
+ end
251
+ end
252
+
253
+ # If there was an error running PrlCtl, show the error and the
254
+ # output.
255
+ if errored
256
+ raise VagrantPlugins::Parallels::Errors::ParallelsError,
257
+ command: command.inspect,
258
+ stderr: r.stderr
259
+ end
260
+
261
+ r.stdout
262
+ end
263
+
264
+ # Executes a command and returns the raw result object.
265
+ def raw(cli, *command, &block)
266
+ int_callback = lambda do
267
+ @interrupted = true
268
+ @logger.info("Interrupted.")
269
+ end
270
+
271
+ # Append in the options for subprocess
272
+ command << { notify: [:stdout, :stderr] }
273
+
274
+ Vagrant::Util::Busy.busy(int_callback) do
275
+ Vagrant::Util::Subprocess.execute(cli, *command, &block)
276
+ end
277
+ end
278
+ end
279
+ end
280
+ end
281
+ end