Class: VagrantPlugins::QEMU::Driver

Inherits:
Object
  • Object
show all
Defined in:
lib/vagrant-qemu/driver.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(id, dir, tmp) ⇒ Driver

Returns a new instance of Driver.



26
27
28
29
30
31
32
33
# File 'lib/vagrant-qemu/driver.rb', line 26

def initialize(id, dir, tmp)
  @vm_id = id
  @data_dir = dir
  @tmp_dir = tmp.join("vagrant-qemu")
  @attached_drives = {disk: [], floppy: [], dvd: []}
  @ssh_port = nil
  @logger = Log4r::Logger.new("vagrant_qemu::driver")
end

Instance Attribute Details

#attached_drivesObject (readonly)

Returns the value of attribute attached_drives.



22
23
24
# File 'lib/vagrant-qemu/driver.rb', line 22

def attached_drives
  @attached_drives
end

#data_dirObject (readonly)

Returns the value of attribute data_dir.



20
21
22
# File 'lib/vagrant-qemu/driver.rb', line 20

def data_dir
  @data_dir
end

#ssh_portInteger? (readonly)

Returns Runtime SSH port (may differ from config after collision correction).

Returns:

  • (Integer, nil)

    Runtime SSH port (may differ from config after collision correction)



24
25
26
# File 'lib/vagrant-qemu/driver.rb', line 24

def ssh_port
  @ssh_port
end

#tmp_dirObject (readonly)

Returns the value of attribute tmp_dir.



21
22
23
# File 'lib/vagrant-qemu/driver.rb', line 21

def tmp_dir
  @tmp_dir
end

#vm_idString (readonly)

Returns VM ID.

Returns:

  • (String)

    VM ID



19
20
21
# File 'lib/vagrant-qemu/driver.rb', line 19

def vm_id
  @vm_id
end

Instance Method Details

#attach_disk(disk) ⇒ Object



470
471
472
# File 'lib/vagrant-qemu/driver.rb', line 470

def attach_disk(disk)
  @attached_drives[:disk] << disk
end

#attach_dvd(disk) ⇒ Object



466
467
468
# File 'lib/vagrant-qemu/driver.rb', line 466

def attach_dvd(disk)
  @attached_drives[:dvd] << disk
end

#created?Boolean

Returns:

  • (Boolean)


359
360
361
# File 'lib/vagrant-qemu/driver.rb', line 359

def created?
  result = @data_dir.join(@vm_id).directory?
end

#deleteObject



46
47
48
49
50
51
52
53
# File 'lib/vagrant-qemu/driver.rb', line 46

def delete
  if created?
    id_dir = @data_dir.join(@vm_id)
    FileUtils.rm_rf(id_dir)
    id_tmp_dir = @tmp_dir.join(@vm_id)
    FileUtils.rm_rf(id_tmp_dir)
  end
end

#disk_dirObject



474
475
476
# File 'lib/vagrant-qemu/driver.rb', line 474

def disk_dir
    @data_dir.join(@vm_id)
end

#execute(*cmd, **opts, &block) ⇒ Object



375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
# File 'lib/vagrant-qemu/driver.rb', line 375

def execute(*cmd, **opts, &block)
  result = nil
  interrupted = false

  if opts && opts[:detach]
    # give it some time to startup
    timeout = 5

    # edit version of "Subprocess.execute" for detach
    workdir = Dir.pwd
    process = ChildProcess.build(*cmd)

    stdout, stdout_writer = ::IO.pipe
    stderr, stderr_writer = ::IO.pipe
    process.io.stdout = stdout_writer
    process.io.stderr = stderr_writer

    process.leader = true
    process.detach = true

    ::Vagrant::Util::SafeChdir.safe_chdir(workdir) do
      process.start
    end

    if RUBY_PLATFORM != "java"
      stdout_writer.close
      stderr_writer.close
    end

    io_data = { stdout: "", stderr: "" }
    start_time = Time.now.to_i
    open_readers = [stdout, stderr]

    while true
      results = ::IO.select(open_readers, nil, nil, 0.1)
      results ||= []
      readers = results[0]

      # Check if we have exceeded our timeout
      break if (Time.now.to_i - start_time) > timeout

      if readers && !readers.empty?
        readers.each do |r|
          data = ::Vagrant::Util::IO.read_until_block(r)
          next if data.empty?

          io_name = r == stdout ? :stdout : :stderr
          io_data[io_name] += data
        end
      end

      break if process.exited?
    end

    if RUBY_PLATFORM == "java"
      stdout_writer.close
      stderr_writer.close
    end

    exit_code = process.exited? ? process.exit_code : 0
    result = ::Vagrant::Util::Subprocess::Result.new(exit_code, io_data[:stdout], io_data[:stderr])
  else
    # Append in the options for subprocess
    cmd << { notify: [:stdout, :stderr, :stdin] }

    interrupted  = false
    int_callback = ->{ interrupted = true }
    result = ::Vagrant::Util::Busy.busy(int_callback) do
      ::Vagrant::Util::Subprocess.execute(*cmd, &block)
    end
  end

  result.stderr.gsub!("\r\n", "\n")
  result.stdout.gsub!("\r\n", "\n")

  if result.exit_code != 0 && !interrupted
    raise Errors::ExecuteError,
      command: cmd.inspect,
      stderr: result.stderr,
      stdout: result.stdout
  end

  if opts
    if opts[:with_stderr]
      return result.stdout + " " + result.stderr
    else
      return result.stdout
    end
  end
end

#get_current_stateObject



35
36
37
38
39
40
41
42
43
44
# File 'lib/vagrant-qemu/driver.rb', line 35

def get_current_state
  case
  when running?
    :running
  when created?
    :stopped
  else
    :not_created
  end
end

#get_ssh_port(default_port) ⇒ Object



299
300
301
302
303
304
305
306
307
308
309
310
311
# File 'lib/vagrant-qemu/driver.rb', line 299

def get_ssh_port(default_port)
  id_tmp_dir = @tmp_dir.join(@vm_id)
  options_file = id_tmp_dir.join("options.yml")

  port = default_port
  if options_file.file?
    # safe_load + File.read (not safe_load_file) so older Psych works too
    options = YAML.safe_load(File.read(options_file), permitted_classes: [Symbol]) rescue nil
    port = options[:ssh_port] if !options.nil? && options.key?(:ssh_port)
  end

  @ssh_port = port
end

#import(options) ⇒ Object



313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
# File 'lib/vagrant-qemu/driver.rb', line 313

def import(options)
  new_id = "vq_" + SecureRandom.urlsafe_base64(8)

  # Make dir
  id_dir = @data_dir.join(new_id)
  FileUtils.mkdir_p(id_dir)
  id_tmp_dir = @tmp_dir.join(new_id)
  FileUtils.mkdir_p(id_tmp_dir)

  # Prepare firmware
  if options[:arch] == "aarch64" && !options[:firmware_format].nil?
    execute("cp", options[:qemu_dir].join("edk2-aarch64-code.fd").to_s, id_dir.join("edk2-aarch64-code.fd").to_s)
    execute("cp", options[:qemu_dir].join("edk2-arm-vars.fd").to_s, id_dir.join("edk2-arm-vars.fd").to_s)
    execute("chmod", "644", id_dir.join("edk2-arm-vars.fd").to_s)
  end

  # Create image
  options[:image_path].each_with_index do |img, i|
    suffix_index = i > 0 ? "-#{i}" : ''

    linked_image = id_dir.join("linked-box#{suffix_index}.img").to_s
    args = ["create", "-f", "qcow2", "-F", "qcow2", "-b", img.to_s]

    if !options[:extra_image_opts].nil?
      options[:extra_image_opts].each do |opt|
        args.push("-o")
        args.push(opt)
      end
    end

    args.push(linked_image)

    if i == 0
      if !options[:disk_resize].nil?
        args.push(options[:disk_resize])
      end
    end

    execute("qemu-img",  *args)
  end

  server = {
    :id => new_id,
  }
end

#running?Boolean

Returns:

  • (Boolean)


363
364
365
366
367
368
369
370
371
372
373
# File 'lib/vagrant-qemu/driver.rb', line 363

def running?
  pid_file = @tmp_dir.join(@vm_id).join("qemu.pid")
  return false if !pid_file.file?

  begin
    Process.kill(0, File.read(pid_file).to_i)
    true
  rescue Errno::ESRCH
    false
  end
end

#start(options) ⇒ Object



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/vagrant-qemu/driver.rb', line 55

def start(options)
  if !running?
    id_dir = @data_dir.join(@vm_id)

    image_path = Array.new
    image_count = id_dir.glob("linked-box*.img").count
    for i in 0..image_count-1 do
      suffix_index = i > 0 ? "-#{i}" : ''
      image_path.append(id_dir.join("linked-box#{suffix_index}.img").to_s)
    end

    id_tmp_dir = @tmp_dir.join(@vm_id)
    FileUtils.mkdir_p(id_tmp_dir)

    # Persist only the runtime state we need to read back later
    persisted_state = {
      :ssh_port => options[:ssh_port],
      :control_port => options[:control_port],
    }
    options_file = id_tmp_dir.join("options.yml")
    File.write(options_file, persisted_state.to_yaml)

    control_socket = ""
    if !options[:control_port].nil?
      control_socket = "port=#{options[:control_port]},host=localhost,ipv4=on"
    else
      unix_socket_path = id_tmp_dir.join("qemu_socket").to_s
      control_socket = "path=#{unix_socket_path}"
    end

    debug_socket = ""
    if !options[:debug_port].nil?
      debug_socket = "port=#{options[:debug_port]},host=localhost,ipv4=on"
    else
      unix_socket_serial_path = id_tmp_dir.join("qemu_socket_serial").to_s
      debug_socket = "path=#{unix_socket_serial_path}"
    end

    cmd = []
    if options[:qemu_bin].nil?
      cmd += %W(qemu-system-#{options[:arch]})
    else
      if options[:qemu_bin].kind_of?(Array)
        cmd += options[:qemu_bin]
      else
        cmd += %W(#{options[:qemu_bin]})
      end
    end

    # Validate that the QEMU binary exists
    qemu_binary = cmd.first
    if !Vagrant::Util::Which.which(qemu_binary) && !File.executable?(qemu_binary)
      raise Errors::QemuBinaryNotFound, binary: qemu_binary
    end

    # basic
    cmd += %W(-machine #{options[:machine]}) if !options[:machine].nil?
    cmd += %W(-cpu #{options[:cpu]}) if !options[:cpu].nil?
    cmd += %W(-smp #{options[:smp]}) if !options[:smp].nil?
    cmd += %W(-m #{options[:memory]}) if !options[:memory].nil?

    # network
    if !options[:net_device].nil?
      private_networks = options[:private_networks] || []
      use_advanced = options[:advanced_network] && !private_networks.empty?

      if use_advanced
        # Dual-NIC: NIC 0 = user-mode (SSH + port forwarding), NIC 1 = advanced backend
        pn = private_networks.first
        mac0, mac1 = Network.nic_macs(@vm_id, pn)

        # NIC 0: user-mode
        cmd += %W(-device #{options[:net_device]},netdev=net0,mac=#{mac0})
        hostfwd = "hostfwd=tcp::#{options[:ssh_port]}-:22"
        options[:ports].each do |v|
          hostfwd += ",hostfwd=#{v}"
        end
        extra_netdev = ""
        if !options[:extra_netdev_args].nil?
          extra_netdev = ",#{options[:extra_netdev_args]}"
        end
        cmd += %W(-netdev user,id=net0,#{hostfwd}#{extra_netdev})

        # NIC 1: platform-specific backend
        # (the static-IP cloud-init seed is built and attached by the
        # CloudInitNetwork action, not here)
        backend = Network.backend_for(options[:net_mode])
        cmd += %W(-device #{options[:net_device]},netdev=net1,mac=#{mac1})
        cmd += backend.build_netdev_args("net1", options)
      else
        # Single NIC: user-mode only (original behavior, no cloud-init)
        cmd += %W(-device #{options[:net_device]},netdev=net0)

        hostfwd = "hostfwd=tcp::#{options[:ssh_port]}-:22"
        options[:ports].each do |v|
          hostfwd += ",hostfwd=#{v}"
        end
        extra_netdev = ""
        if !options[:extra_netdev_args].nil?
          extra_netdev = ",#{options[:extra_netdev_args]}"
        end
        cmd += %W(-netdev user,id=net0,#{hostfwd}#{extra_netdev})
      end
    end

    # drive
    diskid = 0
    extra_drive_args = ""
    if !options[:extra_drive_args].nil?
      extra_drive_args = ",#{options[:extra_drive_args]}"
    end

    if !options[:drive_interface].nil?
      image_path.each do |img|
        cmd += %W(-drive if=#{options[:drive_interface]},id=disk#{diskid},format=qcow2,file=#{img}#{extra_drive_args})
        diskid += 1
      end
    end
    if options[:arch] == "aarch64" && !options[:firmware_format].nil?
      fm1_path = id_dir.join("edk2-aarch64-code.fd").to_s
      fm2_path = id_dir.join("edk2-arm-vars.fd").to_s
      cmd += %W(-drive if=pflash,format=#{options[:firmware_format]},file=#{fm1_path},readonly=on)
      cmd += %W(-drive if=pflash,format=#{options[:firmware_format]},file=#{fm2_path})
    end

    dvd_index = 1
    @attached_drives[:dvd].each do |disk|
      cmd += %W(-drive file=#{disk[:Path]},index=#{dvd_index},media=cdrom)
      dvd_index += 1
    end
    if !options[:drive_interface].nil?
      @attached_drives[:disk].each do |disk|
        cmd += %W(-drive if=#{options[:drive_interface]},id=disk#{diskid},format=qcow2,file=#{disk[:Path]}#{extra_drive_args})
        diskid += 1
      end
    end

    # control
    pid_file = id_tmp_dir.join("qemu.pid").to_s
    cmd += %W(-chardev socket,id=mon0,#{control_socket},server=on,wait=off)
    cmd += %W(-mon chardev=mon0,mode=readline)
    cmd += %W(-chardev socket,id=ser0,#{debug_socket},server=on,wait=off)
    cmd += %W(-serial chardev:ser0)
    cmd += %W(-pidfile #{pid_file})
    if !options[:no_daemonize]
      cmd += %W(-daemonize)
    end

    # other default
    cmd += options[:other_default]

    # user-defined
    cmd += options[:extra_qemu_args]

    opts = {:detach => options[:no_daemonize]}
    execute(*cmd, **opts)
  end
end

#stop(options) ⇒ Object



214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
# File 'lib/vagrant-qemu/driver.rb', line 214

def stop(options)
  return unless running?

  opts = with_persisted_control_port(options)
  timeout = options[:graceful_timeout] || 60

  # 1. ACPI power button (graceful). Only works if a live guest OS is
  #    there to act on it -- e.g. NOT after `systemctl halt`, which leaves
  #    QEMU running with a halted, unresponsive guest.
  send_monitor(opts, "system_powerdown")
  return unless still_running_after?(timeout)

  # 2. Powerdown didn't take. Ask QEMU itself to quit: it stops the VM,
  #    flushes and closes the qcow2 images, then exits cleanly. Does not
  #    depend on the guest, only on the monitor being reachable.
  @logger.warn("VM did not power down within #{timeout}s; sending 'quit' to QEMU")
  send_monitor(opts, "quit")
  return unless still_running_after?(timeout)

  # 3. Last resort: SIGKILL the QEMU process (no flush/cleanup).
  @logger.warn("VM still running after 'quit'; forcing kill")
  force_kill
end