vagrant-parallels 0.0.1.dev

Sign up to get free protection for your applications and to get access to all the features.
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