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



451
452
453
# File 'lib/vagrant-qemu/driver.rb', line 451

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

#attach_dvd(disk) ⇒ Object



447
448
449
# File 'lib/vagrant-qemu/driver.rb', line 447

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

#created?Boolean

Returns:

  • (Boolean)


340
341
342
# File 'lib/vagrant-qemu/driver.rb', line 340

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



455
456
457
# File 'lib/vagrant-qemu/driver.rb', line 455

def disk_dir
    @data_dir.join(@vm_id)
end

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



356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
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
# File 'lib/vagrant-qemu/driver.rb', line 356

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



280
281
282
283
284
285
286
287
288
289
290
291
292
# File 'lib/vagrant-qemu/driver.rb', line 280

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



294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
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
# File 'lib/vagrant-qemu/driver.rb', line 294

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)


344
345
346
347
348
349
350
351
352
353
354
# File 'lib/vagrant-qemu/driver.rb', line 344

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
# File 'lib/vagrant-qemu/driver.rb', line 214

def stop(options)
  if running?
    send_powerdown(with_persisted_control_port(options))
    wait_for_shutdown(options[:graceful_timeout] || 60)
  end
end