vagrant-libvirt 0.4.0 → 0.5.3

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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +251 -33
  3. data/lib/vagrant-libvirt/action.rb +7 -1
  4. data/lib/vagrant-libvirt/action/clean_machine_folder.rb +30 -0
  5. data/lib/vagrant-libvirt/action/create_domain.rb +28 -11
  6. data/lib/vagrant-libvirt/action/create_domain_volume.rb +57 -55
  7. data/lib/vagrant-libvirt/action/create_network_interfaces.rb +0 -3
  8. data/lib/vagrant-libvirt/action/create_networks.rb +11 -4
  9. data/lib/vagrant-libvirt/action/destroy_domain.rb +1 -1
  10. data/lib/vagrant-libvirt/action/forward_ports.rb +36 -37
  11. data/lib/vagrant-libvirt/action/halt_domain.rb +25 -9
  12. data/lib/vagrant-libvirt/action/handle_box_image.rb +170 -77
  13. data/lib/vagrant-libvirt/action/is_running.rb +1 -3
  14. data/lib/vagrant-libvirt/action/is_suspended.rb +4 -4
  15. data/lib/vagrant-libvirt/action/set_boot_order.rb +6 -2
  16. data/lib/vagrant-libvirt/action/wait_till_up.rb +1 -25
  17. data/lib/vagrant-libvirt/cap/{mount_p9.rb → mount_9p.rb} +2 -2
  18. data/lib/vagrant-libvirt/cap/mount_virtiofs.rb +37 -0
  19. data/lib/vagrant-libvirt/cap/{synced_folder.rb → synced_folder_9p.rb} +4 -5
  20. data/lib/vagrant-libvirt/cap/synced_folder_virtiofs.rb +109 -0
  21. data/lib/vagrant-libvirt/config.rb +34 -2
  22. data/lib/vagrant-libvirt/driver.rb +3 -1
  23. data/lib/vagrant-libvirt/errors.rb +24 -1
  24. data/lib/vagrant-libvirt/plugin.rb +14 -5
  25. data/lib/vagrant-libvirt/templates/domain.xml.erb +7 -6
  26. data/lib/vagrant-libvirt/templates/private_network.xml.erb +1 -1
  27. data/lib/vagrant-libvirt/util/byte_number.rb +71 -0
  28. data/lib/vagrant-libvirt/util/network_util.rb +21 -3
  29. data/lib/vagrant-libvirt/version +1 -1
  30. data/locales/en.yml +12 -0
  31. data/spec/spec_helper.rb +9 -1
  32. data/spec/support/binding_proc.rb +24 -0
  33. data/spec/support/matchers/have_file_content.rb +63 -0
  34. data/spec/unit/action/clean_machine_folder_spec.rb +58 -0
  35. data/spec/unit/action/create_domain_spec.rb +15 -5
  36. data/spec/unit/action/create_domain_spec/additional_disks_domain.xml +54 -0
  37. data/spec/unit/action/create_domain_spec/default_domain.xml +49 -0
  38. data/spec/unit/action/create_domain_volume_spec.rb +104 -0
  39. data/spec/unit/action/create_domain_volume_spec/one_disk_in_storage.xml +21 -0
  40. data/spec/unit/action/create_domain_volume_spec/three_disks_in_storage_disk_0.xml +21 -0
  41. data/spec/unit/action/create_domain_volume_spec/three_disks_in_storage_disk_1.xml +21 -0
  42. data/spec/unit/action/create_domain_volume_spec/three_disks_in_storage_disk_2.xml +21 -0
  43. data/spec/unit/action/destroy_domain_spec.rb +1 -1
  44. data/spec/unit/action/forward_ports_spec.rb +202 -0
  45. data/spec/unit/action/halt_domain_spec.rb +90 -0
  46. data/spec/unit/action/handle_box_image_spec.rb +441 -0
  47. data/spec/unit/action/wait_till_up_spec.rb +11 -15
  48. data/spec/unit/config_spec.rb +12 -9
  49. data/spec/unit/templates/domain_all_settings.xml +8 -0
  50. data/spec/unit/templates/domain_spec.rb +20 -1
  51. data/spec/unit/util/byte_number_spec.rb +26 -0
  52. metadata +52 -18
@@ -13,24 +13,40 @@ module VagrantPlugins
13
13
  def call(env)
14
14
  env[:ui].info(I18n.t('vagrant_libvirt.halt_domain'))
15
15
 
16
+ timeout = env[:machine].config.vm.graceful_halt_timeout
16
17
  domain = env[:machine].provider.driver.connection.servers.get(env[:machine].id.to_s)
17
18
  raise Errors::NoDomainError if domain.nil?
18
19
 
19
- begin
20
- env[:machine].guest.capability(:halt)
21
- rescue
22
- @logger.info('Trying Libvirt graceful shutdown.')
23
- domain.shutdown
20
+ if env[:force_halt]
21
+ domain.poweroff
22
+ return @app.call(env)
24
23
  end
25
24
 
26
-
27
25
  begin
28
- domain.wait_for(30) do
29
- !ready?
26
+ Timeout.timeout(timeout) do
27
+ begin
28
+ env[:machine].guest.capability(:halt)
29
+ rescue Timeout::Error
30
+ raise
31
+ rescue
32
+ @logger.info('Trying Libvirt graceful shutdown.')
33
+ # Read domain object again
34
+ dom = env[:machine].provider.driver.connection.servers.get(env[:machine].id.to_s)
35
+ if dom.state.to_s == 'running'
36
+ dom.shutdown
37
+ end
38
+ end
39
+
40
+ domain.wait_for(timeout) do
41
+ !ready?
42
+ end
30
43
  end
31
- rescue Fog::Errors::TimeoutError
44
+ rescue Timeout::Error
32
45
  @logger.info('VM is still running. Calling force poweroff.')
33
46
  domain.poweroff
47
+ rescue
48
+ @logger.error('Failed to shutdown cleanly. Calling force poweroff.')
49
+ domain.poweroff
34
50
  end
35
51
 
36
52
  @app.call(env)
@@ -1,4 +1,8 @@
1
1
  require 'log4r'
2
+ require 'open3'
3
+ require 'json'
4
+
5
+ require 'vagrant-libvirt/util/byte_number'
2
6
 
3
7
  module VagrantPlugins
4
8
  module ProviderLibvirt
@@ -16,118 +20,207 @@ module VagrantPlugins
16
20
  end
17
21
 
18
22
  def call(env)
19
- # Verify box metadata for mandatory values.
20
- #
21
- # Virtual size has to be set for allocating space in storage pool.
22
- box_virtual_size = env[:machine].box.metadata['virtual_size']
23
- raise Errors::NoBoxVirtualSizeSet if box_virtual_size.nil?
23
+ # Handle box formats converting between v1 => v2 and ensuring
24
+ # any obsolete settings are rejected.
24
25
 
25
- # Support qcow2 format only for now, but other formats with backing
26
- # store capability should be usable.
27
- box_format = env[:machine].box.metadata['format']
28
- if box_format.nil?
29
- raise Errors::NoBoxFormatSet
30
- elsif box_format != 'qcow2'
31
- raise Errors::WrongBoxFormatSet
26
+ disks = env[:machine].box.metadata.fetch('disks', [])
27
+ if disks.empty?
28
+ # Handle box v1 format
29
+
30
+ # Only qcow2 format is supported in v1, but other formats with backing
31
+ # store capability should be usable.
32
+ box_format = env[:machine].box.metadata['format']
33
+ HandleBoxImage.verify_box_format(box_format)
34
+
35
+ env[:box_volume_number] = 1
36
+ env[:box_volumes] = [{
37
+ :path => HandleBoxImage.get_box_image_path(env[:machine].box, 'box.img'),
38
+ :name => HandleBoxImage.get_volume_name(env[:machine].box, 'box'),
39
+ :virtual_size => HandleBoxImage.get_virtual_size(env),
40
+ :format => box_format,
41
+ }]
42
+ else
43
+ # Handle box v2 format
44
+ # {
45
+ # 'path': '<path-of-file-box>',
46
+ # 'name': '<name-to-use-in-storage>' # optional, will use index
47
+ # }
48
+ #
49
+ env[:box_volume_number] = disks.length()
50
+ target_volumes = Hash[]
51
+ env[:box_volumes] = Array.new(env[:box_volume_number]) { |i|
52
+ raise Errors::BoxFormatMissingAttribute, attribute: "disks[#{i}]['path']" if disks[i]['path'].nil?
53
+
54
+ image_path = HandleBoxImage.get_box_image_path(env[:machine].box, disks[i]['path'])
55
+ format, virtual_size = HandleBoxImage.get_box_disk_settings(image_path)
56
+ volume_name = HandleBoxImage.get_volume_name(
57
+ env[:machine].box,
58
+ disks[i].fetch('name', disks[i]['path'].sub(/#{File.extname(disks[i]['path'])}$/, '')),
59
+ )
60
+
61
+ # allowing name means needing to check that it doesn't cause a clash
62
+ existing = target_volumes[volume_name]
63
+ if !existing.nil?
64
+ raise Errors::BoxFormatDuplicateVolume, volume: volume_name, new_disk: "disks[#{i}]", orig_disk: "disks[#{existing}]"
65
+ end
66
+ target_volumes[volume_name] = i
67
+
68
+ {
69
+ :path => image_path,
70
+ :name => volume_name,
71
+ :virtual_size => virtual_size,
72
+ :format => HandleBoxImage.verify_box_format(format)
73
+ }
74
+ }
32
75
  end
33
76
 
34
77
  # Get config options
35
78
  config = env[:machine].provider_config
36
- box_image_file = env[:machine].box.directory.join('box.img').to_s
37
- env[:box_volume_name] = env[:machine].box.name.to_s.dup.gsub('/', '-VAGRANTSLASH-')
38
- env[:box_volume_name] << "_vagrant_box_image_#{
39
- begin
40
- env[:machine].box.version.to_s
41
- rescue
42
- ''
43
- end}.img"
79
+ box_image_files = []
80
+ env[:box_volumes].each do |d|
81
+ box_image_files.push(d[:path])
82
+ end
44
83
 
45
84
  # Override box_virtual_size
85
+ box_virtual_size = env[:box_volumes][0][:virtual_size]
46
86
  if config.machine_virtual_size
47
- if config.machine_virtual_size < box_virtual_size
87
+ config_machine_virtual_size = ByteNumber.from_GB(config.machine_virtual_size)
88
+ puts config_machine_virtual_size < box_virtual_size
89
+ if config_machine_virtual_size < box_virtual_size
48
90
  # Warn that a virtual size less than the box metadata size
49
91
  # is not supported and will be ignored
50
92
  env[:ui].warn I18n.t(
51
93
  'vagrant_libvirt.warnings.ignoring_virtual_size_too_small',
52
- requested: config.machine_virtual_size, minimum: box_virtual_size
94
+ requested: config_machine_virtual_size.to_GB, minimum: box_virtual_size.to_GB
53
95
  )
54
96
  else
55
97
  env[:ui].info I18n.t('vagrant_libvirt.manual_resize_required')
56
- box_virtual_size = config.machine_virtual_size
98
+ box_virtual_size = config_machine_virtual_size
57
99
  end
58
100
  end
59
101
  # save for use by later actions
60
- env[:box_virtual_size] = box_virtual_size
102
+ env[:box_volumes][0][:virtual_size] = box_virtual_size
61
103
 
62
104
  # while inside the synchronize block take care not to call the next
63
105
  # action in the chain, as must exit this block first to prevent
64
106
  # locking all subsequent actions as well.
65
107
  @@lock.synchronize do
66
- # Don't continue if image already exists in storage pool.
67
- box_volume = env[:machine].provider.driver.connection.volumes.all(
68
- name: env[:box_volume_name]
69
- ).first
70
- break if box_volume && box_volume.id
71
-
72
- # Box is not available as a storage pool volume. Create and upload
73
- # it as a copy of local box image.
74
- env[:ui].info(I18n.t('vagrant_libvirt.uploading_volume'))
75
-
76
- # Create new volume in storage pool
77
- unless File.exist?(box_image_file)
78
- raise Vagrant::Errors::BoxNotFound, name: env[:machine].box.name
108
+ env[:box_volumes].each_index do |i|
109
+ # Don't continue if image already exists in storage pool.
110
+ box_volume = env[:machine].provider.driver.connection.volumes.all(
111
+ name: env[:box_volumes][i][:name]
112
+ ).first
113
+ next if box_volume && box_volume.id
114
+
115
+ send_box_image(env, config, box_image_files[i], env[:box_volumes][i])
79
116
  end
80
- box_image_size = File.size(box_image_file) # B
81
- message = "Creating volume #{env[:box_volume_name]}"
82
- message << " in storage pool #{config.storage_pool_name}."
83
- @logger.info(message)
117
+ end
84
118
 
85
- @storage_volume_uid = storage_uid env
86
- @storage_volume_gid = storage_gid env
119
+ @app.call(env)
120
+ end
121
+
122
+ protected
87
123
 
124
+ def self.get_volume_name(box, name)
125
+ vol_name = box.name.to_s.dup.gsub('/', '-VAGRANTSLASH-')
126
+ vol_name << "_vagrant_box_image_#{
88
127
  begin
89
- fog_volume = env[:machine].provider.driver.connection.volumes.create(
90
- name: env[:box_volume_name],
91
- allocation: "#{box_image_size / 1024 / 1024}M",
92
- capacity: "#{box_virtual_size}G",
93
- format_type: box_format,
94
- owner: @storage_volume_uid,
95
- group: @storage_volume_gid,
96
- pool_name: config.storage_pool_name
97
- )
98
- rescue Fog::Errors::Error => e
99
- raise Errors::FogCreateVolumeError,
100
- error_message: e.message
128
+ box.version.to_s
129
+ rescue
130
+ ''
101
131
  end
132
+ }_#{name.dup.gsub('/', '-SLASH-')}.img"
133
+ end
102
134
 
103
- # Upload box image to storage pool
104
- ret = upload_image(box_image_file, config.storage_pool_name,
105
- env[:box_volume_name], env) do |progress|
106
- rewriting(env[:ui]) do |ui|
107
- ui.clear_line
108
- ui.report_progress(progress, box_image_size, false)
109
- end
110
- end
135
+ def self.get_virtual_size(env)
136
+ # Virtual size has to be set for allocating space in storage pool.
137
+ box_virtual_size = env[:machine].box.metadata['virtual_size']
138
+ raise Errors::NoBoxVirtualSizeSet if box_virtual_size.nil?
139
+ return ByteNumber.from_GB(box_virtual_size)
140
+ end
111
141
 
112
- # Clear the line one last time since the progress meter doesn't
113
- # disappear immediately.
114
- rewriting(env[:ui]) {|ui| ui.clear_line}
115
-
116
- # If upload failed or was interrupted, remove created volume from
117
- # storage pool.
118
- if env[:interrupted] || !ret
119
- begin
120
- fog_volume.destroy
121
- rescue
122
- nil
123
- end
142
+ def self.get_box_image_path(box, box_name)
143
+ return box.directory.join(box_name).to_s
144
+ end
145
+
146
+ def self.verify_box_format(box_format, disk_index=nil)
147
+ if box_format.nil?
148
+ raise Errors::NoBoxFormatSet
149
+ elsif box_format != 'qcow2'
150
+ if disk_index.nil?
151
+ raise Errors::WrongBoxFormatSet
152
+ else
153
+ raise Errors::WrongDiskFormatSet,
154
+ disk_index: disk_index
124
155
  end
125
156
  end
157
+ return box_format
158
+ end
126
159
 
127
- @app.call(env)
160
+ def self.get_box_disk_settings(image_path)
161
+ stdout, stderr, status = Open3.capture3('qemu-img', 'info', '--output=json', image_path)
162
+ if !status.success?
163
+ raise Errors::BadBoxImage, image: image_path, out: stdout, err: stderr
164
+ end
165
+
166
+ image_info = JSON.parse(stdout)
167
+ format = image_info['format']
168
+ virtual_size = ByteNumber.new(image_info['virtual-size'])
169
+
170
+ return format, virtual_size
128
171
  end
129
172
 
130
- protected
173
+ def send_box_image(env, config, box_image_file, box_volume)
174
+ # Box is not available as a storage pool volume. Create and upload
175
+ # it as a copy of local box image.
176
+ env[:ui].info(I18n.t('vagrant_libvirt.uploading_volume'))
177
+
178
+ # Create new volume in storage pool
179
+ unless File.exist?(box_image_file)
180
+ raise Vagrant::Errors::BoxNotFound, name: env[:machine].box.name
181
+ end
182
+ box_image_size = File.size(box_image_file) # B
183
+ message = "Creating volume #{box_volume[:name]}"
184
+ message << " in storage pool #{config.storage_pool_name}."
185
+ @logger.info(message)
186
+ begin
187
+ fog_volume = env[:machine].provider.driver.connection.volumes.create(
188
+ name: box_volume[:name],
189
+ allocation: "#{box_image_size / 1024 / 1024}M",
190
+ capacity: "#{box_volume[:virtual_size].to_B}B",
191
+ format_type: box_volume[:format],
192
+ owner: storage_uid(env),
193
+ group: storage_gid(env),
194
+ pool_name: config.storage_pool_name
195
+ )
196
+ rescue Fog::Errors::Error => e
197
+ raise Errors::FogCreateVolumeError,
198
+ error_message: e.message
199
+ end
200
+
201
+ # Upload box image to storage pool
202
+ ret = upload_image(box_image_file, config.storage_pool_name,
203
+ box_volume[:name], env) do |progress|
204
+ rewriting(env[:ui]) do |ui|
205
+ ui.clear_line
206
+ ui.report_progress(progress, box_image_size, false)
207
+ end
208
+ end
209
+
210
+ # Clear the line one last time since the progress meter doesn't
211
+ # disappear immediately.
212
+ rewriting(env[:ui]) {|ui| ui.clear_line}
213
+
214
+ # If upload failed or was interrupted, remove created volume from
215
+ # storage pool.
216
+ if env[:interrupted] || !ret
217
+ begin
218
+ fog_volume.destroy
219
+ rescue
220
+ nil
221
+ end
222
+ end
223
+ end
131
224
 
132
225
  # Fog Libvirt currently doesn't support uploading images to storage
133
226
  # pool volumes. Use ruby-libvirt client instead.
@@ -9,9 +9,7 @@ module VagrantPlugins
9
9
  end
10
10
 
11
11
  def call(env)
12
- domain = env[:machine].provider.driver.connection.servers.get(env[:machine].id.to_s)
13
- raise Errors::NoDomainError if domain.nil?
14
- env[:result] = domain.state.to_s == 'running'
12
+ env[:result] = env[:machine].state.id == :running
15
13
 
16
14
  @app.call(env)
17
15
  end
@@ -16,9 +16,9 @@ module VagrantPlugins
16
16
  libvirt_domain = env[:machine].provider.driver.connection.client.lookup_domain_by_uuid(env[:machine].id)
17
17
  if config.suspend_mode == 'managedsave'
18
18
  if libvirt_domain.has_managed_save?
19
- env[:result] = libvirt_domain.has_managed_save?
19
+ env[:result] = env[:machine].state.id == :shutoff
20
20
  else
21
- env[:result] = domain.state.to_s == 'paused'
21
+ env[:result] = env[:machine].state.id == :paused
22
22
  if env[:result]
23
23
  env[:ui].warn('One time switching to pause suspend mode, found a paused VM.')
24
24
  config.suspend_mode = 'pause'
@@ -27,10 +27,10 @@ module VagrantPlugins
27
27
  else
28
28
  if libvirt_domain.has_managed_save?
29
29
  env[:ui].warn('One time switching to managedsave suspend mode, state found.')
30
- env[:result] = true
30
+ env[:result] = [:shutoff, :paused].include?(env[:machine].state.id)
31
31
  config.suspend_mode = 'managedsave'
32
32
  else
33
- env[:result] = domain.state.to_s == 'paused'
33
+ env[:result] = env[:machine].state.id == :paused
34
34
  end
35
35
  end
36
36
 
@@ -86,9 +86,13 @@ module VagrantPlugins
86
86
 
87
87
  def search_network(nets, xml)
88
88
  str = '/domain/devices/interface'
89
- str += "[(@type='network' or @type='udp' or @type='bridge')"
89
+ str += "[(@type='network' or @type='udp' or @type='bridge' or @type='direct')"
90
90
  unless nets.empty?
91
- str += " and source[@network='#{nets.first['network']}']"
91
+ net = nets.first
92
+ network = net['network']
93
+ dev = net['dev']
94
+ str += " and source[@network='#{network}']" if network
95
+ str += " and source[@dev='#{dev}']" if dev
92
96
  end
93
97
  str += ']'
94
98
  @logger.debug(str)
@@ -47,30 +47,6 @@ module VagrantPlugins
47
47
  @logger.info("Got IP address #{env[:ip_address]}")
48
48
  @logger.info("Time for getting IP: #{env[:metrics]['instance_ip_time']}")
49
49
 
50
- # Machine has ip address assigned, now wait till we are able to
51
- # connect via ssh.
52
- env[:metrics]['instance_ssh_time'] = Util::Timer.time do
53
- env[:ui].info(I18n.t('vagrant_libvirt.waiting_for_ssh'))
54
- retryable(on: Fog::Errors::TimeoutError, tries: 60) do
55
- # If we're interrupted don't worry about waiting
56
- next if env[:interrupted]
57
-
58
- # Wait till we are able to connect via ssh.
59
- loop do
60
- # If we're interrupted then just back out
61
- break if env[:interrupted]
62
- break if env[:machine].communicate.ready?
63
- sleep 2
64
- end
65
- end
66
- end
67
- # just return if interrupted and let the warden call recover
68
- return if env[:interrupted]
69
- @logger.info("Time for SSH ready: #{env[:metrics]['instance_ssh_time']}")
70
-
71
- # Booted and ready for use.
72
- # env[:ui].info(I18n.t("vagrant_libvirt.ready"))
73
-
74
50
  @app.call(env)
75
51
  end
76
52
 
@@ -80,7 +56,7 @@ module VagrantPlugins
80
56
  end
81
57
 
82
58
  def terminate(env)
83
- if env[:machine].provider.state.id != :not_created
59
+ if env[:machine].state.id != :not_created
84
60
  # If we're not supposed to destroy on error then just return
85
61
  return unless env[:destroy_on_error]
86
62
 
@@ -4,10 +4,10 @@ require 'vagrant/util/retryable'
4
4
  module VagrantPlugins
5
5
  module ProviderLibvirt
6
6
  module Cap
7
- class MountP9
7
+ class Mount9P
8
8
  extend Vagrant::Util::Retryable
9
9
 
10
- def self.mount_p9_shared_folder(machine, folders)
10
+ def self.mount_9p_shared_folder(machine, folders)
11
11
  folders.each do |_name, opts|
12
12
  # Expand the guest path so we can handle things like "~/vagrant"
13
13
  expanded_guest_path = machine.guest.capability(