Class: VagrantPlugins::Utm::Driver::Version_4_5

Inherits:
Base
  • Object
show all
Defined in:
lib/vagrant_utm/driver/version_4_5.rb

Overview

Driver for UTM 4.5.x

Direct Known Subclasses

Version_4_6

Instance Method Summary collapse

Methods inherited from Base

#execute, #execute_shell, #random_mac_address, #raw, #raw_shell

Constructor Details

#initialize(uuid) ⇒ Version_4_5

rubocop:disable Naming/ClassAndModuleCamelCase,Metrics/ClassLength



14
15
16
17
18
19
# File 'lib/vagrant_utm/driver/version_4_5.rb', line 14

def initialize(uuid)
  super()

  @logger = Log4r::Logger.new("vagrant::provider::utm::version_4_5")
  @uuid = uuid
end

Instance Method Details

#check_qemu_guest_agentObject



31
32
33
34
35
36
37
38
39
# File 'lib/vagrant_utm/driver/version_4_5.rb', line 31

def check_qemu_guest_agent
  # Check if the qemu-guest-agent is installed and running
  # Ideally do: utmctl exec id --cmd systemctl is-active qemu-guest-agent
  # But this is not returning anything, so we just do any utmctl exec command
  # Here we check if the user is root
  output = execute("exec", @uuid, "--cmd", "whoami")
  # check if output contains 'root'
  output.include?("root")
end

#clear_forwarded_portsObject



21
22
23
24
25
26
27
28
29
# File 'lib/vagrant_utm/driver/version_4_5.rb', line 21

def clear_forwarded_ports
  args = []
  read_forwarded_ports(@uuid).each do |nic, name, _, _|
    args.concat(["--index", nic.to_s, name])
  end

  command = ["clear_port_forwards.applescript", @uuid] + args
  execute_osa_script(command) unless args.empty?
end

#create_snapshot(machine_id, snapshot_name) ⇒ Object



41
42
43
44
45
46
# File 'lib/vagrant_utm/driver/version_4_5.rb', line 41

def create_snapshot(machine_id, snapshot_name)
  list_result = list
  machine_name = list_result.find(uuid: machine_id).name
  machine_file = get_vm_file(machine_name)
  execute_shell("qemu-img", "snapshot", "-c", snapshot_name, machine_file)
end

#deleteObject



257
258
259
# File 'lib/vagrant_utm/driver/version_4_5.rb', line 257

def delete
  execute("delete", @uuid)
end

#delete_snapshot(machine_id, snapshot_name) ⇒ Object



48
49
50
51
52
53
# File 'lib/vagrant_utm/driver/version_4_5.rb', line 48

def delete_snapshot(machine_id, snapshot_name)
  list_result = list
  machine_name = list_result.find(uuid: machine_id).name
  machine_file = get_vm_file(machine_name)
  execute_shell("qemu-img", "snapshot", "-d", snapshot_name, machine_file)
end

#execute_osa_script(command) ⇒ Object



277
278
279
280
281
# File 'lib/vagrant_utm/driver/version_4_5.rb', line 277

def execute_osa_script(command)
  script_path = @script_path.join(command[0])
  cmd = ["osascript", script_path.to_s] + command[1..]
  execute_shell(*cmd)
end

#export(_path) ⇒ Object



55
56
57
58
# File 'lib/vagrant_utm/driver/version_4_5.rb', line 55

def export(_path)
  @logger.info("This version of UTM does not support exporting VMs
    Please upgrade to the latest version of UTM or UTM 'Share' feature in UI to export the virtual machine")
end

#forward_ports(ports) ⇒ Object

rubocop:disable Metrics/CyclomaticComplexity



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/vagrant_utm/driver/version_4_5.rb', line 85

def forward_ports(ports) # rubocop:disable Metrics/CyclomaticComplexity
  args = []
  ports.each do |options|
    # Convert to UTM protcol enum
    protocol_code = case options[:protocol]
                    when "tcp"
                      "TcPp"
                    when "udp"
                      "UdPp"
                    else
                      raise Errors::ForwardedPortInvalidProtocol
                    end

    pf_builder = [
      # options[:name], # Name is not supported in UTM
      protocol_code || "TcPp", # Default to TCP
      options[:guestip] || "",
      options[:guestport],
      options[:hostip] || "",
      options[:hostport]
    ]

    args.concat(["--index", options[:adapter].to_s,
                 pf_builder.join(",")])
  end

  command = ["add_port_forwards.applescript", @uuid] + args
  execute_osa_script(command) unless args.empty?
end

#get_vm_file(vm_name) ⇒ Object



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/vagrant_utm/driver/version_4_5.rb', line 115

def get_vm_file(vm_name)
  # Get the current username
  username = ENV["USER"] || Etc.getlogin

  data_path = "/Users/#{username}/Library/Containers/com.utmapp.UTM/Data/Documents/#{vm_name}.utm/Data"
  # Define the regex for UUID pattern
  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$/

  # List the files in the directory and filter by regex
  matching_files = Dir.entries(data_path).select do |file|
    file.match?(pattern)
  end

  if matching_files.length == 1
    # If there is exactly one matching file, return full path to file
    "#{data_path}/#{matching_files[0]}"
  elsif matching_files.length > 1
    # If there are multiple matching files, raise an error
    raise Errors::SnapShotMultipleVMFiles, directory: data_path, files: matching_files
  else
    # If there are no matching files, raise an error
    raise Errors::SnapShotVMFileNotFound, directory: data_path
  end
end

#haltObject



269
270
271
# File 'lib/vagrant_utm/driver/version_4_5.rb', line 269

def halt
  execute("stop", @uuid)
end

#import(utm_file_url) ⇒ uuid

Execute the ‘utm://downloadVM?url=’ See docs.getutm.app/advanced/remote-control/

Parameters:

  • utm_file_url (String)

    The url to the UTM file.

Returns:

  • (uuid)

    The UUID of the imported machine.



297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
# File 'lib/vagrant_utm/driver/version_4_5.rb', line 297

def import(utm_file_url)
  script_path = @script_path.join("downloadVM.sh")
  cmd = [script_path.to_s, utm_file_url]
  execute_shell(*cmd)
  # wait for the VM to be imported
  # TODO: UTM API to give the progress of the import
  # along with the UUID of the imported VM
  # sleep(60)
  # Get the UUID of the imported VM
  # HACK: Currently we do not know the UUID of the imported VM
  # So, we just get the UUID of the last VM in the list
  # which is the last imported VM (unless UTM changes the order)
  # TODO: Use UTM API to get the UUID of the imported VM
  # last_uuid
end

#last_uuiduuid

Return UUID of the last VM in the list.

Returns:

  • (uuid)

    The UUID of the VM.



315
316
317
318
# File 'lib/vagrant_utm/driver/version_4_5.rb', line 315

def last_uuid
  list_result = list
  list_result.last.uuid
end

#listListResult

Execute the ‘list’ command and returns the list of machines.

Returns:

  • (ListResult)

    The list of machines.



285
286
287
288
289
290
291
# File 'lib/vagrant_utm/driver/version_4_5.rb', line 285

def list
  script_path = @script_path.join("list_vm.js")
  cmd = ["osascript", script_path.to_s]
  result = execute_shell(*cmd)
  data = JSON.parse(result)
  Model::ListResult.new(data)
end

#list_snapshots(machine_id) ⇒ Object

rubocop:disable Metrics/AbcSize



60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/vagrant_utm/driver/version_4_5.rb', line 60

def list_snapshots(machine_id) # rubocop:disable Metrics/AbcSize
  list_result = list
  machine_name = list_result.find(uuid: machine_id).name
  machine_file = get_vm_file(machine_name)
  output = execute_shell("qemu-img", "snapshot", "-l", machine_file)

  return [] if output.nil? || output.strip.empty?

  @logger.debug("list_snapshots_here: #{output}")

  result = []
  output.split("\n").map do |line|
    result << ::Regexp.last_match(1).to_s if line =~ /^\d+\s+(\w+)/
  end

  result.sort
end

#read_forwarded_ports(uuid = nil) ⇒ Object



146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/vagrant_utm/driver/version_4_5.rb', line 146

def read_forwarded_ports(uuid = nil)
  uuid ||= @uuid

  @logger.debug("read_forward_ports: uuid=#{uuid}")

  # If we care about active VMs only, then we check the state
  # to verify the VM is running.
  # This is taken care by the caller , read used ports
  # return [] if active_only && check_state != :started

  # Get the forwarded ports from emulated Network interface
  # Format: [nicIndex, name, hostPort, guestPort]
  # We use hostPort as the name, since UTM does not support name
  # Because hostport is and should be unique
  results = []
  command = ["read_forwarded_ports.applescript", uuid]
  info = execute_osa_script(command)
  info.split("\n").each do |line|
    # Parse info, Forwarding(nicIndex)(ruleIndex)="Protocol,GuestIP,GuestPort,HostIP,HostPort"
    next unless (matcher = /^Forwarding\((\d+)\)\((\d+)\)="(.+?),.*?,(.+?),.*?,(.+?)"$/.match(line))

    #        nicIndex         name( our hostPort)   hostport        guestport
    result = [matcher[1].to_i, matcher[5], matcher[5].to_i, matcher[4].to_i]
    @logger.debug("  - #{result.inspect}")
    results << result
  end

  results
end

#read_guest_ipObject



176
177
178
179
# File 'lib/vagrant_utm/driver/version_4_5.rb', line 176

def read_guest_ip
  output = execute("ip-address", @uuid)
  output.strip.split("\n")
end

#read_network_interfacesObject



181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/vagrant_utm/driver/version_4_5.rb', line 181

def read_network_interfaces
  nics = {}
  command = ["read_network_interfaces.applescript", @uuid]
  info = execute_osa_script(command)
  info.split("\n").each do |line|
    next unless (matcher = /^nic(\d+),(.+?)$/.match(line))

    adapter = matcher[1].to_i
    type = matcher[2].to_sym
    nics[adapter] ||= {}
    nics[adapter][:type] = type
  end

  nics
end

#read_stateObject

virtualbox plugin style



217
218
219
220
# File 'lib/vagrant_utm/driver/version_4_5.rb', line 217

def read_state
  output = execute("status", @uuid)
  output.strip.to_sym
end

#read_used_ports(active_only = true) ⇒ Object

We handle the active only case here So we can avoid calling the utmctl status command for each VM



225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/vagrant_utm/driver/version_4_5.rb', line 225

def read_used_ports(active_only = true) # rubocop:disable Style/OptionalBooleanParameter
  @logger.debug("read_used_ports: active_only=#{active_only}")
  ports = []
  list.machines.each do |machine|
    # Ignore our own used ports
    next if machine.uuid == @uuid
    # Ignore inactive VMs if we only care about active VMs
    next if active_only && machine.state != :started

    read_forwarded_ports(machine.uuid).each do |_, _, hostport, _|
      ports << hostport
    end
  end

  ports
end

#read_vmsObject



242
243
244
245
246
247
248
249
250
# File 'lib/vagrant_utm/driver/version_4_5.rb', line 242

def read_vms
  results = {}
  list.machines.each do |machine|
    # Store UUID (Unique) as key and name as value
    results[machine.uuid] = machine.name
  end

  results
end

#restore_snapshot(machine_id, snapshot_name) ⇒ Object



78
79
80
81
82
83
# File 'lib/vagrant_utm/driver/version_4_5.rb', line 78

def restore_snapshot(machine_id, snapshot_name)
  list_result = list
  machine_name = list_result.find(uuid: machine_id).name
  machine_file = get_vm_file(machine_name)
  execute_shell("qemu-img", "snapshot", "-a", snapshot_name, machine_file)
end

#set_mac_address(mac) ⇒ Object

rubocop:disable Naming/AccessorMethodName



197
198
199
200
201
202
203
# File 'lib/vagrant_utm/driver/version_4_5.rb', line 197

def set_mac_address(mac) # rubocop:disable Naming/AccessorMethodName
  # Set the MAC address of the first NIC (index 0)
  # Set MAC address to given value or randomize it if nil
  mac = random_mac_address if mac.nil?
  command = ["set_mac_address.applescript", @uuid, "0", mac]
  execute_osa_script(command)
end

#set_name(name) ⇒ Object

rubocop:disable Naming/AccessorMethodName



252
253
254
255
# File 'lib/vagrant_utm/driver/version_4_5.rb', line 252

def set_name(name) # rubocop:disable Naming/AccessorMethodName
  command = ["customize_vm.applescript", @uuid, "--name", name.to_s]
  execute_osa_script(command)
end

#ssh_port(expected_port) ⇒ Object



205
206
207
208
209
210
211
212
213
214
# File 'lib/vagrant_utm/driver/version_4_5.rb', line 205

def ssh_port(expected_port)
  @logger.debug("Searching for SSH port: #{expected_port.inspect}")

  # Look for the forwarded port only by comparing the guest port
  read_forwarded_ports.each do |_, _, hostport, guestport|
    return hostport if guestport == expected_port
  end

  nil
end

#startObject



261
262
263
# File 'lib/vagrant_utm/driver/version_4_5.rb', line 261

def start
  execute("start", @uuid)
end

#start_disposableObject



265
266
267
# File 'lib/vagrant_utm/driver/version_4_5.rb', line 265

def start_disposable
  execute("start", @uuid, "--disposable")
end

#suspendObject



273
274
275
# File 'lib/vagrant_utm/driver/version_4_5.rb', line 273

def suspend
  execute("suspend", @uuid)
end

#verify!Object



320
321
322
323
324
325
# File 'lib/vagrant_utm/driver/version_4_5.rb', line 320

def verify!
  # Verify proper functionality of UTM
  # add any command that should be checked
  # we now only check if the 'utmctl' command is available
  execute("--list")
end

#vm_exists?(uuid) ⇒ Boolean

Check if the VM with the given UUID exists.

Returns:

  • (Boolean)


141
142
143
144
# File 'lib/vagrant_utm/driver/version_4_5.rb', line 141

def vm_exists?(uuid)
  list_result = list
  list_result.any?(uuid)
end