vagrant_utm 0.1.6 → 0.2.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7e9244b20397925333750f59097f53ae24d91d577a2b576bb9708575900e6322
4
- data.tar.gz: 661fdde402f8e5524c615ca564f2e2996ba802faae4e9def172c3cc40ad4b675
3
+ metadata.gz: ec09354bc45c159ba048b7965ffd6802e865a3f60e99e74d2bdd5f9bb5bf3166
4
+ data.tar.gz: 001d7226ab9fdd498d8340b0a095eaebe1774424418095f0a73fbb42e04fc1c9
5
5
  SHA512:
6
- metadata.gz: 443831d9674fa2256109ecae5d40bbd6e25034e6226287188238e2659d7b8e9ba6628b5bf26bbfed4e16e06000724b8008f39c099c5a4b018aaba2715fd00728
7
- data.tar.gz: 21a86fd8e831745d6cac5b48d3b7d582e033be905afc1de4b1206984d8a9a9c6e8020ecd428c6a2dda11628d0ac4880ed44b6821ad144ac7321eebfb42343bc9
6
+ metadata.gz: d83c1d82fdcb5dc2983edd7b16c4a72b76b934a7b66bb379b80e46e85a70a6ad0b2e1720c56f1311f3cc42f465034d08acf66c78f8a6ddac15c155ab0b001236
7
+ data.tar.gz: 5247adf195d74c9992cb03451baa55f5ac5c7c5d4bc8a58f2e651b73f044bcf806fe7a8ed66a43119e549bf2c8df754e7c137f75119394c31155626e5c7662e7
data/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.0] - 2026-10-14
4
+
5
+ ### Added
6
+ - Use UTM 5.0.6's native snapshot API through `utmctl` for supported QEMU
7
+ and Apple Virtualization VMs (Apple snapshots require macOS 27).
8
+ - Support UTM 5.0.4+ VLAN guest and DHCP network properties through
9
+ `network_interface` and `update_network_interface`.
10
+
11
+ ### Changed
12
+ - Let UTM handle snapshot creation, listing, restoration, and deletion across
13
+ supported VM states.
14
+ - Stop a running VM before restoring a snapshot to avoid UTM's live restore RPC
15
+ timeout, then restart it unless Vagrant's `--no-start` option is given.
16
+
17
+ ### Removed
18
+ - Remove the `qemu-img` snapshot workaround. Snapshots now require UTM 5.0.6 or
19
+ newer; keep an older plugin release if you need snapshots on older UTM.
20
+
3
21
  ## [0.1.6] - 2026-08-14
4
22
 
5
23
  ### Added
@@ -94,4 +112,4 @@ IMPORTANT: This version of the plugin only works with UTM version 4.5.1 and abov
94
112
  ## [0.0.1] (Pilot release) - 2024-08-08
95
113
 
96
114
  * Initial release with all basic vagrant commands
97
- * Uses UTM file in zip format from a url as VM box to import
115
+ * Uses UTM file in zip format from a url as VM box to import
data/README.md CHANGED
@@ -27,7 +27,7 @@ vagrant up
27
27
 
28
28
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
29
29
 
30
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
30
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, follow the [release steps](notes/README.md#to-release). The tag-triggered GitHub Actions workflow builds and publishes the gem.
31
31
 
32
32
  ### Vagrant
33
33
 
data/docs/commands.md CHANGED
@@ -9,7 +9,7 @@ nav_order: 2
9
9
 
10
10
  This page lists all the supported Vagrant commands which depend on the UTM provider. Eg. `up`, `suspend`, `resume`, `halt`.
11
11
 
12
- Adds note to the command which are have some limitations. Eg. `snapshot`.
12
+ Adds notes to commands which have provider-specific behavior. Eg. `snapshot`.
13
13
 
14
14
  The Vagrant commands that do not depend on provider are not listed and will continue to work. Eg. `global-status`
15
15
 
@@ -84,13 +84,23 @@ This resumes a Vagrant managed machine that was previously suspended, perhaps wi
84
84
 
85
85
  **Command: `vagrant snapshot`**
86
86
 
87
- {: .warning }
88
- Snapshot feature is not available in UTM.
89
- The plugin just provides experimental feature using qemu-img
87
+ `utmctl snapshot`
90
88
 
91
- Vagrant UTM provider supports offline snapshots using
92
- qemu-img. Hence only VM with single qcow2 file is supported.
89
+ With UTM 5.0.6 or newer, the provider uses UTM's native named snapshots.
90
+ UTM supports QEMU snapshots on macOS. Apple Virtualization snapshots require
91
+ macOS 27. A snapshot of a running QEMU VM includes its running state; a
92
+ snapshot of a stopped VM saves its disks. UTM handles saving, listing, and
93
+ deleting snapshots through its native API.
93
94
 
95
+ `vagrant snapshot restore` stops a running VM before restoring its snapshot.
96
+ Vagrant starts the restored VM afterward unless `--no-start` is given. UTM may
97
+ resume saved RAM state when the restored VM next starts. The provider does not call
98
+ `utmctl discard-state` during a restore, because that would discard the saved
99
+ running state.
100
+
101
+ Older UTM releases do not expose the 5.0.6 snapshot API. This plugin no longer uses
102
+ `qemu-img` as a fallback. If you need snapshots on UTM older than 5.0.6,
103
+ keep the previous plugin version that paired with that UTM release.
94
104
 
95
105
 
96
106
  ## **SSH**
@@ -27,6 +27,35 @@ config.vm.provider "utm" do |u|
27
27
  end
28
28
  ```
29
29
 
30
+ ## Shared network VLAN / DHCP (UTM 5.0.4+)
31
+
32
+ UTM 5.0.4 exposed AppleScript properties for the shared/host network VLAN subnet and DHCP pool ([utmapp/UTM#7710](https://github.com/utmapp/UTM/pull/7710)), so you no longer need to set them manually in the UTM UI ([utmapp/UTM#3294](https://github.com/utmapp/UTM/issues/3294)).
33
+
34
+ Index `0` is typically the first (Shared Network) adapter used by Vagrant boxes:
35
+
36
+ ```ruby
37
+ config.vm.provider "utm" do |u|
38
+ u.network_interface 0,
39
+ vlan_guest_address: "192.168.222.0/24",
40
+ vlan_dhcp_start_address: "192.168.222.2",
41
+ vlan_dhcp_end_address: "192.168.222.254",
42
+ isolate_from_host: false
43
+ end
44
+ ```
45
+
46
+ Optional keys: `vlan_guest_address`, `vlan_guest_address_ipv6`, `vlan_dhcp_start_address`, `vlan_dhcp_end_address`, `isolate_from_host`.
47
+
48
+ You can also call the driver directly when the machine already exists:
49
+
50
+ ```ruby
51
+ machine.provider.driver.update_network_interface(
52
+ 0,
53
+ vlan_guest_address: "192.168.222.0/24",
54
+ vlan_dhcp_start_address: "192.168.222.2",
55
+ vlan_dhcp_end_address: "192.168.222.254"
56
+ )
57
+ ```
58
+
30
59
  ## Other customization
31
60
 
32
61
  ```ruby
@@ -7,15 +7,16 @@ nav_order: 1
7
7
 
8
8
  The Table below maps the vagrant commands to the corresponding UTM commands that are executed.
9
9
 
10
- | Vagrant | utmctl / AppleScript |
11
- | --- | ------------------ |
12
- | `vagrant up` | `utmctl start` |
13
- | `vagrant halt` | `utmctl stop` |
14
- | `vagrant suspend` | `utmctl suspend` |
15
- | `vagrant resume` | `utmctl start` |
16
- | `vagrant reload` | update configuration of vm with config |
17
- | `vagrant destroy` | `utmctl delete` |
18
- | `vagrant status` | `utmctl status` |
19
- | `vagrant disposable` | `utmctl start --disposable` |
20
- | `vagrant snapshot` | `qemu-img snapshot` |
21
- | `vagrant ip-address` | `utmctl ip-address` |
10
+ | Vagrant | utmctl / AppleScript |
11
+ | --- | --- |
12
+ | `vagrant up` | `utmctl start` |
13
+ | `vagrant halt` | `utmctl stop` |
14
+ | `vagrant suspend` | `utmctl suspend` |
15
+ | `vagrant resume` | `utmctl start` |
16
+ | `vagrant reload` | update configuration of vm with config |
17
+ | `vagrant destroy` | `utmctl delete` |
18
+ | `vagrant status` | `utmctl status` |
19
+ | `vagrant disposable` | `utmctl start --disposable` |
20
+ | `vagrant snapshot` | `utmctl snapshot` |
21
+ | `vagrant ip-address` | `utmctl ip-address` |
22
+ | Provider `network_interface` (UTM 5.0.4+) | AppleScript network VLAN/DHCP properties |
@@ -23,8 +23,8 @@ So we rely on Apple Scripting Bridge scripts which can be executed using `osascr
23
23
  When it comes to writing the OSA scripts, our first choice is applescript language due to its simplicity and support.
24
24
  But, if we need to exchange complex data like json from UTM to plugin we use Javascript (Note: Some APIs are difficult to get it working in JS, hence our first choice is applescript).
25
25
 
26
- **3. Shell command** :
27
- All interactions with UTM should be possible with `utmctl` or `osascript`. But, due to the unavailablity of certain commands or features, we work around the issue by using direct shell command.
28
- For Example:
26
+ **3. Shell command** :
27
+ All interactions with UTM should be possible with `utmctl` or `osascript`.
28
+ Direct shell commands are reserved for capabilities that UTM does not expose.
29
29
 
30
- * Snapshot `qemu-img snapshot ...`
30
+ Snapshots use UTM's native `utmctl snapshot` commands.
data/docs/known_issues.md CHANGED
@@ -19,6 +19,5 @@ Draw back - UTM does not expose export API. (UTM already has 'Share')~~
19
19
 
20
20
  4. Hide: Any plugin action will bring up the main UTM window. However, a properly built UTM box with no display will run headless.
21
21
 
22
- 5. vagrant snapshot: Even though UTM does not have snapshot feature, this plugin has a experimental support for offline VM snapshots using qemu-img.
23
- The VM must be stopped, for any snapshot commands to work.
24
- The snapshot only works for **single** qcow2 based VM images
22
+ 5. `vagrant snapshot` requires UTM 5.0.6 or newer (native `utmctl snapshot`).
23
+ There is no `qemu-img` fallback in this plugin version.
@@ -13,7 +13,7 @@ module VagrantPlugins
13
13
  @event = event
14
14
  end
15
15
 
16
- def call(env) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity
16
+ def call(env)
17
17
  customizations = []
18
18
  env[:machine].provider_config.customizations.each do |event, command|
19
19
  customizations << command if event == @event
@@ -24,26 +24,35 @@ module VagrantPlugins
24
24
 
25
25
  # Execute each customization command.
26
26
  customizations.each do |command|
27
- processed_command = command.collect do |arg|
28
- arg = env[:machine].id if arg == :id
29
- arg.to_s
30
- end
31
-
32
- begin
33
- env[:machine].provider.driver.execute_osa_script(
34
- processed_command
35
- )
36
- rescue VagrantPlugins::Utm::Errors::CommandError => e
37
- raise Vagrant::Errors::VMCustomizationFailed, {
38
- command: command,
39
- error: e.inspect
40
- }
41
- end
27
+ execute_customization(env, command)
42
28
  end
43
29
  end
44
30
 
45
31
  @app.call(env)
46
32
  end
33
+
34
+ private
35
+
36
+ def execute_customization(env, command)
37
+ if command.first == :network_interface
38
+ env[:machine].provider.driver.update_network_interface(command[1], command[2])
39
+ else
40
+ execute_osa_customization(env, command)
41
+ end
42
+ rescue VagrantPlugins::Utm::Errors::CommandError => e
43
+ raise Vagrant::Errors::VMCustomizationFailed, {
44
+ command: command,
45
+ error: e.inspect
46
+ }
47
+ end
48
+
49
+ def execute_osa_customization(env, command)
50
+ processed_command = command.map do |arg|
51
+ arg = env[:machine].id if arg == :id
52
+ arg.to_s
53
+ end
54
+ env[:machine].provider.driver.execute_osa_script(processed_command)
55
+ end
47
56
  end
48
57
  end
49
58
  end
@@ -14,7 +14,6 @@ module VagrantPlugins
14
14
  autoload :CheckAccessible, action_root.join("check_accessible")
15
15
  autoload :CheckCreated, action_root.join("check_created")
16
16
  autoload :CheckGuestAdditions, action_root.join("check_guest_additions")
17
- autoload :CheckQemuImg, action_root.join("check_qemu_img")
18
17
  autoload :CheckRunning, action_root.join("check_running")
19
18
  autoload :CheckUtm, action_root.join("check_utm")
20
19
  autoload :ClearForwardedPorts, action_root.join("clear_forwarded_ports")
@@ -266,17 +265,9 @@ module VagrantPlugins
266
265
  def self.action_snapshot_delete
267
266
  Vagrant::Action::Builder.new.tap do |b|
268
267
  b.use CheckUtm
269
- b.use CheckQemuImg
270
268
  b.use Call, Created do |env, b2|
271
269
  if env[:result]
272
- # qemu-img needs write-lock to file, so VM should be stopped
273
- b2.use Call, IsStopped do |env2, b3|
274
- if env2[:result]
275
- b3.use SnapshotDelete
276
- else
277
- b3.use MessageNotStopped
278
- end
279
- end
270
+ b2.use SnapshotDelete
280
271
  else
281
272
  b2.use MessageNotCreated
282
273
  end
@@ -288,20 +279,19 @@ module VagrantPlugins
288
279
  def self.action_snapshot_restore # rubocop:disable Metrics/AbcSize
289
280
  Vagrant::Action::Builder.new.tap do |b|
290
281
  b.use CheckUtm
291
- b.use CheckQemuImg
292
282
  b.use Call, Created do |env, b2|
293
283
  raise Vagrant::Errors::VMNotCreatedError unless env[:result]
294
284
 
295
285
  b2.use CheckAccessible
286
+ # Restoring a running QEMU VM can leave UTM waiting for its RPC.
287
+ # Stop it first, then let Vagrant honor --no-start below.
296
288
  b2.use EnvSet, force_halt: true
297
289
  b2.use action_halt
298
- b2.use SnapshotRestore
299
-
300
- b2.use Call, IsEnvSet, :snapshot_delete do |env2, b3|
301
- b3.use action_snapshot_delete if env2[:result]
302
- end
303
-
304
290
  b2.use Call, IsEnvSet, :snapshot_start do |env2, b3|
291
+ b3.use SnapshotRestore
292
+ b3.use Call, IsEnvSet, :snapshot_delete do |env3, b4|
293
+ b4.use action_snapshot_delete if env3[:result]
294
+ end
305
295
  b3.use action_start if env2[:result]
306
296
  end
307
297
  end
@@ -312,17 +302,9 @@ module VagrantPlugins
312
302
  def self.action_snapshot_save
313
303
  Vagrant::Action::Builder.new.tap do |b|
314
304
  b.use CheckUtm
315
- b.use CheckQemuImg
316
305
  b.use Call, Created do |env, b2|
317
306
  if env[:result]
318
- # qemu-img does offline snapshot, so VM should be stopped
319
- b2.use Call, IsStopped do |env2, b3|
320
- if env2[:result]
321
- b3.use SnapshotSave
322
- else
323
- b3.use MessageNotStopped
324
- end
325
- end
307
+ b2.use SnapshotSave
326
308
  else
327
309
  b2.use MessageNotCreated
328
310
  end
@@ -27,13 +27,7 @@ module VagrantPlugins
27
27
  def self.snapshot_list(machine)
28
28
  return [] if machine.id.nil?
29
29
 
30
- begin
31
- machine.provider.driver.list_snapshots(machine.id)
32
- rescue VagrantPlugins::Utm::Errors::CommandError => e
33
- raise Errors::SnapShotCommandFailed, {
34
- stderr: e.inspect
35
- }
36
- end
30
+ machine.provider.driver.list_snapshots(machine.id)
37
31
  end
38
32
  end
39
33
  end
@@ -7,6 +7,13 @@ module VagrantPlugins
7
7
  module Utm
8
8
  # This is the configuration class for the UTM provider.
9
9
  class Config < Vagrant.plugin("2", :config)
10
+ NETWORK_OPTION_FLAGS = {
11
+ vlan_guest_address: "--vlan-guest-address",
12
+ vlan_guest_address_ipv6: "--vlan-guest-address-ipv6",
13
+ vlan_dhcp_start_address: "--vlan-dhcp-start-address",
14
+ vlan_dhcp_end_address: "--vlan-dhcp-end-address"
15
+ }.freeze
16
+
10
17
  # If true, will check if guest additions are installed and up to
11
18
  # date. By default, this is true.
12
19
  #
@@ -123,6 +130,33 @@ module VagrantPlugins
123
130
  customize("pre-boot", ["customize_vm.applescript", :id, "--directory-share-mode", mode_code])
124
131
  end
125
132
 
133
+ # Configure VLAN / DHCP settings on a UTM network interface.
134
+ #
135
+ # Maps to the AppleScript network properties added in UTM 5.0.4
136
+ # (utmapp/UTM#7710). These apply to shared/host network modes and let
137
+ # you pin the guest subnet / DHCP pool without editing the UTM UI
138
+ # (see utmapp/UTM#3294).
139
+ #
140
+ # @param index [Integer] network interface index (0 = first adapter)
141
+ # @param options [Hash]
142
+ # @option options [String] :vlan_guest_address IPv4 subnet CIDR (e.g. "192.168.222.0/24")
143
+ # @option options [String] :vlan_guest_address_ipv6 IPv6 prefix (e.g. "fec0::/64")
144
+ # @option options [String] :vlan_dhcp_start_address first DHCP pool address
145
+ # @option options [String] :vlan_dhcp_end_address last DHCP pool address
146
+ # @option options [Boolean] :isolate_from_host isolate guest from host
147
+ # @return [void]
148
+ def network_interface(index, **options)
149
+ if network_option_arguments(options).empty?
150
+ raise Vagrant::Errors::ConfigInvalid,
151
+ errors: "network_interface requires at least one of: " \
152
+ "vlan_guest_address, vlan_guest_address_ipv6, " \
153
+ "vlan_dhcp_start_address, vlan_dhcp_end_address, " \
154
+ "isolate_from_host"
155
+ end
156
+
157
+ customize("pre-boot", [:network_interface, index, options])
158
+ end
159
+
126
160
  # This is the hook that is called to finalize the object before it
127
161
  # is put into use.
128
162
  def finalize!
@@ -161,6 +195,26 @@ module VagrantPlugins
161
195
  def to_s
162
196
  "UTM"
163
197
  end
198
+
199
+ private
200
+
201
+ def network_option_arguments(options)
202
+ NETWORK_OPTION_FLAGS.flat_map do |key, flag|
203
+ option_argument(flag, options[key])
204
+ end + isolate_from_host_argument(options[:isolate_from_host])
205
+ end
206
+
207
+ def option_argument(flag, value)
208
+ return [] if value.nil? || value.to_s.empty?
209
+
210
+ [flag, value.to_s]
211
+ end
212
+
213
+ def isolate_from_host_argument(value)
214
+ return [] if value.nil?
215
+
216
+ ["--isolate-from-host", value ? "true" : "false"]
217
+ end
164
218
  end
165
219
  end
166
220
  end
@@ -39,6 +39,26 @@ module VagrantPlugins
39
39
  # @return [Boolean]
40
40
  def check_qemu_guest_agent; end
41
41
 
42
+ # Create a named full-VM snapshot.
43
+ def create_snapshot(_machine_id, _snapshot_name)
44
+ snapshot_unsupported!
45
+ end
46
+
47
+ # Delete a named full-VM snapshot.
48
+ def delete_snapshot(_machine_id, _snapshot_name)
49
+ snapshot_unsupported!
50
+ end
51
+
52
+ # List named full-VM snapshots.
53
+ def list_snapshots(_machine_id)
54
+ snapshot_unsupported!
55
+ end
56
+
57
+ # Restore a named full-VM snapshot.
58
+ def restore_snapshot(_machine_id, _snapshot_name)
59
+ snapshot_unsupported!
60
+ end
61
+
42
62
  # Forwards a set of ports for a VM.
43
63
  #
44
64
  # This will not affect any previously set forwarded ports,
@@ -103,6 +123,14 @@ module VagrantPlugins
103
123
  # @return [Hash]
104
124
  def read_network_interfaces; end
105
125
 
126
+ # Update VLAN / DHCP settings on a network interface (UTM 5.0.4+).
127
+ #
128
+ # @param [Integer] index Network interface index
129
+ # @param [Hash] options VLAN / DHCP options
130
+ def update_network_interface(_index, _options = {})
131
+ raise Errors::UtmInvalidVersion, supported_versions: "5.0.4+"
132
+ end
133
+
106
134
  # Sets the MAC address of the first network adapter.
107
135
  #
108
136
  # @param [String] mac MAC address without any spaces/hyphens.
@@ -333,6 +361,11 @@ module VagrantPlugins
333
361
 
334
362
  private
335
363
 
364
+ def snapshot_unsupported!
365
+ raise Errors::UtmInvalidVersion,
366
+ supported_versions: "5.0.6+ (required for snapshots)"
367
+ end
368
+
336
369
  # List of LANG values to attempt to use
337
370
  LANG_VARIATIONS = %w[C.UTF-8 C.utf8 en_US.UTF-8 en_US.utf8 C POSIX].map(&:freeze).freeze
338
371
 
@@ -121,6 +121,7 @@ module VagrantPlugins
121
121
  :start,
122
122
  :start_disposable,
123
123
  :suspend,
124
+ :update_network_interface,
124
125
  :vm_exists?
125
126
 
126
127
  protected
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "log4r"
4
- require "etc"
5
- require "vagrant/util/platform"
6
4
 
7
5
  require File.expand_path("base", __dir__)
8
6
 
@@ -38,50 +36,11 @@ module VagrantPlugins
38
36
  output.include?("root")
39
37
  end
40
38
 
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
39
  def export(_path)
56
40
  @logger.info("This version of UTM does not support exporting VMs
57
41
  Please upgrade to the latest version of UTM or UTM 'Share' feature in UI to export the virtual machine")
58
42
  end
59
43
 
60
- def list_snapshots(machine_id) # rubocop:disable Metrics/AbcSize
61
- list_result = list
62
- machine_name = list_result.find(uuid: machine_id).name
63
- machine_file = get_vm_file(machine_name)
64
- output = execute_shell("qemu-img", "snapshot", "-l", machine_file)
65
-
66
- return [] if output.nil? || output.strip.empty?
67
-
68
- @logger.debug("list_snapshots_here: #{output}")
69
-
70
- result = []
71
- output.split("\n").map do |line|
72
- result << ::Regexp.last_match(1).to_s if line =~ /^\d+\s+(\w+)/
73
- end
74
-
75
- result.sort
76
- end
77
-
78
- def restore_snapshot(machine_id, snapshot_name)
79
- list_result = list
80
- machine_name = list_result.find(uuid: machine_id).name
81
- machine_file = get_vm_file(machine_name)
82
- execute_shell("qemu-img", "snapshot", "-a", snapshot_name, machine_file)
83
- end
84
-
85
44
  def forward_ports(ports) # rubocop:disable Metrics/CyclomaticComplexity
86
45
  args = []
87
46
  ports.each do |options|
@@ -112,31 +71,6 @@ module VagrantPlugins
112
71
  execute_osa_script(command) unless args.empty?
113
72
  end
114
73
 
115
- def get_vm_file(vm_name)
116
- # Get the current username
117
- username = ENV["USER"] || Etc.getlogin
118
-
119
- data_path = "/Users/#{username}/Library/Containers/com.utmapp.UTM/Data/Documents/#{vm_name}.utm/Data"
120
- # Define the regex for UUID pattern
121
- 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$/
122
-
123
- # List the files in the directory and filter by regex
124
- matching_files = Dir.entries(data_path).select do |file|
125
- file.match?(pattern)
126
- end
127
-
128
- if matching_files.length == 1
129
- # If there is exactly one matching file, return full path to file
130
- "#{data_path}/#{matching_files[0]}"
131
- elsif matching_files.length > 1
132
- # If there are multiple matching files, raise an error
133
- raise Errors::SnapShotMultipleVMFiles, directory: data_path, files: matching_files
134
- else
135
- # If there are no matching files, raise an error
136
- raise Errors::SnapShotVMFileNotFound, directory: data_path
137
- end
138
- end
139
-
140
74
  # Check if the VM with the given UUID exists.
141
75
  def vm_exists?(uuid)
142
76
  list_result = list
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "json"
4
+
3
5
  require File.expand_path("version_4_7", __dir__)
4
6
 
5
7
  module VagrantPlugins
@@ -7,11 +9,108 @@ module VagrantPlugins
7
9
  module Driver
8
10
  # Driver for UTM 5.0.x
9
11
  class Version_5_0 < Version_4_7 # rubocop:disable Naming/ClassAndModuleCamelCase
12
+ # UTM AppleScript VLAN network properties land in 5.0.4
13
+ # (utmapp/UTM#7710).
14
+ VLAN_NETWORK_MIN_VERSION = "5.0.4"
15
+ NETWORK_OPTION_FLAGS = {
16
+ vlan_guest_address: "--vlan-guest-address",
17
+ vlan_guest_address_ipv6: "--vlan-guest-address-ipv6",
18
+ vlan_dhcp_start_address: "--vlan-dhcp-start-address",
19
+ vlan_dhcp_end_address: "--vlan-dhcp-end-address"
20
+ }.freeze
21
+ SNAPSHOT_MIN_VERSION = "5.0.6"
22
+
10
23
  def initialize(uuid)
11
24
  super
12
25
 
13
26
  @logger = Log4r::Logger.new("vagrant::provider::utm::version_5_0")
14
27
  end
28
+
29
+ # Update VLAN / DHCP settings on a network interface.
30
+ #
31
+ # These properties apply to shared/host network modes and map to the
32
+ # AppleScript fields added in utmapp/UTM#7710 (UTM 5.0.4+).
33
+ #
34
+ # @param [Integer] index Network interface index (0-based)
35
+ # @param [Hash] options
36
+ # @option options [String] :vlan_guest_address IPv4 subnet CIDR
37
+ # @option options [String] :vlan_guest_address_ipv6 IPv6 prefix
38
+ # @option options [String] :vlan_dhcp_start_address DHCP pool start
39
+ # @option options [String] :vlan_dhcp_end_address DHCP pool end
40
+ # @option options [Boolean] :isolate_from_host Isolate guest from host
41
+ def update_network_interface(index, options = {})
42
+ require_vlan_network_support!(__method__)
43
+
44
+ args = ["--index", index.to_s] + network_option_arguments(options)
45
+
46
+ if args.length == 2
47
+ raise ArgumentError,
48
+ "update_network_interface requires at least one network option"
49
+ end
50
+
51
+ command = ["update_network_interface.applescript", @uuid] + args
52
+ execute_osa_script(command)
53
+ end
54
+
55
+ def create_snapshot(machine_id, snapshot_name)
56
+ require_snapshot_support!
57
+ execute("snapshot", "create", machine_id, "--name", snapshot_name)
58
+ end
59
+
60
+ def delete_snapshot(machine_id, snapshot_name)
61
+ require_snapshot_support!
62
+ execute("snapshot", "delete", machine_id, "--snapshot", snapshot_name)
63
+ end
64
+
65
+ def list_snapshots(machine_id)
66
+ require_snapshot_support!
67
+ output = execute("snapshot", "list", machine_id, "--json")
68
+ JSON.parse(output).map { |snapshot| snapshot.fetch("name") }
69
+ end
70
+
71
+ def restore_snapshot(machine_id, snapshot_name)
72
+ require_snapshot_support!
73
+ execute("snapshot", "restore", machine_id, "--snapshot", snapshot_name)
74
+ end
75
+
76
+ private
77
+
78
+ def network_option_arguments(options)
79
+ NETWORK_OPTION_FLAGS.flat_map do |key, flag|
80
+ option_argument(flag, options[key])
81
+ end + isolate_from_host_argument(options[:isolate_from_host])
82
+ end
83
+
84
+ def option_argument(flag, value)
85
+ return [] if value.nil? || value.to_s.empty?
86
+
87
+ [flag, value.to_s]
88
+ end
89
+
90
+ def isolate_from_host_argument(value)
91
+ return [] if value.nil?
92
+
93
+ ["--isolate-from-host", value ? "true" : "false"]
94
+ end
95
+
96
+ def require_vlan_network_support!(method_name)
97
+ version = read_utm_version
98
+ return if Gem::Version.new(version) >= Gem::Version.new(VLAN_NETWORK_MIN_VERSION)
99
+
100
+ raise Errors::UtmInvalidVersion,
101
+ supported_versions: "#{VLAN_NETWORK_MIN_VERSION}+ " \
102
+ "(required for #{method_name})"
103
+ end
104
+
105
+ def require_snapshot_support!
106
+ return if Gem::Version.new(read_utm_version) >= Gem::Version.new(SNAPSHOT_MIN_VERSION)
107
+
108
+ snapshot_unsupported!
109
+ end
110
+
111
+ def read_utm_version
112
+ @read_utm_version ||= execute("version").strip
113
+ end
15
114
  end
16
115
  end
17
116
  end
@@ -52,26 +52,6 @@ module VagrantPlugins
52
52
  class ForwardedPortInvalidProtocol < UtmError
53
53
  error_key(:forwarded_port_invalid_protocol)
54
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
55
  end
76
56
  end
77
57
  end
@@ -0,0 +1,86 @@
1
+ ---
2
+ -- update_network_interface.applescript
3
+ -- Update VLAN / DHCP settings on a UTM network interface (UTM 5.0.4+).
4
+ --
5
+ -- Usage:
6
+ -- osascript update_network_interface.applescript <VM_UUID> --index <N> [options]
7
+ --
8
+ -- Options (all optional; omitted values are left unchanged):
9
+ -- --vlan-guest-address <CIDR> e.g. 192.168.222.0/24
10
+ -- --vlan-guest-address-ipv6 <prefix> e.g. fec0::/64
11
+ -- --vlan-dhcp-start-address <IPv4>
12
+ -- --vlan-dhcp-end-address <IPv4>
13
+ -- --isolate-from-host <true|false>
14
+ --
15
+ -- These properties apply to shared/host network modes (see UTM #7710 / #3294).
16
+
17
+ on run argv
18
+ set vmID to item 1 of argv
19
+ set nicIndex to missing value
20
+ set vlanGuestAddress to missing value
21
+ set vlanGuestAddressIpv6 to missing value
22
+ set vlanDhcpStartAddress to missing value
23
+ set vlanDhcpEndAddress to missing value
24
+ set isolateFromHost to missing value
25
+
26
+ repeat with i from 2 to (count of argv)
27
+ set currentArg to item i of argv
28
+ if currentArg is "--index" then
29
+ set nicIndex to item (i + 1) of argv as integer
30
+ else if currentArg is "--vlan-guest-address" then
31
+ set vlanGuestAddress to item (i + 1) of argv
32
+ else if currentArg is "--vlan-guest-address-ipv6" then
33
+ set vlanGuestAddressIpv6 to item (i + 1) of argv
34
+ else if currentArg is "--vlan-dhcp-start-address" then
35
+ set vlanDhcpStartAddress to item (i + 1) of argv
36
+ else if currentArg is "--vlan-dhcp-end-address" then
37
+ set vlanDhcpEndAddress to item (i + 1) of argv
38
+ else if currentArg is "--isolate-from-host" then
39
+ set isolateArg to item (i + 1) of argv
40
+ if isolateArg is "true" or isolateArg is "yes" or isolateArg is "1" then
41
+ set isolateFromHost to true
42
+ else
43
+ set isolateFromHost to false
44
+ end if
45
+ end if
46
+ end repeat
47
+
48
+ if nicIndex is missing value then
49
+ error "Missing required --index <nicIndex>"
50
+ end if
51
+
52
+ tell application "UTM"
53
+ set vm to virtual machine id vmID
54
+ set config to configuration of vm
55
+ set networkInterfaces to network interfaces of config
56
+ set updated to false
57
+
58
+ repeat with anInterface in networkInterfaces
59
+ if nicIndex is index of anInterface then
60
+ if vlanGuestAddress is not missing value then
61
+ set vlan guest address of anInterface to vlanGuestAddress
62
+ end if
63
+ if vlanGuestAddressIpv6 is not missing value then
64
+ set vlan guest address ipv6 of anInterface to vlanGuestAddressIpv6
65
+ end if
66
+ if vlanDhcpStartAddress is not missing value then
67
+ set vlan dhcp start address of anInterface to vlanDhcpStartAddress
68
+ end if
69
+ if vlanDhcpEndAddress is not missing value then
70
+ set vlan dhcp end address of anInterface to vlanDhcpEndAddress
71
+ end if
72
+ if isolateFromHost is not missing value then
73
+ set isolate from host of anInterface to isolateFromHost
74
+ end if
75
+ set updated to true
76
+ end if
77
+ end repeat
78
+
79
+ if updated is false then
80
+ error "Network interface index " & nicIndex & " not found"
81
+ end if
82
+
83
+ -- VM must be stopped
84
+ update configuration of vm with config
85
+ end tell
86
+ end run
@@ -4,6 +4,6 @@ module VagrantPlugins
4
4
  # Top level module for the Utm provider plugin.
5
5
  module Utm
6
6
  # Current version of the Utm provider plugin.
7
- VERSION = "0.1.6"
7
+ VERSION = "0.2.0"
8
8
  end
9
9
  end
data/locales/en.yml CHANGED
@@ -32,27 +32,6 @@ en:
32
32
  The virtual machine is not running.
33
33
  macos_required: |-
34
34
  MacOS is required to run this provider
35
- qemu_img_required: |-
36
- qemu-img is required to use snapshot feature.
37
- You can install qemu-img using `brew install qemu`.
38
- snapshot_command_failed: |-
39
- There was an error while executing `qemu-img`, a CLI used by vagrant-utm
40
- for managing UTM snapshots. qemu-img only supports offline operations, so
41
- the VM must be stopped before any snapshot operation.
42
- The error is shown below.
43
-
44
- Error: %{stderr}
45
- snapshot_multiple_vm_files: |-
46
- Multiple VM files detected. vagrant-utm which uses qemu-img does not support
47
- snapshot feature when multiple VM files are present.
48
-
49
- Directory checked: %{directory}
50
- Files found: %{files}
51
- snapshot_vm_file_not_found: |-
52
- VM file not detected. vagrant-utm checks for single qcow2 file named in
53
- UUID format in VM's data directory.
54
-
55
- Directory checked: %{directory}
56
35
  utm_import_failed: |-
57
36
  UTM virtual machine import failed.
58
37
  utmctl_error: |-
data/notes/README.md CHANGED
@@ -16,20 +16,34 @@ cd docs
16
16
  bundle exec jekyll serve
17
17
  ```
18
18
 
19
- To release
19
+ To release
20
20
 
21
- To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`
22
- GitHub action upon tag push with "v*" will publish gem to GHR and rubygems
21
+ Publishing is done by GitHub Actions. Pushing a tag matching "v*" triggers `.github/workflows/gem.yml`,
22
+ which builds the gem once and then publishes it to GitHub Packages and rubygems.org, and creates the
23
+ GitHub release with the `.gem` attached.
23
24
 
24
- 1. Update
25
- CHANGELOG.md
25
+ So locally we only bump the version and push the tag.
26
+
27
+ 1. Update
28
+ CHANGELOG.md (move the `[Unreleased]` entries under a new `[x.y.z] - YYYY-MM-DD` heading)
26
29
  version number in `version.rb`
27
- version number Gemlock file
28
- 2. Commit
29
- 3. Run `bundle exec rake release` (Commit and tags are pushed)
30
- 4. Cancel push to rubygems.org
30
+ version number in `Gemfile.lock` (the `vagrant_utm (x.y.z)` line under `PATH`)
31
+ 2. Commit
32
+ 3. Run `bundle exec rake release:guard_clean release:source_control_push`
33
+ (checks the tree is clean, creates the annotated tag, pushes the commit and the tag)
34
+ 4. Watch the run: `gh run watch $(gh run list --limit 1 --json databaseId --jq '.[0].databaseId')`
35
+
36
+ Do not run plain `bundle exec rake release`. It appends `release:rubygem_push`, which publishes the gem
37
+ from your machine and duplicates what GHA already does — the old workaround was to cancel that push by
38
+ hand, which is a race you can lose. Running the two subtasks above is the same flow without the race.
39
+
40
+ After the run, confirm the gem actually shipped what you expect:
31
41
 
32
- GHA will publish gems to GHR and rubygems
42
+ ```bash
43
+ gem unpack vagrant_utm-x.y.z.gem
44
+ # every shipped applescript should be ASCII; non-ASCII bytes broke 0.1.5 (see #50)
45
+ file vagrant_utm-x.y.z/lib/vagrant_utm/scripts/*.applescript
46
+ ```
33
47
 
34
48
 
35
49
  To update specific gems in the project
@@ -48,3 +62,15 @@ Unable to resolve dependency: user requested 'vagrant_utm (= 0.1.1)'
48
62
 
49
63
  Due to mismatch versions between global installed version and plugin version in the development setup, since they are same name.
50
64
  Fix: Uninstall the global version, while using different version of development setup
65
+
66
+ If `bundle install` fails building grpc
67
+
68
+ ```
69
+ third_party/zlib/zutil.h:147: #define fdopen(fd,mode) NULL
70
+ _stdio.h:320: error: expected identifier or '('
71
+ ```
72
+
73
+ grpc 1.56.2 (pulled in by the pinned vagrant 2.4.1) vendors a zlib that defines `fdopen` to `NULL` on
74
+ macOS, which newer Xcode clang rejects. CI is unaffected because Linux uses a precompiled grpc, so the
75
+ test suite still runs there. Until the vagrant pin moves, `rake` also works without `bundle exec`, which
76
+ is enough to run the release subtasks above.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vagrant_utm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Naveenraj M
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-08-14 00:00:00.000000000 Z
11
+ date: 2026-10-14 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Vagrant UTM provider that allows you to manage UTM virtual machines.
14
14
  email:
@@ -57,7 +57,6 @@ files:
57
57
  - lib/vagrant_utm/action/check_accessible.rb
58
58
  - lib/vagrant_utm/action/check_created.rb
59
59
  - lib/vagrant_utm/action/check_guest_additions.rb
60
- - lib/vagrant_utm/action/check_qemu_img.rb
61
60
  - lib/vagrant_utm/action/check_running.rb
62
61
  - lib/vagrant_utm/action/check_utm.rb
63
62
  - lib/vagrant_utm/action/clear_forwarded_ports.rb
@@ -126,6 +125,7 @@ files:
126
125
  - lib/vagrant_utm/scripts/read_shared_folders_args.js
127
126
  - lib/vagrant_utm/scripts/remove_qemu_additional_args.applescript
128
127
  - lib/vagrant_utm/scripts/set_mac_address.applescript
128
+ - lib/vagrant_utm/scripts/update_network_interface.applescript
129
129
  - lib/vagrant_utm/synced_folder.rb
130
130
  - lib/vagrant_utm/util/compile_forwarded_ports.rb
131
131
  - lib/vagrant_utm/util/unix_mount_helpers.rb
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module VagrantPlugins
4
- module Utm
5
- module Action
6
- # This action checks if qemu-img is installed.
7
- class CheckQemuImg
8
- def initialize(app, _env)
9
- @app = app
10
- end
11
-
12
- def call(env)
13
- qemu_img_present = Vagrant::Util::Which.which("qemu-img")
14
- raise Errors::QemuImgRequired unless qemu_img_present
15
-
16
- @app.call(env)
17
- end
18
- end
19
- end
20
- end
21
- end