vagrant_utm 0.0.1

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 (96) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +11 -0
  4. data/CHANGELOG.md +5 -0
  5. data/CODE_OF_CONDUCT.md +132 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +59 -0
  8. data/Rakefile +12 -0
  9. data/docs/.gitignore +15 -0
  10. data/docs/Gemfile +10 -0
  11. data/docs/Gemfile.lock +276 -0
  12. data/docs/README.md +174 -0
  13. data/docs/_config.yml +76 -0
  14. data/docs/_includes/footer_custom.html +3 -0
  15. data/docs/_sass/gallery.scss +64 -0
  16. data/docs/_virtual_machines/archlinux-arm.md +13 -0
  17. data/docs/assets/images/favicon.ico +0 -0
  18. data/docs/assets/images/logo.png +0 -0
  19. data/docs/assets/images/screens/archlinux-logo.png +0 -0
  20. data/docs/assets/images/screens/debian-11-xfce-arm64.png +0 -0
  21. data/docs/boxes/creating_utm_box.md +70 -0
  22. data/docs/boxes/index.md +6 -0
  23. data/docs/boxes/utm_box_gallery.md +117 -0
  24. data/docs/commands.md +156 -0
  25. data/docs/configuration.md +51 -0
  26. data/docs/features/index.md +5 -0
  27. data/docs/features/synced_folders.md +28 -0
  28. data/docs/index.md +103 -0
  29. data/docs/internals/actions.md +20 -0
  30. data/docs/internals/index.md +5 -0
  31. data/docs/internals/status.md +25 -0
  32. data/docs/internals/utm_api.md +31 -0
  33. data/docs/known_issues.md +24 -0
  34. data/lib/vagrant_utm/action/boot.rb +22 -0
  35. data/lib/vagrant_utm/action/boot_disposable.rb +22 -0
  36. data/lib/vagrant_utm/action/check_accessible.rb +33 -0
  37. data/lib/vagrant_utm/action/check_created.rb +24 -0
  38. data/lib/vagrant_utm/action/check_guest_additions.rb +37 -0
  39. data/lib/vagrant_utm/action/check_qemu_img.rb +21 -0
  40. data/lib/vagrant_utm/action/check_running.rb +25 -0
  41. data/lib/vagrant_utm/action/check_utm.rb +30 -0
  42. data/lib/vagrant_utm/action/clear_forwarded_ports.rb +26 -0
  43. data/lib/vagrant_utm/action/created.rb +26 -0
  44. data/lib/vagrant_utm/action/customize.rb +50 -0
  45. data/lib/vagrant_utm/action/destroy.rb +25 -0
  46. data/lib/vagrant_utm/action/download_confirm.rb +19 -0
  47. data/lib/vagrant_utm/action/export.rb +22 -0
  48. data/lib/vagrant_utm/action/forced_halt.rb +28 -0
  49. data/lib/vagrant_utm/action/forward_ports.rb +90 -0
  50. data/lib/vagrant_utm/action/import.rb +63 -0
  51. data/lib/vagrant_utm/action/is_paused.rb +26 -0
  52. data/lib/vagrant_utm/action/is_running.rb +26 -0
  53. data/lib/vagrant_utm/action/is_stopped.rb +26 -0
  54. data/lib/vagrant_utm/action/message_already_running.rb +22 -0
  55. data/lib/vagrant_utm/action/message_not_created.rb +22 -0
  56. data/lib/vagrant_utm/action/message_not_running.rb +22 -0
  57. data/lib/vagrant_utm/action/message_not_stopped.rb +22 -0
  58. data/lib/vagrant_utm/action/message_will_not_create.rb +23 -0
  59. data/lib/vagrant_utm/action/message_will_not_destroy.rb +23 -0
  60. data/lib/vagrant_utm/action/prepare_forwarded_port_collision_params.rb +43 -0
  61. data/lib/vagrant_utm/action/resume.rb +24 -0
  62. data/lib/vagrant_utm/action/set_id.rb +20 -0
  63. data/lib/vagrant_utm/action/set_name.rb +62 -0
  64. data/lib/vagrant_utm/action/snapshot_delete.rb +34 -0
  65. data/lib/vagrant_utm/action/snapshot_restore.rb +28 -0
  66. data/lib/vagrant_utm/action/snapshot_save.rb +34 -0
  67. data/lib/vagrant_utm/action/suspend.rb +23 -0
  68. data/lib/vagrant_utm/action/wait_for_running.rb +28 -0
  69. data/lib/vagrant_utm/action.rb +413 -0
  70. data/lib/vagrant_utm/cap.rb +40 -0
  71. data/lib/vagrant_utm/config.rb +141 -0
  72. data/lib/vagrant_utm/disposable.rb +16 -0
  73. data/lib/vagrant_utm/driver/base.rb +358 -0
  74. data/lib/vagrant_utm/driver/meta.rb +132 -0
  75. data/lib/vagrant_utm/driver/version_4_5.rb +307 -0
  76. data/lib/vagrant_utm/errors.rb +77 -0
  77. data/lib/vagrant_utm/model/forwarded_port.rb +75 -0
  78. data/lib/vagrant_utm/model/list_result.rb +77 -0
  79. data/lib/vagrant_utm/plugin.rb +65 -0
  80. data/lib/vagrant_utm/provider.rb +139 -0
  81. data/lib/vagrant_utm/scripts/add_port_forwards.applescript +72 -0
  82. data/lib/vagrant_utm/scripts/clear_port_forwards.applescript +56 -0
  83. data/lib/vagrant_utm/scripts/customize_vm.applescript +59 -0
  84. data/lib/vagrant_utm/scripts/downloadVM.sh +1 -0
  85. data/lib/vagrant_utm/scripts/list_vm.js +32 -0
  86. data/lib/vagrant_utm/scripts/open_with_utm.js +30 -0
  87. data/lib/vagrant_utm/scripts/read_forwarded_ports.applescript +27 -0
  88. data/lib/vagrant_utm/scripts/read_guest_ip.applescript +9 -0
  89. data/lib/vagrant_utm/scripts/read_network_interfaces.applescript +12 -0
  90. data/lib/vagrant_utm/util/compile_forwarded_ports.rb +43 -0
  91. data/lib/vagrant_utm/version.rb +9 -0
  92. data/lib/vagrant_utm.rb +40 -0
  93. data/locales/en.yml +154 -0
  94. data/sig/vagrant_utm.rbs +4 -0
  95. data/vagrantfile_examples/Vagrantfile +27 -0
  96. metadata +140 -0
@@ -0,0 +1,358 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "log4r"
4
+ require "pathname"
5
+ require "vagrant/util/busy"
6
+ require "vagrant/util/subprocess"
7
+ require "vagrant/util/which"
8
+ require_relative "../model/list_result"
9
+
10
+ module VagrantPlugins
11
+ module Utm
12
+ module Driver
13
+ # Executes commands on the host machine through the AppleScript bridge interface
14
+ # paired with a command line interface.
15
+ class Base # rubocop:disable Metrics/ClassLength
16
+ # Include this so we can use `Subprocess` more easily.
17
+ include Vagrant::Util::Retryable
18
+
19
+ def initialize
20
+ @logger = Log4r::Logger.new("vagrant::provider::utm::base")
21
+
22
+ # This flag is used to keep track of interrupted state (SIGINT)
23
+ @interrupted = false
24
+ # The path to the scripts directory.
25
+ @script_path = Pathname.new(File.expand_path("../scripts", __dir__))
26
+
27
+ # Set 'utmctl' path
28
+ @utmctl_path = Vagrant::Util::Which.which("utmctl")
29
+
30
+ # if not found, fall back to /usr/local/bin/utmctl
31
+ @utmctl_path ||= "/Applications/UTM.app/Contents/MacOS/utmctl"
32
+ @logger.info("utmctl path: #{@utmctl_path}")
33
+ end
34
+
35
+ # Clears the forwarded ports that have been set on the virtual machine.
36
+ def clear_forwarded_ports; end
37
+
38
+ # Checks if the qemu-guest-agent is installed and running in this VM.
39
+ # @return [Boolean]
40
+ def check_qemu_guest_agent; end
41
+
42
+ # Forwards a set of ports for a VM.
43
+ #
44
+ # This will not affect any previously set forwarded ports,
45
+ # so be sure to delete those if you need to.
46
+ #
47
+ # The format of each port hash should be the following:
48
+ #
49
+ # {
50
+ # name: "foo",
51
+ # hostport: 8500,
52
+ # guestport: 80,
53
+ # adapter: 1,
54
+ # protocol: "tcp"
55
+ # }
56
+ #
57
+ # Note that "adapter" and "protocol" are optional and will default
58
+ # to 1 and "tcp" respectively.
59
+ #
60
+ # @param [Array<Hash>] ports An array of ports to set. See documentation
61
+ # for more information on the format.
62
+ def forward_ports(ports); end
63
+
64
+ # Check if the VM with the given UUID (Name) exists.
65
+ def vm_exists?(uuid); end
66
+
67
+ # Returns a list of forwarded ports for a VM.
68
+ #
69
+ # @param [String] uuid UUID of the VM to read from, or `nil` if this
70
+ # VM.
71
+
72
+ # @return [Array<Array>] An array of arrays, each of which contains
73
+ # [nic, name(hostport), hostport, guestport]
74
+ def read_forwarded_ports(uuid = nil); end
75
+
76
+ # Returns the current state of this VM.
77
+ #
78
+ # @return [Symbol]
79
+ def read_state; end
80
+
81
+ # Returns a list of all forwarded ports in use by active
82
+ # virtual machines.
83
+ #
84
+ # @param [Boolean] active_only If true, only VMs that are running will
85
+ # be checked.
86
+ # @return [Array]
87
+ def read_used_ports(active_only: true); end
88
+
89
+ # Returns the IP address of the guest machine.
90
+ #
91
+ # @return [String] The IP address of the guest machine.
92
+ def read_guest_ip; end
93
+
94
+ # Returns a list of network interfaces of the VM.
95
+ #
96
+ # @return [Hash]
97
+ def read_network_interfaces; end
98
+
99
+ # Execute the 'list' command and returns the list of machines.
100
+ # @return [ListResult] The list of machines.
101
+ def list; end
102
+
103
+ # Execute the 'utm://downloadVM?url='
104
+ # See https://docs.getutm.app/advanced/remote-control/
105
+ # @param utm_file_url [String] The url to the UTM file.
106
+ # @return [uuid] The UUID of the imported machine.
107
+ def import(utm_file_url); end
108
+
109
+ # Sets the name of the virtual machine.
110
+ # @param name [String] The new name of the machine.
111
+ # @return [void]
112
+ def set_name(name); end # rubocop:disable Naming/AccessorMethodName
113
+
114
+ # Reads the SSH port of this VM.
115
+ #
116
+ # @param [Integer] expected Expected guest port of SSH.
117
+ def ssh_port(expected); end
118
+
119
+ # Starts the virtual machine referenced by this driver.
120
+ # @return [void]
121
+ def start; end
122
+
123
+ # Starts the virtual machine in disposable mode.
124
+ # @return [void]
125
+ def start_disposable; end
126
+
127
+ # Deletes the virtual machine references by this driver.
128
+ # @return [void]
129
+ def delete; end
130
+
131
+ # Return UUID of the last VM in the list.
132
+ # @return [uuid] The UUID of the VM.
133
+ def last_uuid; end
134
+
135
+ # Halts the virtual machine (pulls the plug).
136
+ def halt; end
137
+
138
+ # Suspend the virtual machine.
139
+ def suspend; end
140
+
141
+ # Verifies that the driver is ready to accept work.
142
+ #
143
+ # This should raise a VagrantError if things are not ready.
144
+ def verify!; end
145
+
146
+ # Execute a script using the OSA interface.
147
+ def execute_osa_script(command); end
148
+
149
+ # Execute a shell command and return the output.
150
+ def execute_shell(*command, &block) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
151
+ # Get the options hash if it exists
152
+ opts = {}
153
+ opts = command.pop if command.last.is_a?(Hash)
154
+
155
+ tries = 0
156
+ tries = 3 if opts[:retryable]
157
+
158
+ # Variable to store our execution result
159
+ r = nil
160
+
161
+ retryable(on: VagrantPlugins::Utm::Errors::CommandError, tries: tries, sleep: 1) do
162
+ # If there is an error with VBoxManage, this gets set to true
163
+ errored = false
164
+
165
+ # Execute the command
166
+ r = raw_shell(*command, &block)
167
+
168
+ # If the command was a failure, then raise an exception that is
169
+ # nicely handled by Vagrant.
170
+ if r.exit_code != 0
171
+ if @interrupted
172
+ @logger.info("Exit code != 0, but interrupted. Ignoring.")
173
+ else
174
+ errored = true
175
+ end
176
+ end
177
+
178
+ if errored
179
+ raise VagrantPlugins::Utm::Errors::CommandError,
180
+ command: command.inspect,
181
+ stderr: r.stderr,
182
+ stdout: r.stdout
183
+ end
184
+ end
185
+
186
+ # Return the output, making sure to replace any Windows-style
187
+ # newlines with Unix-style.
188
+ # AppleScript logs are always in stderr, so we return that
189
+ # if there is any output.
190
+ if r.stdout && !r.stdout.empty?
191
+ r.stdout.gsub("\r\n", "\n")
192
+ elsif r.stderr && !r.stderr.empty?
193
+ r.stderr.gsub("\r\n", "\n")
194
+ else
195
+ ""
196
+ end
197
+ end
198
+
199
+ # Executes a command and returns the raw result object.
200
+ def raw_shell(*command, &block)
201
+ int_callback = lambda do
202
+ @interrupted = true
203
+
204
+ # We have to execute this in a thread due to trap contexts
205
+ # and locks.
206
+ Thread.new { @logger.info("Interrupted.") }.join
207
+ end
208
+
209
+ # Append in the options for subprocess
210
+ # NOTE: We include the LANG env var set to C to prevent command output
211
+ # from being localized
212
+ command << { notify: %i[stdout stderr], env: env_lang }
213
+
214
+ Vagrant::Util::Busy.busy(int_callback) do
215
+ Vagrant::Util::Subprocess.execute(*command, &block)
216
+ end
217
+ rescue Vagrant::Util::Subprocess::LaunchError => e
218
+ raise Vagrant::Errors::UtmLaunchError,
219
+ message: e.to_s
220
+ end
221
+
222
+ # Execute the given subcommand for utmctl and return the output.
223
+ # Copied from https://github.com/hashicorp/vagrant/blob/main/plugins/providers/virtualbox/driver/base.rb.
224
+ # @param [String] subcommand The subcommand to execute.
225
+ # @return [String] The output of the command.
226
+ def execute(*command, &block) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
227
+ # Get the options hash if it exists
228
+ opts = {}
229
+ opts = command.pop if command.last.is_a?(Hash)
230
+
231
+ tries = 0
232
+ tries = 3 if opts[:retryable]
233
+
234
+ # Variable to store our execution result
235
+ r = nil
236
+
237
+ retryable(on: Errors::UtmctlError, tries: tries, sleep: 1) do # rubocop:disable Metrics/BlockLength
238
+ # if there is an error with utmctl, this gets set to true
239
+ errored = false
240
+
241
+ # Execute the command
242
+ r = raw(*command, &block)
243
+
244
+ # If the command was a failure, then raise an exception that is
245
+ # nicely handled by Vagrant.
246
+ if r.exit_code != 0
247
+ if @interrupted
248
+ @logger.info("Exit code != 0, but interrupted. Ignoring.")
249
+ elsif r.exit_code == 126
250
+ # To be consistent with VBoxManage
251
+ raise Errors::UtmctlNotFoundError
252
+ else
253
+ errored = true
254
+ end
255
+ else
256
+ # if utmctl fails but doesn't exit with an error code
257
+ # Handle those cases here
258
+
259
+ if r.stderr =~ /Error/
260
+ @logger.info("Error found, assuming error.")
261
+ errored = true
262
+ end
263
+
264
+ if r.stderr =~ /OSStatus error/
265
+ @logger.info("OSStatus error found, assuming error.")
266
+ errored = true
267
+
268
+ end
269
+ end
270
+
271
+ # If there was an error running utmctl, show the error and the output
272
+ if errored
273
+ raise Errors::UtmctlError,
274
+ command: command.inspect,
275
+ stderr: r.stderr,
276
+ stdout: r.stdout
277
+ end
278
+ end
279
+
280
+ # Return the output, making sure to replace any Windows-style
281
+ # newlines with Unix-style.
282
+ r.stdout.gsub("\r\n", "\n")
283
+ end
284
+
285
+ # Executes a utmctl command and returns the raw result object.
286
+ def raw(*command, &block)
287
+ int_callback = lambda do
288
+ @interrupted = true
289
+
290
+ # We have to execute this in a thread due to trap contexts
291
+ # and locks.
292
+ Thread.new { @logger.info("Interrupted.") }.join
293
+ end
294
+
295
+ # Append in the options for subprocess
296
+ # NOTE: We include the LANG env var set to C to prevent command output
297
+ # from being localized
298
+ command << { notify: %i[stdout stderr], env: env_lang }
299
+
300
+ Vagrant::Util::Busy.busy(int_callback) do
301
+ Vagrant::Util::Subprocess.execute(@utmctl_path, *command, &block)
302
+ end
303
+ rescue Vagrant::Util::Subprocess::LaunchError => e
304
+ raise Vagrant::Errors::UtmLaunchError,
305
+ message: e.to_s
306
+ end
307
+
308
+ private
309
+
310
+ # List of LANG values to attempt to use
311
+ LANG_VARIATIONS = %w[C.UTF-8 C.utf8 en_US.UTF-8 en_US.utf8 C POSIX].map(&:freeze).freeze
312
+
313
+ # By default set the LANG to C. If the host has the locale command
314
+ # available, check installed locales and verify C is included (or
315
+ # use C variant if available).
316
+ def env_lang # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
317
+ # If already set, just return immediately
318
+ return @env_lang if @env_lang
319
+
320
+ # Default the LANG to C
321
+ @env_lang = { LANG: "C" }
322
+
323
+ # If the locale command is not available, return default
324
+ return @env_lang unless Vagrant::Util::Which.which("locale")
325
+
326
+ return @env_lang = @@env_lang if defined?(@@env_lang)
327
+
328
+ @logger.debug("validating LANG value for UTM cli commands")
329
+ # Get list of available locales on the system
330
+ result = Vagrant::Util::Subprocess.execute("locale", "-a")
331
+
332
+ # If the command results in an error, just log the error
333
+ # and return the default value
334
+ if result.exit_code != 0
335
+ @logger.warn("locale command failed (exit code: #{result.exit_code}): #{result.stderr}")
336
+ return @env_lang
337
+ end
338
+ available = result.stdout.lines.map(&:chomp).find_all do |l|
339
+ l == "C" || l == "POSIX" || l.start_with?("C.") || l.start_with?("en_US.")
340
+ end
341
+ @logger.debug("list of available C locales: #{available.inspect}")
342
+
343
+ # Attempt to find a valid LANG from locale list
344
+ lang = LANG_VARIATIONS.detect { |l| available.include?(l) }
345
+
346
+ if lang
347
+ @logger.debug("valid variation found for LANG value: #{lang}")
348
+ @env_lang[:LANG] = lang
349
+ @@env_lang = @env_lang # rubocop:disable Style/ClassVars
350
+ end
351
+
352
+ @logger.debug("LANG value set: #{@env_lang[:LANG].inspect}")
353
+ @env_lang
354
+ end
355
+ end
356
+ end
357
+ end
358
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ require "log4r"
6
+
7
+ require "vagrant/util/retryable"
8
+
9
+ require File.expand_path("base", __dir__)
10
+
11
+ module VagrantPlugins
12
+ module Utm
13
+ module Driver
14
+ # This driver uses the AppleScript bridge interface to interact with UTM.
15
+ class Meta < Base
16
+ # This is raised if the VM is not found when initializing a driver
17
+ # with a UUID.
18
+ class VMNotFound < StandardError; end
19
+
20
+ # We use forwardable to do all our driver forwarding
21
+ extend Forwardable
22
+
23
+ # We cache the read UTM version here once we have one,
24
+ # since during the execution of Vagrant, it likely doesn't change.
25
+ @version = nil
26
+ @@version_lock = Mutex.new # rubocop:disable Style/ClassVars
27
+
28
+ # The UUID of the virtual machine we represent (Name in UTM).
29
+ attr_reader :uuid
30
+
31
+ # The version of UTM that is running.
32
+ attr_reader :version
33
+
34
+ include Vagrant::Util::Retryable
35
+
36
+ def initialize(uuid = nil) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/AbcSize,Metrics/PerceivedComplexity
37
+ # Setup the base
38
+ super()
39
+
40
+ @logger = Log4r::Logger.new("vagrant::provider::utm::meta")
41
+ @uuid = uuid
42
+
43
+ @@version_lock.synchronize do
44
+ unless @version
45
+ begin
46
+ @version = read_version
47
+ rescue Errors::CommandError
48
+ # This means that UTM was not found, so we raise this
49
+ # error here.
50
+ raise Errors::UtmNotDetected
51
+ end
52
+ end
53
+ end
54
+
55
+ # Instantiate the proper version driver for UTM
56
+ @logger.debug("Finding driver for UTM version: #{@version}")
57
+ driver_map = {
58
+ "4.5" => Version_4_5
59
+ }
60
+
61
+ # UTM 4.5.0 just doesn't work with Vagrant (https://github.com/utmapp/UTM/issues/5963),
62
+ # so show error
63
+ raise Errors::UtmInvalidVersion if @version.start_with?("4.5.0")
64
+
65
+ driver_klass = nil
66
+ driver_map.each do |key, klass|
67
+ if @version.start_with?(key)
68
+ driver_klass = klass
69
+ break
70
+ end
71
+ end
72
+
73
+ unless driver_klass
74
+ supported_versions = driver_map.keys.sort.join(", ")
75
+ raise Errors::UtmInvalidVersion,
76
+ supported_versions: supported_versions
77
+ end
78
+
79
+ @logger.info("Using UTM driver: #{driver_klass}")
80
+ @driver = driver_klass.new(@uuid)
81
+
82
+ return unless @uuid
83
+ # Verify the VM exists, and if it doesn't, then don't worry
84
+ # about it (mark the UUID as nil)
85
+ raise VMNotFound unless @driver.vm_exists?(@uuid)
86
+ end
87
+
88
+ def_delegators :@driver,
89
+ :check_qemu_guest_agent,
90
+ :clear_forwarded_ports,
91
+ :create_snapshot,
92
+ :delete,
93
+ :delete_snapshot,
94
+ :execute_osa_script,
95
+ :forward_ports,
96
+ :halt,
97
+ :import,
98
+ :last_uuid,
99
+ :list,
100
+ :list_snapshots,
101
+ :read_forwarded_ports,
102
+ :read_guest_ip,
103
+ :read_network_interfaces,
104
+ :read_state,
105
+ :read_used_ports,
106
+ :restore_snapshot,
107
+ :set_name,
108
+ :ssh_port,
109
+ :start,
110
+ :start_disposable,
111
+ :suspend,
112
+ :vm_exists?
113
+
114
+ protected
115
+
116
+ # This returns the version of UTM that is running.
117
+ #
118
+ # @return [String]
119
+ def read_version
120
+ # The version string is in the format "4.5.3"
121
+ # Error: Can’t get application "UTM"
122
+ # Success: "4.5.0"
123
+ cmd = ["osascript", "-e", 'tell application "System Events" to return version of application "UTM"']
124
+ output = execute_shell(*cmd)
125
+ return output.strip unless output =~ /get application/
126
+
127
+ raise Errors::UtmNotDetected
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end