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,307 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "log4r"
4
+ require "etc"
5
+ require "vagrant/util/platform"
6
+
7
+ require File.expand_path("base", __dir__)
8
+
9
+ module VagrantPlugins
10
+ module Utm
11
+ module Driver
12
+ # Driver for UTM 4.5.x
13
+ class Version_4_5 < Base # rubocop:disable Naming/ClassAndModuleCamelCase,Metrics/ClassLength
14
+ def initialize(uuid)
15
+ super()
16
+
17
+ @logger = Log4r::Logger.new("vagrant::provider::utm_4_5")
18
+ @uuid = uuid
19
+ end
20
+
21
+ def clear_forwarded_ports
22
+ args = []
23
+ read_forwarded_ports(@uuid).each do |nic, name, _, _|
24
+ args.concat(["--index", nic.to_s, name])
25
+ end
26
+
27
+ command = ["clear_port_forwards.applescript", @uuid] + args
28
+ execute_osa_script(command) unless args.empty?
29
+ end
30
+
31
+ def check_qemu_guest_agent
32
+ # Check if the qemu-guest-agent is installed and running
33
+ # Ideally do: utmctl exec id --cmd systemctl is-active qemu-guest-agent
34
+ # But this is not returning anything, so we just do any utmctl exec command
35
+ # Here we check if the user is root
36
+ output = execute("exec", @uuid, "--cmd", "whoami")
37
+ # check if output contains 'root'
38
+ output.include?("root")
39
+ end
40
+
41
+ def create_snapshot(machine_id, snapshot_name)
42
+ list_result = list
43
+ machine_name = list_result.find(uuid: machine_id).name
44
+ machine_file = get_vm_file(machine_name)
45
+ execute_shell("qemu-img", "snapshot", "-c", snapshot_name, machine_file)
46
+ end
47
+
48
+ def delete_snapshot(machine_id, snapshot_name)
49
+ list_result = list
50
+ machine_name = list_result.find(uuid: machine_id).name
51
+ machine_file = get_vm_file(machine_name)
52
+ execute_shell("qemu-img", "snapshot", "-d", snapshot_name, machine_file)
53
+ end
54
+
55
+ def list_snapshots(machine_id) # rubocop:disable Metrics/AbcSize
56
+ list_result = list
57
+ machine_name = list_result.find(uuid: machine_id).name
58
+ machine_file = get_vm_file(machine_name)
59
+ output = execute_shell("qemu-img", "snapshot", "-l", machine_file)
60
+
61
+ return [] if output.nil? || output.strip.empty?
62
+
63
+ @logger.debug("list_snapshots_here: #{output}")
64
+
65
+ result = []
66
+ output.split("\n").map do |line|
67
+ result << ::Regexp.last_match(1).to_s if line =~ /^\d+\s+(\w+)/
68
+ end
69
+
70
+ result.sort
71
+ end
72
+
73
+ def restore_snapshot(machine_id, snapshot_name)
74
+ list_result = list
75
+ machine_name = list_result.find(uuid: machine_id).name
76
+ machine_file = get_vm_file(machine_name)
77
+ execute_shell("qemu-img", "snapshot", "-a", snapshot_name, machine_file)
78
+ end
79
+
80
+ def forward_ports(ports) # rubocop:disable Metrics/CyclomaticComplexity
81
+ args = []
82
+ ports.each do |options|
83
+ # Convert to UTM protcol enum
84
+ protocol_code = case options[:protocol]
85
+ when "tcp"
86
+ "TcPp"
87
+ when "udp"
88
+ "UdPp"
89
+ else
90
+ raise Errors::ForwardedPortInvalidProtocol
91
+ end
92
+
93
+ pf_builder = [
94
+ # options[:name], # Name is not supported in UTM
95
+ protocol_code || "TcPp", # Default to TCP
96
+ options[:guestip] || "",
97
+ options[:guestport],
98
+ options[:hostip] || "",
99
+ options[:hostport]
100
+ ]
101
+
102
+ args.concat(["--index", options[:adapter].to_s,
103
+ pf_builder.join(",")])
104
+ end
105
+
106
+ command = ["add_port_forwards.applescript", @uuid] + args
107
+ execute_osa_script(command) unless args.empty?
108
+ end
109
+
110
+ def get_vm_file(vm_name)
111
+ # Get the current username
112
+ username = ENV["USER"] || Etc.getlogin
113
+
114
+ data_path = "/Users/#{username}/Library/Containers/com.utmapp.UTM/Data/Documents/#{vm_name}.utm/Data"
115
+ # Define the regex for UUID pattern
116
+ pattern = /^\b[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}\b.qcow2$/
117
+
118
+ # List the files in the directory and filter by regex
119
+ matching_files = Dir.entries(data_path).select do |file|
120
+ file.match?(pattern)
121
+ end
122
+
123
+ if matching_files.length == 1
124
+ # If there is exactly one matching file, return full path to file
125
+ "#{data_path}/#{matching_files[0]}"
126
+ elsif matching_files.length > 1
127
+ # If there are multiple matching files, raise an error
128
+ raise Errors::SnapShotMultipleVMFiles, directory: data_path, files: matching_files
129
+ else
130
+ # If there are no matching files, raise an error
131
+ raise Errors::SnapShotVMFileNotFound, directory: data_path
132
+ end
133
+ end
134
+
135
+ # Check if the VM with the given UUID exists.
136
+ def vm_exists?(uuid)
137
+ list_result = list
138
+ list_result.any?(uuid)
139
+ end
140
+
141
+ def read_forwarded_ports(uuid = nil)
142
+ uuid ||= @uuid
143
+
144
+ @logger.debug("read_forward_ports: uuid=#{uuid}")
145
+
146
+ # If we care about active VMs only, then we check the state
147
+ # to verify the VM is running.
148
+ # This is taken care by the caller , read used ports
149
+ # return [] if active_only && check_state != :started
150
+
151
+ # Get the forwarded ports from emulated Network interface
152
+ # Format: [nicIndex, name, hostPort, guestPort]
153
+ # We use hostPort as the name, since UTM does not support name
154
+ # Because hostport is and should be unique
155
+ results = []
156
+ command = ["read_forwarded_ports.applescript", uuid]
157
+ info = execute_osa_script(command)
158
+ info.split("\n").each do |line|
159
+ # Parse info, Forwarding(nicIndex)(ruleIndex)="Protocol,GuestIP,GuestPort,HostIP,HostPort"
160
+ next unless (matcher = /^Forwarding\((\d+)\)\((\d+)\)="(.+?),.*?,(.+?),.*?,(.+?)"$/.match(line))
161
+
162
+ # nicIndex name( our hostPort) hostport guestport
163
+ result = [matcher[1].to_i, matcher[5], matcher[5].to_i, matcher[4].to_i]
164
+ @logger.debug(" - #{result.inspect}")
165
+ results << result
166
+ end
167
+
168
+ results
169
+ end
170
+
171
+ def read_guest_ip
172
+ command = ["read_guest_ip.applescript", @uuid]
173
+ output = execute_osa_script(command)
174
+ output.strip
175
+ end
176
+
177
+ def read_network_interfaces
178
+ nics = {}
179
+ command = ["read_network_interfaces.applescript", @uuid]
180
+ info = execute_osa_script(command)
181
+ info.split("\n").each do |line|
182
+ next unless (matcher = /^nic(\d+),(.+?)$/.match(line))
183
+
184
+ adapter = matcher[1].to_i
185
+ type = matcher[2].to_sym
186
+ nics[adapter] ||= {}
187
+ nics[adapter][:type] = type
188
+ end
189
+
190
+ nics
191
+ end
192
+
193
+ def ssh_port(expected_port)
194
+ @logger.debug("Searching for SSH port: #{expected_port.inspect}")
195
+
196
+ # Look for the forwarded port only by comparing the guest port
197
+ read_forwarded_ports.each do |_, _, hostport, guestport|
198
+ return hostport if guestport == expected_port
199
+ end
200
+
201
+ nil
202
+ end
203
+
204
+ # virtualbox plugin style
205
+ def read_state
206
+ output = execute("status", @uuid)
207
+ output.strip.to_sym
208
+ end
209
+
210
+ # We handle the active only case here
211
+ # So we can avoid calling the utmctl status command
212
+ # for each VM
213
+ def read_used_ports(active_only = true) # rubocop:disable Style/OptionalBooleanParameter
214
+ @logger.debug("read_used_ports: active_only=#{active_only}")
215
+ ports = []
216
+ list.machines.each do |machine|
217
+ # Ignore our own used ports
218
+ next if machine.uuid == @uuid
219
+ # Ignore inactive VMs if we only care about active VMs
220
+ next if active_only && machine.state != :started
221
+
222
+ read_forwarded_ports(machine.uuid).each do |_, _, hostport, _|
223
+ ports << hostport
224
+ end
225
+ end
226
+
227
+ ports
228
+ end
229
+
230
+ def set_name(name) # rubocop:disable Naming/AccessorMethodName
231
+ command = ["customize_vm.applescript", @uuid, "--name", name.to_s]
232
+ execute_osa_script(command)
233
+ end
234
+
235
+ def delete
236
+ execute("delete", @uuid)
237
+ end
238
+
239
+ def start
240
+ execute("start", @uuid)
241
+ end
242
+
243
+ def start_disposable
244
+ execute("start", @uuid, "--disposable")
245
+ end
246
+
247
+ def halt
248
+ execute("stop", @uuid)
249
+ end
250
+
251
+ def suspend
252
+ execute("suspend", @uuid)
253
+ end
254
+
255
+ def execute_osa_script(command)
256
+ script_path = @script_path.join(command[0])
257
+ cmd = ["osascript", script_path.to_s] + command[1..]
258
+ execute_shell(*cmd)
259
+ end
260
+
261
+ # Execute the 'list' command and returns the list of machines.
262
+ # @return [ListResult] The list of machines.
263
+ def list
264
+ script_path = @script_path.join("list_vm.js")
265
+ cmd = ["osascript", script_path.to_s]
266
+ result = execute_shell(*cmd)
267
+ data = JSON.parse(result)
268
+ Model::ListResult.new(data)
269
+ end
270
+
271
+ # Execute the 'utm://downloadVM?url='
272
+ # See https://docs.getutm.app/advanced/remote-control/
273
+ # @param utm_file_url [String] The url to the UTM file.
274
+ # @return [uuid] The UUID of the imported machine.
275
+ def import(utm_file_url)
276
+ script_path = @script_path.join("downloadVM.sh")
277
+ cmd = [script_path.to_s, utm_file_url]
278
+ execute_shell(*cmd)
279
+ # wait for the VM to be imported
280
+ # TODO: UTM API to give the progress of the import
281
+ # along with the UUID of the imported VM
282
+ # sleep(60)
283
+ # Get the UUID of the imported VM
284
+ # HACK: Currently we do not know the UUID of the imported VM
285
+ # So, we just get the UUID of the last VM in the list
286
+ # which is the last imported VM (unless UTM changes the order)
287
+ # TODO: Use UTM API to get the UUID of the imported VM
288
+ # last_uuid
289
+ end
290
+
291
+ # Return UUID of the last VM in the list.
292
+ # @return [uuid] The UUID of the VM.
293
+ def last_uuid
294
+ list_result = list
295
+ list_result.last.uuid
296
+ end
297
+
298
+ def verify!
299
+ # Verify proper functionality of UTM
300
+ # add any command that should be checked
301
+ # we now only check if the 'utmctl' command is available
302
+ execute("--list")
303
+ end
304
+ end
305
+ end
306
+ end
307
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VagrantPlugins
4
+ module Utm
5
+ module Errors
6
+ # Base class for all errors raised by the UTM provider.
7
+ class UtmError < Vagrant::Errors::VagrantError
8
+ error_namespace("vagrant_utm.errors")
9
+ end
10
+
11
+ # This error is raised if the platform is not MacOS.
12
+ class MacOSRequired < UtmError
13
+ error_key(:macos_required)
14
+ end
15
+
16
+ # This error is raised if the UTM is not found.
17
+ class UtmNotDetected < UtmError
18
+ error_key(:utm_not_detected)
19
+ end
20
+
21
+ # This error is raised if the UTM version is not supported.
22
+ class UtmInvalidVersion < UtmError
23
+ error_key(:utm_invalid_version)
24
+ end
25
+
26
+ # This error is raised if the utmctl is not found.
27
+ class UtmctlNotFoundError < UtmError
28
+ error_key(:utmctl_not_found)
29
+ end
30
+
31
+ # This error is raised if the utmctl command fails.
32
+ class UtmctlError < UtmError
33
+ error_key(:utmctl_error)
34
+ end
35
+
36
+ # This error is raised if the utm or other binary like osa fail to launch.
37
+ class UtmLaunchError < UtmError
38
+ error_key(:utm_launch_error)
39
+ end
40
+
41
+ # This error is raised if a shell/osascript command fails.
42
+ class CommandError < UtmError
43
+ error_key(:command_error)
44
+ end
45
+
46
+ # This error is raised if UTM file was failed to import.
47
+ class UtmImportFailed < UtmError
48
+ error_key(:utm_import_failed)
49
+ end
50
+
51
+ # This error is raised if invalid protocol is used in forwarded ports.
52
+ class ForwardedPortInvalidProtocol < UtmError
53
+ error_key(:forwarded_port_invalid_protocol)
54
+ end
55
+
56
+ # This error is raised if the qemu-img is not detected.
57
+ class QemuImgRequired < UtmError
58
+ error_key(:qemu_img_required)
59
+ end
60
+
61
+ # This error is raised if the snapshot command failed.
62
+ class SnapShotCommandFailed < UtmError
63
+ error_key(:snapshot_command_failed)
64
+ end
65
+
66
+ # This error is raised if multiple VM files are found during snapshot.
67
+ class SnapShotMultipleVMFiles < UtmError
68
+ error_key(:snapshot_multiple_vm_files)
69
+ end
70
+
71
+ # This error is raised if the VM is not found.
72
+ class SnapShotVMFileNotFound < UtmError
73
+ error_key(:snapshot_vm_file_not_found)
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) HashiCorp, Inc.
4
+ # SPDX-License-Identifier: BUSL-1.1
5
+
6
+ module VagrantPlugins
7
+ module Utm
8
+ module Model
9
+ # Represents a single forwarded port for UTM. This has various
10
+ # helpers and defaults for a forwarded port.
11
+ class ForwardedPort
12
+ # The 'Emulated VLAN' adapter on which to attach the forwarded port.
13
+ #
14
+ # @return [Integer]
15
+ attr_reader :adapter
16
+
17
+ # If true, this port should be auto-corrected.
18
+ # TODO: This is not implemented yet.
19
+ # @return [Boolean]
20
+ attr_reader :auto_correct
21
+
22
+ # The unique ID for the forwarded port.
23
+ #
24
+ # @return [String]
25
+ attr_reader :id
26
+
27
+ # The protocol to forward.
28
+ #
29
+ # @return [String]
30
+ attr_reader :protocol
31
+
32
+ # The IP that the forwarded port will connect to on the guest machine.
33
+ #
34
+ # @return [String]
35
+ attr_reader :guest_ip
36
+
37
+ # The port on the guest to be exposed on the host.
38
+ #
39
+ # @return [Integer]
40
+ attr_reader :guest_port
41
+
42
+ # The IP that the forwarded port will bind to on the host machine.
43
+ #
44
+ # @return [String]
45
+ attr_reader :host_ip
46
+
47
+ # The port on the host used to access the port on the guest.
48
+ #
49
+ # @return [Integer]
50
+ attr_reader :host_port
51
+
52
+ def initialize(id, host_port, guest_port, options)
53
+ @id = id
54
+ @guest_port = guest_port
55
+ @host_port = host_port
56
+
57
+ options ||= {}
58
+ @auto_correct = false
59
+ @auto_correct = options[:auto_correct] if options.key?(:auto_correct)
60
+ @adapter = (options[:adapter] || 1).to_i # if adapter is not set, use 1. index 0 is the default adapter
61
+ @guest_ip = options[:guest_ip] || nil
62
+ @host_ip = options[:host_ip] || nil
63
+ @protocol = options[:protocol] || "tcp" # default to TCP
64
+ end
65
+
66
+ # This corrects the host port and changes it to the given new port.
67
+ # TODO: This is not implemented yet.
68
+ # @param [Integer] new_port The new port
69
+ def correct_host_port(new_port)
70
+ @host_port = new_port
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VagrantPlugins
4
+ module Utm
5
+ module Model
6
+ # Represents the result of a 'utmctl list' operation.
7
+ class ListResult
8
+ # @return [Array<ListResultItem>] The list of machines.
9
+ attr_accessor :machines
10
+
11
+ # Initialize the result from raw data.
12
+ # @param [Array<Hash>] data The raw data.
13
+ def initialize(data)
14
+ @machines = []
15
+ data.each do |machine|
16
+ item = ListResultItem.new(machine)
17
+ @machines << item
18
+ end
19
+ end
20
+
21
+ # Checks if a machine with the given UUID exists.
22
+ # @param uuid [String] The UUID of the machine.
23
+ # @return [Boolean]
24
+ def any?(uuid)
25
+ @machines.any? { |i| i.uuid == uuid }
26
+ end
27
+
28
+ # Finds a machine with the given name or uuid.
29
+ # @param [String] name The name of the machine.
30
+ # @return [ListResultItem]
31
+ def find(name: nil, uuid: nil)
32
+ if name
33
+ @machines.find { |i| i.name == name }
34
+ elsif uuid
35
+ @machines.find { |i| i.uuid == uuid }
36
+ end
37
+ end
38
+
39
+ # Return the last machine in the list.
40
+ # @return [ListResultItem]
41
+ def last
42
+ @machines.last
43
+ end
44
+
45
+ # Represents an item in the list result.
46
+ class ListResultItem
47
+ # @return [String] The UUID of the machine.
48
+ attr_accessor :uuid
49
+ # @return [String] The name of the machine.
50
+ attr_accessor :name
51
+ # @return [String] The state of the machine.
52
+ attr_accessor :state
53
+
54
+ # Initialize the result from raw data.
55
+ # @param [Hash] data The raw data.
56
+ def initialize(data)
57
+ @uuid = data["UUID"]
58
+ @name = data["Name"]
59
+ @state = data["Status"]
60
+ end
61
+
62
+ # Returns the state of the machine using Vagrant symbols.
63
+ def vagrant_state
64
+ case @state
65
+ when "running"
66
+ :running
67
+ when "stopped", "suspended"
68
+ :stopped
69
+ else
70
+ :host_state_unknown
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ # check if the Vagrant gem is available
4
+ begin
5
+ require "vagrant"
6
+ rescue LoadError
7
+ raise "The 'vagrant' gem could not be found. Please make sure it is installed."
8
+ end
9
+
10
+ # check if the Vagrant version is sufficient
11
+ raise "The Vagrant UTM plugin is only compatible with Vagrant 2.3 or later" if Vagrant::VERSION < "2.3.0"
12
+
13
+ module VagrantPlugins
14
+ module Utm
15
+ # This is the main entry point for the UTM provider plugin.
16
+ class Plugin < Vagrant.plugin("2")
17
+ name "utm"
18
+ description <<-DESCRIPTION
19
+ UTM provider allows Vagrant to manage and control
20
+ VMs using UTM through Apple Scripting Bridge.
21
+ DESCRIPTION
22
+
23
+ # Register the provider
24
+ # TODO: Define box format for UTM
25
+ # IDEA: UTM file comes as a zip file containing
26
+ # directory with Data/qcow2, Data/efi_vars.fd and config.plist
27
+ # Box format will only require additional metadata.json file
28
+ # Till then use UTM file directly and so box_optional: true
29
+ provider(:utm, box_optional: true, parallel: false) do
30
+ setup_i18n
31
+ require_relative "provider"
32
+ Provider
33
+ end
34
+
35
+ # Register the configuration
36
+ config(:utm, :provider) do
37
+ require_relative "config"
38
+ Config
39
+ end
40
+
41
+ # Register capabilities
42
+ provider_capability(:utm, :forwarded_ports) do
43
+ require_relative "cap"
44
+ Cap
45
+ end
46
+
47
+ provider_capability(:utm, :snapshot_list) do
48
+ require_relative "cap"
49
+ Cap
50
+ end
51
+
52
+ # Register the command
53
+ command "disposable" do
54
+ require_relative "disposable"
55
+ Disposable
56
+ end
57
+
58
+ # Load the translation files
59
+ def self.setup_i18n
60
+ I18n.load_path << File.expand_path("locales/en.yml", Utm.source_root)
61
+ I18n.reload!
62
+ end
63
+ end
64
+ end
65
+ end