vagrant-libvirt 0.0.41 → 0.0.42

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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/.coveralls.yml +1 -0
  3. data/.github/issue_template.md +37 -0
  4. data/.gitignore +21 -0
  5. data/.travis.yml +24 -0
  6. data/Gemfile +26 -0
  7. data/LICENSE +22 -0
  8. data/README.md +1380 -0
  9. data/Rakefile +8 -0
  10. data/example_box/README.md +29 -0
  11. data/example_box/Vagrantfile +60 -0
  12. data/example_box/metadata.json +5 -0
  13. data/lib/vagrant-libvirt.rb +29 -0
  14. data/lib/vagrant-libvirt/action.rb +370 -0
  15. data/lib/vagrant-libvirt/action/create_domain.rb +322 -0
  16. data/lib/vagrant-libvirt/action/create_domain_volume.rb +87 -0
  17. data/lib/vagrant-libvirt/action/create_network_interfaces.rb +302 -0
  18. data/lib/vagrant-libvirt/action/create_networks.rb +361 -0
  19. data/lib/vagrant-libvirt/action/destroy_domain.rb +83 -0
  20. data/lib/vagrant-libvirt/action/destroy_networks.rb +95 -0
  21. data/lib/vagrant-libvirt/action/forward_ports.rb +227 -0
  22. data/lib/vagrant-libvirt/action/halt_domain.rb +41 -0
  23. data/lib/vagrant-libvirt/action/handle_box_image.rb +156 -0
  24. data/lib/vagrant-libvirt/action/handle_storage_pool.rb +57 -0
  25. data/lib/vagrant-libvirt/action/is_created.rb +18 -0
  26. data/lib/vagrant-libvirt/action/is_running.rb +21 -0
  27. data/lib/vagrant-libvirt/action/is_suspended.rb +42 -0
  28. data/lib/vagrant-libvirt/action/message_already_created.rb +16 -0
  29. data/lib/vagrant-libvirt/action/message_not_created.rb +16 -0
  30. data/lib/vagrant-libvirt/action/message_not_running.rb +16 -0
  31. data/lib/vagrant-libvirt/action/message_not_suspended.rb +16 -0
  32. data/lib/vagrant-libvirt/action/message_will_not_destroy.rb +17 -0
  33. data/lib/vagrant-libvirt/action/package_domain.rb +105 -0
  34. data/lib/vagrant-libvirt/action/prepare_nfs_settings.rb +94 -0
  35. data/lib/vagrant-libvirt/action/prepare_nfs_valid_ids.rb +17 -0
  36. data/lib/vagrant-libvirt/action/prune_nfs_exports.rb +27 -0
  37. data/lib/vagrant-libvirt/action/read_mac_addresses.rb +40 -0
  38. data/lib/vagrant-libvirt/action/remove_libvirt_image.rb +20 -0
  39. data/lib/vagrant-libvirt/action/remove_stale_volume.rb +50 -0
  40. data/lib/vagrant-libvirt/action/resume_domain.rb +34 -0
  41. data/lib/vagrant-libvirt/action/set_boot_order.rb +109 -0
  42. data/lib/vagrant-libvirt/action/set_name_of_domain.rb +64 -0
  43. data/lib/vagrant-libvirt/action/share_folders.rb +71 -0
  44. data/lib/vagrant-libvirt/action/start_domain.rb +307 -0
  45. data/lib/vagrant-libvirt/action/suspend_domain.rb +40 -0
  46. data/lib/vagrant-libvirt/action/wait_till_up.rb +109 -0
  47. data/lib/vagrant-libvirt/cap/mount_p9.rb +42 -0
  48. data/lib/vagrant-libvirt/cap/nic_mac_addresses.rb +17 -0
  49. data/lib/vagrant-libvirt/cap/synced_folder.rb +113 -0
  50. data/lib/vagrant-libvirt/config.rb +746 -0
  51. data/lib/vagrant-libvirt/driver.rb +118 -0
  52. data/lib/vagrant-libvirt/errors.rb +153 -0
  53. data/lib/vagrant-libvirt/plugin.rb +92 -0
  54. data/lib/vagrant-libvirt/provider.rb +130 -0
  55. data/lib/vagrant-libvirt/templates/default_storage_pool.xml.erb +13 -0
  56. data/lib/vagrant-libvirt/templates/domain.xml.erb +244 -0
  57. data/lib/vagrant-libvirt/templates/private_network.xml.erb +42 -0
  58. data/lib/vagrant-libvirt/templates/public_interface.xml.erb +26 -0
  59. data/lib/vagrant-libvirt/util.rb +11 -0
  60. data/lib/vagrant-libvirt/util/collection.rb +19 -0
  61. data/lib/vagrant-libvirt/util/erb_template.rb +22 -0
  62. data/lib/vagrant-libvirt/util/error_codes.rb +100 -0
  63. data/lib/vagrant-libvirt/util/network_util.rb +151 -0
  64. data/lib/vagrant-libvirt/util/timer.rb +17 -0
  65. data/lib/vagrant-libvirt/version.rb +5 -0
  66. data/locales/en.yml +162 -0
  67. data/spec/spec_helper.rb +9 -0
  68. data/spec/support/environment_helper.rb +46 -0
  69. data/spec/support/libvirt_context.rb +30 -0
  70. data/spec/support/sharedcontext.rb +34 -0
  71. data/spec/unit/action/destroy_domain_spec.rb +97 -0
  72. data/spec/unit/action/set_name_of_domain_spec.rb +21 -0
  73. data/spec/unit/action/wait_till_up_spec.rb +127 -0
  74. data/spec/unit/config_spec.rb +113 -0
  75. data/spec/unit/templates/domain_all_settings.xml +137 -0
  76. data/spec/unit/templates/domain_defaults.xml +46 -0
  77. data/spec/unit/templates/domain_spec.rb +84 -0
  78. data/tools/create_box.sh +130 -0
  79. data/tools/prepare_redhat_for_box.sh +119 -0
  80. data/vagrant-libvirt.gemspec +54 -0
  81. metadata +93 -3
@@ -0,0 +1,40 @@
1
+ require 'log4r'
2
+
3
+ module VagrantPlugins
4
+ module ProviderLibvirt
5
+ module Action
6
+ # Suspend domain.
7
+ class SuspendDomain
8
+ def initialize(app, _env)
9
+ @logger = Log4r::Logger.new('vagrant_libvirt::action::suspend_domain')
10
+ @app = app
11
+ end
12
+
13
+ # make pause
14
+ def call(env)
15
+ env[:ui].info(I18n.t('vagrant_libvirt.suspending_domain'))
16
+
17
+ domain = env[:machine].provider.driver.connection.servers.get(env[:machine].id.to_s)
18
+ raise Errors::NoDomainError if domain.nil?
19
+
20
+ config = env[:machine].provider_config
21
+ if config.suspend_mode == 'managedsave'
22
+ libvirt_domain = env[:machine].provider.driver.connection.client.lookup_domain_by_uuid(env[:machine].id)
23
+ begin
24
+ libvirt_domain.managed_save
25
+ rescue => e
26
+ env[:ui].error("Error doing a managed save for domain. It may have entered a paused state.
27
+ Check the output of `virsh managedsave DOMAIN_NAME --verbose` on the VM host, error: #{e.message}")
28
+ end
29
+ else
30
+ domain.suspend
31
+ end
32
+
33
+ @logger.info("Machine #{env[:machine].id} is suspended ")
34
+
35
+ @app.call(env)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,109 @@
1
+ require 'log4r'
2
+ require 'vagrant-libvirt/errors'
3
+ require 'vagrant-libvirt/util/timer'
4
+ require 'vagrant/util/retryable'
5
+
6
+ module VagrantPlugins
7
+ module ProviderLibvirt
8
+ module Action
9
+ # Wait till domain is started, till it obtains an IP address and is
10
+ # accessible via ssh.
11
+ class WaitTillUp
12
+ include Vagrant::Util::Retryable
13
+
14
+ def initialize(app, _env)
15
+ @logger = Log4r::Logger.new('vagrant_libvirt::action::wait_till_up')
16
+ @app = app
17
+ end
18
+
19
+ def call(env)
20
+ # Initialize metrics if they haven't been
21
+ env[:metrics] ||= {}
22
+
23
+ # Get domain object
24
+ domain = env[:machine].provider.driver.get_domain(env[:machine].id.to_s)
25
+ if domain.nil?
26
+ raise Errors::NoDomainError,
27
+ error_message: "Domain #{env[:machine].id} not found"
28
+ end
29
+
30
+ # Wait for domain to obtain an ip address. Ip address is searched
31
+ # from arp table, either localy or remotely via ssh, if libvirt
32
+ # connection was done via ssh.
33
+ env[:ip_address] = nil
34
+ env[:metrics]['instance_ip_time'] = Util::Timer.time do
35
+ @logger.debug("Searching for IP for MAC address: #{domain.mac}")
36
+ env[:ui].info(I18n.t('vagrant_libvirt.waiting_for_ip'))
37
+ retryable(on: Fog::Errors::TimeoutError, tries: 300) do
38
+ # If we're interrupted don't worry about waiting
39
+ return terminate(env) if env[:interrupted]
40
+
41
+ # Wait for domain to obtain an ip address
42
+ domain.wait_for(2) do
43
+ addresses.each_pair do |_type, ip|
44
+ env[:ip_address] = ip[0] unless ip[0].nil?
45
+ end
46
+ !env[:ip_address].nil?
47
+ end
48
+ end
49
+ end
50
+ @logger.info("Got IP address #{env[:ip_address]}")
51
+ @logger.info("Time for getting IP: #{env[:metrics]['instance_ip_time']}")
52
+
53
+ # Machine has ip address assigned, now wait till we are able to
54
+ # connect via ssh.
55
+ env[:metrics]['instance_ssh_time'] = Util::Timer.time do
56
+ env[:ui].info(I18n.t('vagrant_libvirt.waiting_for_ssh'))
57
+ retryable(on: Fog::Errors::TimeoutError, tries: 60) do
58
+ # If we're interrupted don't worry about waiting
59
+ next if env[:interrupted]
60
+
61
+ # Wait till we are able to connect via ssh.
62
+ loop do
63
+ # If we're interrupted then just back out
64
+ break if env[:interrupted]
65
+ break if env[:machine].communicate.ready?
66
+ sleep 2
67
+ end
68
+ end
69
+ end
70
+ # if interrupted above, just terminate immediately
71
+ return terminate(env) if env[:interrupted]
72
+ @logger.info("Time for SSH ready: #{env[:metrics]['instance_ssh_time']}")
73
+
74
+ # Booted and ready for use.
75
+ # env[:ui].info(I18n.t("vagrant_libvirt.ready"))
76
+
77
+ @app.call(env)
78
+ end
79
+
80
+ def recover(env)
81
+ return if env['vagrant.error'].is_a?(Vagrant::Errors::VagrantError)
82
+
83
+ # Undo the import
84
+ terminate(env)
85
+ end
86
+
87
+ def terminate(env)
88
+ if env[:machine].provider.state.id != :not_created
89
+ # If we're not supposed to destroy on error then just return
90
+ return unless env[:destroy_on_error]
91
+
92
+ if env[:halt_on_error]
93
+ halt_env = env.dup
94
+ halt_env.delete(:interrupted)
95
+ halt_env[:config_validate] = false
96
+ env[:action_runner].run(Action.action_halt, halt_env)
97
+ else
98
+ destroy_env = env.dup
99
+ destroy_env.delete(:interrupted)
100
+ destroy_env[:config_validate] = false
101
+ destroy_env[:force_confirm_destroy] = true
102
+ env[:action_runner].run(Action.action_destroy, destroy_env)
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,42 @@
1
+ require 'digest/md5'
2
+ require 'vagrant/util/retryable'
3
+
4
+ module VagrantPlugins
5
+ module ProviderLibvirt
6
+ module Cap
7
+ class MountP9
8
+ extend Vagrant::Util::Retryable
9
+
10
+ def self.mount_p9_shared_folder(machine, folders)
11
+ folders.each do |_name, opts|
12
+ # Expand the guest path so we can handle things like "~/vagrant"
13
+ expanded_guest_path = machine.guest.capability(
14
+ :shell_expand_guest_path, opts[:guestpath]
15
+ )
16
+
17
+ # Do the actual creating and mounting
18
+ machine.communicate.sudo("mkdir -p #{expanded_guest_path}")
19
+
20
+ # Mount
21
+ mount_tag = Digest::MD5.new.update(opts[:hostpath]).to_s[0, 31]
22
+
23
+ mount_opts = '-o trans=virtio'
24
+ mount_opts += ",access=#{opts[:owner]}" if opts[:owner]
25
+ mount_opts += ",version=#{opts[:version]}" if opts[:version]
26
+ mount_opts += ",#{opts[:mount_opts]}" if opts[:mount_opts]
27
+
28
+ mount_command = "mount -t 9p #{mount_opts} '#{mount_tag}' #{expanded_guest_path}"
29
+ retryable(on: Vagrant::Errors::LinuxMountFailed,
30
+ tries: 5,
31
+ sleep: 3) do
32
+ machine.communicate.sudo('modprobe 9p')
33
+ machine.communicate.sudo('modprobe 9pnet_virtio')
34
+ machine.communicate.sudo(mount_command,
35
+ error_class: Vagrant::Errors::LinuxMountFailed)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,17 @@
1
+ module VagrantPlugins
2
+ module ProviderLibvirt
3
+ module Cap
4
+ class NicMacAddresses
5
+ def self.nic_mac_addresses(machine)
6
+ # Vagrant expects a Hash with an index starting at 1 as key
7
+ # and the mac as uppercase string without colons as value
8
+ nic_macs = {}
9
+ machine.provider.mac_addresses.each do |index, mac|
10
+ nic_macs[index + 1] = mac.upcase.delete(':')
11
+ end
12
+ nic_macs
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,113 @@
1
+ require 'log4r'
2
+ require 'ostruct'
3
+ require 'nokogiri'
4
+ require 'digest/md5'
5
+
6
+ require 'vagrant/util/subprocess'
7
+ require 'vagrant/errors'
8
+ require 'vagrant-libvirt/errors'
9
+ # require_relative "helper"
10
+
11
+ module VagrantPlugins
12
+ module SyncedFolder9p
13
+ class SyncedFolder < Vagrant.plugin('2', :synced_folder)
14
+ include Vagrant::Util
15
+ include VagrantPlugins::ProviderLibvirt::Util::ErbTemplate
16
+
17
+ def initialize(*args)
18
+ super
19
+ @logger = Log4r::Logger.new('vagrant_libvirt::synced_folders::9p')
20
+ end
21
+
22
+ def usable?(machine, _raise_error = false)
23
+ # bail now if not using libvirt since checking version would throw error
24
+ return false unless machine.provider_name == :libvirt
25
+
26
+ # <filesystem/> support in device attach/detach introduced in 1.2.2
27
+ # version number format is major * 1,000,000 + minor * 1,000 + release
28
+ libvirt_version = machine.provider.driver.connection.client.libversion
29
+ libvirt_version >= 1_002_002
30
+ end
31
+
32
+ def prepare(machine, folders, _opts)
33
+ raise Vagrant::Errors::Error('No libvirt connection') if machine.provider.driver.connection.nil?
34
+ @conn = machine.provider.driver.connection.client
35
+
36
+ begin
37
+ # loop through folders
38
+ folders.each do |id, folder_opts|
39
+ folder_opts.merge!(target: id,
40
+ accessmode: 'passthrough',
41
+ mount: true,
42
+ readonly: nil) { |_k, ov, _nv| ov }
43
+
44
+ mount_tag = Digest::MD5.new.update(folder_opts[:hostpath]).to_s[0, 31]
45
+ folder_opts[:mount_tag] = mount_tag
46
+
47
+ machine.ui.info "================\nMachine id: #{machine.id}\nShould be mounting folders\n #{id}, opts: #{folder_opts}"
48
+
49
+ #xml = to_xml('filesystem', folder_opts)
50
+ xml = Nokogiri::XML::Builder.new do |xml|
51
+ xml.filesystem(type: 'mount', accessmode: folder_opts[:accessmode]) do
52
+ xml.driver(type: 'path', wrpolicy: 'immediate')
53
+ xml.source(dir: folder_opts[:hostpath])
54
+ xml.target(dir: mount_tag)
55
+ xml.readonly unless folder_opts[:readonly].nil?
56
+ end
57
+ end.to_xml(
58
+ save_with: Nokogiri::XML::Node::SaveOptions::NO_DECLARATION |
59
+ Nokogiri::XML::Node::SaveOptions::NO_EMPTY_TAGS |
60
+ Nokogiri::XML::Node::SaveOptions::FORMAT
61
+ )
62
+ # puts "<<<<< XML:\n #{xml}\n >>>>>"
63
+ @conn.lookup_domain_by_uuid(machine.id).attach_device(xml, 0)
64
+ end
65
+ rescue => e
66
+ machine.ui.error("could not attach device because: #{e}")
67
+ raise VagrantPlugins::ProviderLibvirt::Errors::AttachDeviceError,
68
+ error_message: e.message
69
+ end
70
+ end
71
+
72
+ # TODO: once up, mount folders
73
+ def enable(machine, folders, _opts)
74
+ # Go through each folder and mount
75
+ machine.ui.info('mounting p9 share in guest')
76
+ # Only mount folders that have a guest path specified.
77
+ mount_folders = {}
78
+ folders.each do |id, opts|
79
+ next unless opts[:mount] && opts[:guestpath] && !opts[:guestpath].empty?
80
+ mount_folders[id] = opts.dup
81
+ # merge common options if not given
82
+ mount_folders[id].merge!(version: '9p2000.L') { |_k, ov, _nv| ov }
83
+ end
84
+ # Mount the actual folder
85
+ machine.guest.capability(
86
+ :mount_p9_shared_folder, mount_folders
87
+ )
88
+ end
89
+
90
+ def cleanup(machine, _opts)
91
+ if machine.provider.driver.connection.nil?
92
+ raise Vagrant::Errors::Error('No libvirt connection')
93
+ end
94
+ @conn = machine.provider.driver.connection.client
95
+ begin
96
+ if machine.id && machine.id != ''
97
+ dom = @conn.lookup_domain_by_uuid(machine.id)
98
+ Nokogiri::XML(dom.xml_desc).xpath(
99
+ '/domain/devices/filesystem'
100
+ ).each do |xml|
101
+ dom.detach_device(xml.to_s)
102
+ machine.ui.info 'Cleaned up shared folders'
103
+ end
104
+ end
105
+ rescue => e
106
+ machine.ui.error("could not detach device because: #{e}")
107
+ raise VagrantPlugins::ProviderLibvirt::Errors::DetachDeviceError,
108
+ error_message: e.message
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,746 @@
1
+ require 'vagrant'
2
+
3
+ class Numeric
4
+ Alphabet = ('a'..'z').to_a
5
+ def vdev
6
+ s = ''
7
+ q = self
8
+ (q, r = (q - 1).divmod(26)) && s.prepend(Alphabet[r]) until q.zero?
9
+ 'vd' + s
10
+ end
11
+ end
12
+
13
+ module VagrantPlugins
14
+ module ProviderLibvirt
15
+ class Config < Vagrant.plugin('2', :config)
16
+ # manually specify URI
17
+ # will supercede most other options if provided
18
+ attr_accessor :uri
19
+
20
+ # A hypervisor name to access via Libvirt.
21
+ attr_accessor :driver
22
+
23
+ # The name of the server, where libvirtd is running.
24
+ attr_accessor :host
25
+
26
+ # If use ssh tunnel to connect to Libvirt.
27
+ attr_accessor :connect_via_ssh
28
+ # Path towards the libvirt socket
29
+ attr_accessor :socket
30
+
31
+ # The username to access Libvirt.
32
+ attr_accessor :username
33
+
34
+ # Password for Libvirt connection.
35
+ attr_accessor :password
36
+
37
+ # ID SSH key file
38
+ attr_accessor :id_ssh_key_file
39
+
40
+ # Libvirt storage pool name, where box image and instance snapshots will
41
+ # be stored.
42
+ attr_accessor :storage_pool_name
43
+
44
+ # Turn on to prevent hostname conflicts
45
+ attr_accessor :random_hostname
46
+
47
+ # Libvirt default network
48
+ attr_accessor :management_network_name
49
+ attr_accessor :management_network_address
50
+ attr_accessor :management_network_mode
51
+ attr_accessor :management_network_mac
52
+ attr_accessor :management_network_guest_ipv6
53
+ attr_accessor :management_network_autostart
54
+ attr_accessor :management_network_pci_bus
55
+ attr_accessor :management_network_pci_slot
56
+
57
+ # Default host prefix (alternative to use project folder name)
58
+ attr_accessor :default_prefix
59
+
60
+ # Domain specific settings used while creating new domain.
61
+ attr_accessor :uuid
62
+ attr_accessor :memory
63
+ attr_accessor :memory_backing
64
+ attr_accessor :channel
65
+ attr_accessor :cpus
66
+ attr_accessor :cpu_mode
67
+ attr_accessor :cpu_model
68
+ attr_accessor :cpu_fallback
69
+ attr_accessor :cpu_features
70
+ attr_accessor :cpu_topology
71
+ attr_accessor :features
72
+ attr_accessor :numa_nodes
73
+ attr_accessor :loader
74
+ attr_accessor :boot_order
75
+ attr_accessor :machine_type
76
+ attr_accessor :machine_arch
77
+ attr_accessor :machine_virtual_size
78
+ attr_accessor :disk_bus
79
+ attr_accessor :disk_device
80
+ attr_accessor :nic_model_type
81
+ attr_accessor :nested
82
+ attr_accessor :volume_cache
83
+ attr_accessor :kernel
84
+ attr_accessor :cmd_line
85
+ attr_accessor :initrd
86
+ attr_accessor :dtb
87
+ attr_accessor :emulator_path
88
+ attr_accessor :graphics_type
89
+ attr_accessor :graphics_autoport
90
+ attr_accessor :graphics_port
91
+ attr_accessor :graphics_passwd
92
+ attr_accessor :graphics_ip
93
+ attr_accessor :video_type
94
+ attr_accessor :video_vram
95
+ attr_accessor :keymap
96
+ attr_accessor :kvm_hidden
97
+ attr_accessor :sound_type
98
+
99
+ # Sets the information for connecting to a host TPM device
100
+ # Only supports socket-based TPMs
101
+ attr_accessor :tpm_model
102
+ attr_accessor :tpm_type
103
+ attr_accessor :tpm_path
104
+
105
+ # Sets the max number of NICs that can be created
106
+ # Default set to 8. Don't change the default unless you know
107
+ # what are doing
108
+ attr_accessor :nic_adapter_count
109
+
110
+ # Storage
111
+ attr_accessor :disks
112
+ attr_accessor :cdroms
113
+
114
+ # Inputs
115
+ attr_accessor :inputs
116
+
117
+ # Channels
118
+ attr_accessor :channels
119
+
120
+ # PCI device passthrough
121
+ attr_accessor :pcis
122
+
123
+ # Random number device passthrough
124
+ attr_accessor :rng
125
+
126
+ # Watchdog device
127
+ attr_accessor :watchdog_dev
128
+
129
+ # USB device passthrough
130
+ attr_accessor :usbs
131
+
132
+ # Redirected devices
133
+ attr_accessor :redirdevs
134
+ attr_accessor :redirfilters
135
+
136
+ # smartcard device
137
+ attr_accessor :smartcard_dev
138
+
139
+ # Suspend mode
140
+ attr_accessor :suspend_mode
141
+
142
+ # Autostart
143
+ attr_accessor :autostart
144
+
145
+ # Attach mgmt network
146
+ attr_accessor :mgmt_attach
147
+
148
+ # Additional qemuargs arguments
149
+ attr_accessor :qemu_args
150
+
151
+ def initialize
152
+ @uri = UNSET_VALUE
153
+ @driver = UNSET_VALUE
154
+ @host = UNSET_VALUE
155
+ @connect_via_ssh = UNSET_VALUE
156
+ @username = UNSET_VALUE
157
+ @password = UNSET_VALUE
158
+ @id_ssh_key_file = UNSET_VALUE
159
+ @storage_pool_name = UNSET_VALUE
160
+ @random_hostname = UNSET_VALUE
161
+ @management_network_name = UNSET_VALUE
162
+ @management_network_address = UNSET_VALUE
163
+ @management_network_mode = UNSET_VALUE
164
+ @management_network_mac = UNSET_VALUE
165
+ @management_network_guest_ipv6 = UNSET_VALUE
166
+ @management_network_autostart = UNSET_VALUE
167
+ @management_network_pci_slot = UNSET_VALUE
168
+ @management_network_pci_bus = UNSET_VALUE
169
+
170
+ # Domain specific settings.
171
+ @uuid = UNSET_VALUE
172
+ @memory = UNSET_VALUE
173
+ @memory_backing = UNSET_VALUE
174
+ @cpus = UNSET_VALUE
175
+ @cpu_mode = UNSET_VALUE
176
+ @cpu_model = UNSET_VALUE
177
+ @cpu_fallback = UNSET_VALUE
178
+ @cpu_features = UNSET_VALUE
179
+ @cpu_topology = UNSET_VALUE
180
+ @features = UNSET_VALUE
181
+ @numa_nodes = UNSET_VALUE
182
+ @loader = UNSET_VALUE
183
+ @machine_type = UNSET_VALUE
184
+ @machine_arch = UNSET_VALUE
185
+ @machine_virtual_size = UNSET_VALUE
186
+ @disk_bus = UNSET_VALUE
187
+ @disk_device = UNSET_VALUE
188
+ @nic_model_type = UNSET_VALUE
189
+ @nested = UNSET_VALUE
190
+ @volume_cache = UNSET_VALUE
191
+ @kernel = UNSET_VALUE
192
+ @initrd = UNSET_VALUE
193
+ @dtb = UNSET_VALUE
194
+ @cmd_line = UNSET_VALUE
195
+ @emulator_path = UNSET_VALUE
196
+ @graphics_type = UNSET_VALUE
197
+ @graphics_autoport = UNSET_VALUE
198
+ @graphics_port = UNSET_VALUE
199
+ @graphics_ip = UNSET_VALUE
200
+ @graphics_passwd = UNSET_VALUE
201
+ @video_type = UNSET_VALUE
202
+ @video_vram = UNSET_VALUE
203
+ @sound_type = UNSET_VALUE
204
+ @keymap = UNSET_VALUE
205
+ @kvm_hidden = UNSET_VALUE
206
+
207
+ @tpm_model = UNSET_VALUE
208
+ @tpm_type = UNSET_VALUE
209
+ @tpm_path = UNSET_VALUE
210
+
211
+ @nic_adapter_count = UNSET_VALUE
212
+
213
+ # Boot order
214
+ @boot_order = []
215
+ # Storage
216
+ @disks = []
217
+ @cdroms = []
218
+
219
+ # Inputs
220
+ @inputs = UNSET_VALUE
221
+
222
+ # Channels
223
+ @channels = UNSET_VALUE
224
+
225
+ # PCI device passthrough
226
+ @pcis = UNSET_VALUE
227
+
228
+ # Random number device passthrough
229
+ @rng = UNSET_VALUE
230
+
231
+ # Watchdog device
232
+ @watchdog_dev = UNSET_VALUE
233
+
234
+ # USB device passthrough
235
+ @usbs = UNSET_VALUE
236
+
237
+ # Redirected devices
238
+ @redirdevs = UNSET_VALUE
239
+ @redirfilters = UNSET_VALUE
240
+
241
+ # smartcard device
242
+ @smartcard_dev = UNSET_VALUE
243
+
244
+ # Suspend mode
245
+ @suspend_mode = UNSET_VALUE
246
+
247
+ # Autostart
248
+ @autostart = UNSET_VALUE
249
+
250
+ # Attach mgmt network
251
+ @mgmt_attach = UNSET_VALUE
252
+
253
+ @qemu_args = []
254
+ end
255
+
256
+ def boot(device)
257
+ @boot_order << device # append
258
+ end
259
+
260
+ def _get_device(disks)
261
+ # skip existing devices and also the first one (vda)
262
+ exist = disks.collect { |x| x[:device] } + [1.vdev.to_s]
263
+ skip = 1 # we're 1 based, not 0 based...
264
+ loop do
265
+ dev = skip.vdev # get lettered device
266
+ return dev unless exist.include?(dev)
267
+ skip += 1
268
+ end
269
+ end
270
+
271
+ def _get_cdrom_dev(cdroms)
272
+ exist = Hash[cdroms.collect { |x| [x[:dev], true] }]
273
+ # hda - hdc
274
+ curr = 'a'.ord
275
+ while curr <= 'd'.ord
276
+ dev = 'hd' + curr.chr
277
+ if exist[dev]
278
+ curr += 1
279
+ next
280
+ else
281
+ return dev
282
+ end
283
+ end
284
+
285
+ # is it better to raise our own error, or let libvirt cause the exception?
286
+ raise 'Only four cdroms may be attached at a time'
287
+ end
288
+
289
+ def _generate_numa
290
+ @numa_nodes.collect { |x|
291
+ # Perform some validation of cpu values
292
+ unless x[:cpus] =~ /^\d+-\d+$/
293
+ raise 'numa_nodes[:cpus] must be in format "integer-integer"'
294
+ end
295
+
296
+ # Convert to KiB
297
+ x[:memory] = x[:memory].to_i * 1024
298
+ }
299
+
300
+ # Grab the value of the last @numa_nodes[:cpus] and verify @cpus matches
301
+ # Note: [:cpus] is zero based and @cpus is not, so we need to +1
302
+ last_cpu = @numa_nodes.last[:cpus]
303
+ last_cpu = last_cpu.scan(/\d+$/)[0]
304
+ last_cpu = last_cpu.to_i + 1
305
+
306
+ if @cpus != last_cpu.to_i
307
+ raise 'The total number of numa_nodes[:cpus] must equal config.cpus'
308
+ end
309
+
310
+ @numa_nodes
311
+ end
312
+
313
+ def cpu_feature(options = {})
314
+ if options[:name].nil? || options[:policy].nil?
315
+ raise 'CPU Feature name AND policy must be specified'
316
+ end
317
+
318
+ @cpu_features = [] if @cpu_features == UNSET_VALUE
319
+
320
+ @cpu_features.push(name: options[:name],
321
+ policy: options[:policy])
322
+ end
323
+
324
+ def cputopology(options = {})
325
+ if options[:sockets].nil? || options[:cores].nil? || options[:threads].nil?
326
+ raise 'CPU topology must have all of sockets, cores and threads specified'
327
+ end
328
+
329
+ if @cpu_topology == UNSET_VALUE
330
+ @cpu_topology = {}
331
+ end
332
+
333
+ @cpu_topology[:sockets] = options[:sockets]
334
+ @cpu_topology[:cores] = options[:cores]
335
+ @cpu_topology[:threads] = options[:threads]
336
+ end
337
+
338
+ def memorybacking(option, config = {})
339
+ case option
340
+ when :source
341
+ raise 'Source type must be specified' if config[:type].nil?
342
+ when :access
343
+ raise 'Access mode must be specified' if config[:mode].nil?
344
+ when :allocation
345
+ raise 'Allocation mode must be specified' if config[:mode].nil?
346
+ end
347
+
348
+ @memory_backing = [] if @memory_backing == UNSET_VALUE
349
+ @memory_backing.push(name: option,
350
+ config: config)
351
+ end
352
+
353
+ def input(options = {})
354
+ if options[:type].nil? || options[:bus].nil?
355
+ raise 'Input type AND bus must be specified'
356
+ end
357
+
358
+ @inputs = [] if @inputs == UNSET_VALUE
359
+
360
+ @inputs.push(type: options[:type],
361
+ bus: options[:bus])
362
+ end
363
+
364
+ def channel(options = {})
365
+ if options[:type].nil?
366
+ raise 'Channel type must be specified.'
367
+ elsif options[:type] == 'unix' && options[:target_type] == 'guestfwd'
368
+ # Guest forwarding requires a target (ip address) and a port
369
+ if options[:target_address].nil? || options[:target_port].nil? ||
370
+ options[:source_path].nil?
371
+ raise 'guestfwd requires target_address, target_port and source_path'
372
+ end
373
+ end
374
+
375
+ @channels = [] if @channels == UNSET_VALUE
376
+
377
+ @channels.push(type: options[:type],
378
+ source_mode: options[:source_mode],
379
+ source_path: options[:source_path],
380
+ target_address: options[:target_address],
381
+ target_name: options[:target_name],
382
+ target_port: options[:target_port],
383
+ target_type: options[:target_type])
384
+ end
385
+
386
+ def random(options = {})
387
+ if !options[:model].nil? && options[:model] != 'random'
388
+ raise 'The only supported rng backend is "random".'
389
+ end
390
+
391
+ @rng = {} if @rng == UNSET_VALUE
392
+
393
+ @rng[:model] = options[:model]
394
+ end
395
+
396
+ def pci(options = {})
397
+ if options[:bus].nil? || options[:slot].nil? || options[:function].nil?
398
+ raise 'Bus AND slot AND function must be specified. Check `lspci` for that numbers.'
399
+ end
400
+
401
+ @pcis = [] if @pcis == UNSET_VALUE
402
+
403
+ @pcis.push(bus: options[:bus],
404
+ slot: options[:slot],
405
+ function: options[:function])
406
+ end
407
+
408
+ def watchdog(options = {})
409
+ if options[:model].nil?
410
+ raise 'Model must be specified.'
411
+ end
412
+
413
+ if @watchdog_dev == UNSET_VALUE
414
+ @watchdog_dev = {}
415
+ end
416
+
417
+ @watchdog_dev[:model] = options[:model]
418
+ @watchdog_dev[:action] = options[:action] || 'reset'
419
+ end
420
+
421
+
422
+ def usb(options = {})
423
+ if (options[:bus].nil? || options[:device].nil?) && options[:vendor].nil? && options[:product].nil?
424
+ raise 'Bus and device and/or vendor and/or product must be specified. Check `lsusb` for these.'
425
+ end
426
+
427
+ @usbs = [] if @usbs == UNSET_VALUE
428
+
429
+ @usbs.push(bus: options[:bus],
430
+ device: options[:device],
431
+ vendor: options[:vendor],
432
+ product: options[:product],
433
+ startupPolicy: options[:startupPolicy])
434
+ end
435
+
436
+ def redirdev(options = {})
437
+ raise 'Type must be specified.' if options[:type].nil?
438
+
439
+ @redirdevs = [] if @redirdevs == UNSET_VALUE
440
+
441
+ @redirdevs.push(type: options[:type])
442
+ end
443
+
444
+ def redirfilter(options = {})
445
+ raise 'Option allow must be specified.' if options[:allow].nil?
446
+
447
+ @redirfilters = [] if @redirfilters == UNSET_VALUE
448
+
449
+ @redirfilters.push(class: options[:class] || -1,
450
+ vendor: options[:class] || -1,
451
+ product: options[:class] || -1,
452
+ version: options[:class] || -1,
453
+ allow: options[:allow])
454
+ end
455
+
456
+ def smartcard(options = {})
457
+ if options[:mode].nil?
458
+ raise 'Option mode must be specified.'
459
+ elsif options[:mode] != 'passthrough'
460
+ raise 'Currently only passthrough mode is supported!'
461
+ elsif options[:type] == 'tcp' && (options[:source_mode].nil? || options[:source_host].nil? || options[:source_service].nil?)
462
+ raise 'If using type "tcp", option "source_mode", "source_host" and "source_service" must be specified.'
463
+ end
464
+
465
+ if @smartcard_dev == UNSET_VALUE
466
+ @smartcard_dev = {}
467
+ end
468
+
469
+ @smartcard_dev[:mode] = options[:mode]
470
+ @smartcard_dev[:type] = options[:type] || 'spicevmc'
471
+ @smartcard_dev[:source_mode] = options[:source_mode] if @smartcard_dev[:type] == 'tcp'
472
+ @smartcard_dev[:source_host] = options[:source_host] if @smartcard_dev[:type] == 'tcp'
473
+ @smartcard_dev[:source_service] = options[:source_service] if @smartcard_dev[:type] == 'tcp'
474
+ end
475
+
476
+ # NOTE: this will run twice for each time it's needed- keep it idempotent
477
+ def storage(storage_type, options = {})
478
+ if storage_type == :file
479
+ if options[:device] == :cdrom
480
+ _handle_cdrom_storage(options)
481
+ else
482
+ _handle_disk_storage(options)
483
+ end
484
+ end
485
+ end
486
+
487
+ def _handle_cdrom_storage(options = {})
488
+ # <disk type="file" device="cdrom">
489
+ # <source file="/home/user/virtio-win-0.1-100.iso"/>
490
+ # <target dev="hdc"/>
491
+ # <readonly/>
492
+ # <address type='drive' controller='0' bus='1' target='0' unit='0'/>
493
+ # </disk>
494
+ #
495
+ # note the target dev will need to be changed with each cdrom drive (hdc, hdd, etc),
496
+ # as will the address unit number (unit=0, unit=1, etc)
497
+
498
+ options = {
499
+ bus: 'ide',
500
+ path: nil
501
+ }.merge(options)
502
+
503
+ cdrom = {
504
+ dev: options[:dev],
505
+ bus: options[:bus],
506
+ path: options[:path]
507
+ }
508
+
509
+ @cdroms << cdrom
510
+ end
511
+
512
+ def _handle_disk_storage(options = {})
513
+ options = {
514
+ type: 'qcow2',
515
+ size: '10G', # matches the fog default
516
+ path: nil,
517
+ bus: 'virtio'
518
+ }.merge(options)
519
+
520
+ disk = {
521
+ device: options[:device],
522
+ type: options[:type],
523
+ size: options[:size],
524
+ path: options[:path],
525
+ bus: options[:bus],
526
+ cache: options[:cache] || 'default',
527
+ allow_existing: options[:allow_existing],
528
+ shareable: options[:shareable],
529
+ serial: options[:serial]
530
+ }
531
+
532
+ @disks << disk # append
533
+ end
534
+
535
+ def qemuargs(options = {})
536
+ @qemu_args << options if options[:value]
537
+ end
538
+
539
+ # code to generate URI from a config moved out of the connect action
540
+ def _generate_uri
541
+ # builds the libvirt connection URI from the given driver config
542
+ # Setup connection uri.
543
+ uri = @driver.dup
544
+ virt_path = case uri
545
+ when 'qemu', 'openvz', 'uml', 'phyp', 'parallels', 'kvm'
546
+ '/system'
547
+ when '@en', 'esx'
548
+ '/'
549
+ when 'vbox', 'vmwarews', 'hyperv'
550
+ '/session'
551
+ else
552
+ raise "Require specify driver #{uri}"
553
+ end
554
+ if uri == 'kvm'
555
+ uri = 'qemu' # use qemu uri for kvm domain type
556
+ end
557
+
558
+ if @connect_via_ssh
559
+ uri << '+ssh://'
560
+ uri << @username + '@' if @username
561
+
562
+ uri << if @host
563
+ @host
564
+ else
565
+ 'localhost'
566
+ end
567
+ else
568
+ uri << '://'
569
+ uri << @host if @host
570
+ end
571
+
572
+ uri << virt_path
573
+ uri << '?no_verify=1'
574
+
575
+ if @id_ssh_key_file
576
+ # set ssh key for access to libvirt host
577
+ uri << "\&keyfile="
578
+ # if no slash, prepend $HOME/.ssh/
579
+ @id_ssh_key_file.prepend("#{`echo ${HOME}`.chomp}/.ssh/") if @id_ssh_key_file !~ /\A\//
580
+ uri << @id_ssh_key_file
581
+ end
582
+ # set path to libvirt socket
583
+ uri << "\&socket=" + @socket if @socket
584
+ uri
585
+ end
586
+
587
+ def finalize!
588
+ @driver = 'kvm' if @driver == UNSET_VALUE
589
+ @host = nil if @host == UNSET_VALUE
590
+ @connect_via_ssh = false if @connect_via_ssh == UNSET_VALUE
591
+ @username = nil if @username == UNSET_VALUE
592
+ @password = nil if @password == UNSET_VALUE
593
+ @id_ssh_key_file = 'id_rsa' if @id_ssh_key_file == UNSET_VALUE
594
+ @storage_pool_name = 'default' if @storage_pool_name == UNSET_VALUE
595
+ @random_hostname = false if @random_hostname == UNSET_VALUE
596
+ @management_network_name = 'vagrant-libvirt' if @management_network_name == UNSET_VALUE
597
+ @management_network_address = '192.168.121.0/24' if @management_network_address == UNSET_VALUE
598
+ @management_network_mode = 'nat' if @management_network_mode == UNSET_VALUE
599
+ @management_network_mac = nil if @management_network_mac == UNSET_VALUE
600
+ @management_network_guest_ipv6 = 'yes' if @management_network_guest_ipv6 == UNSET_VALUE
601
+ @management_network_autostart = false if @management_network_autostart == UNSET_VALUE
602
+ @management_network_pci_bus = nil if @management_network_pci_bus == UNSET_VALUE
603
+ @management_network_pci_slot = nil if @management_network_pci_slot == UNSET_VALUE
604
+
605
+ # generate a URI if none is supplied
606
+ @uri = _generate_uri if @uri == UNSET_VALUE
607
+
608
+ # Domain specific settings.
609
+ @uuid = '' if @uuid == UNSET_VALUE
610
+ @memory = 512 if @memory == UNSET_VALUE
611
+ @memory_backing = [] if @memory_backing == UNSET_VALUE
612
+ @cpus = 1 if @cpus == UNSET_VALUE
613
+ @cpu_mode = 'host-model' if @cpu_mode == UNSET_VALUE
614
+ @cpu_model = if (@cpu_model == UNSET_VALUE) && (@cpu_mode == 'custom')
615
+ 'qemu64'
616
+ elsif @cpu_mode != 'custom'
617
+ ''
618
+ end
619
+ @cpu_topology = {} if @cpu_topology == UNSET_VALUE
620
+ @cpu_fallback = 'allow' if @cpu_fallback == UNSET_VALUE
621
+ @cpu_features = [] if @cpu_features == UNSET_VALUE
622
+ @features = ['acpi','apic','pae'] if @features == UNSET_VALUE
623
+ @numa_nodes = @numa_nodes == UNSET_VALUE ? nil : _generate_numa
624
+ @loader = nil if @loader == UNSET_VALUE
625
+ @machine_type = nil if @machine_type == UNSET_VALUE
626
+ @machine_arch = nil if @machine_arch == UNSET_VALUE
627
+ @machine_virtual_size = nil if @machine_virtual_size == UNSET_VALUE
628
+ @disk_bus = 'virtio' if @disk_bus == UNSET_VALUE
629
+ @disk_device = 'vda' if @disk_device == UNSET_VALUE
630
+ @nic_model_type = nil if @nic_model_type == UNSET_VALUE
631
+ @nested = false if @nested == UNSET_VALUE
632
+ @volume_cache = 'default' if @volume_cache == UNSET_VALUE
633
+ @kernel = nil if @kernel == UNSET_VALUE
634
+ @cmd_line = '' if @cmd_line == UNSET_VALUE
635
+ @initrd = '' if @initrd == UNSET_VALUE
636
+ @dtb = nil if @dtb == UNSET_VALUE
637
+ @graphics_type = 'vnc' if @graphics_type == UNSET_VALUE
638
+ @graphics_autoport = 'yes' if @graphics_port == UNSET_VALUE
639
+ @graphics_autoport = 'no' if @graphics_port != UNSET_VALUE
640
+ if (@graphics_type != 'vnc' && @graphics_type != 'spice') ||
641
+ @graphics_passwd == UNSET_VALUE
642
+ @graphics_passwd = nil
643
+ end
644
+ @graphics_port = -1 if @graphics_port == UNSET_VALUE
645
+ @graphics_ip = '127.0.0.1' if @graphics_ip == UNSET_VALUE
646
+ @video_type = 'cirrus' if @video_type == UNSET_VALUE
647
+ @video_vram = 9216 if @video_vram == UNSET_VALUE
648
+ @sound_type = nil if @sound_type == UNSET_VALUE
649
+ @keymap = 'en-us' if @keymap == UNSET_VALUE
650
+ @kvm_hidden = false if @kvm_hidden == UNSET_VALUE
651
+ @tpm_model = 'tpm-tis' if @tpm_model == UNSET_VALUE
652
+ @tpm_type = 'passthrough' if @tpm_type == UNSET_VALUE
653
+ @tpm_path = nil if @tpm_path == UNSET_VALUE
654
+ @nic_adapter_count = 8 if @nic_adapter_count == UNSET_VALUE
655
+ @emulator_path = nil if @emulator_path == UNSET_VALUE
656
+
657
+ # Boot order
658
+ @boot_order = [] if @boot_order == UNSET_VALUE
659
+
660
+ # Storage
661
+ @disks = [] if @disks == UNSET_VALUE
662
+ @disks.map! do |disk|
663
+ disk[:device] = _get_device(@disks) if disk[:device].nil?
664
+ disk
665
+ end
666
+ @cdroms = [] if @cdroms == UNSET_VALUE
667
+ @cdroms.map! do |cdrom|
668
+ cdrom[:dev] = _get_cdrom_dev(@cdroms) if cdrom[:dev].nil?
669
+ cdrom
670
+ end
671
+
672
+ # Inputs
673
+ @inputs = [{ type: 'mouse', bus: 'ps2' }] if @inputs == UNSET_VALUE
674
+
675
+ # Channels
676
+ @channels = [] if @channels == UNSET_VALUE
677
+
678
+ # PCI device passthrough
679
+ @pcis = [] if @pcis == UNSET_VALUE
680
+
681
+ # Random number generator passthrough
682
+ @rng = {} if @rng == UNSET_VALUE
683
+
684
+ # Watchdog device
685
+ @watchdog_dev = {} if @watchdog_dev == UNSET_VALUE
686
+
687
+ # USB device passthrough
688
+ @usbs = [] if @usbs == UNSET_VALUE
689
+
690
+ # Redirected devices
691
+ @redirdevs = [] if @redirdevs == UNSET_VALUE
692
+ @redirfilters = [] if @redirfilters == UNSET_VALUE
693
+
694
+ # smartcard device
695
+ @smartcard_dev = {} if @smartcard_dev == UNSET_VALUE
696
+
697
+ # Suspend mode
698
+ @suspend_mode = 'pause' if @suspend_mode == UNSET_VALUE
699
+
700
+ # Autostart
701
+ @autostart = false if @autostart == UNSET_VALUE
702
+
703
+ # Attach mgmt network
704
+ @mgmt_attach = true if @mgmt_attach == UNSET_VALUE
705
+
706
+ @qemu_args = [] if @qemu_args == UNSET_VALUE
707
+ end
708
+
709
+ def validate(machine)
710
+ errors = _detected_errors
711
+
712
+ machine.provider_config.disks.each do |disk|
713
+ if disk[:path] && (disk[:path][0] == '/')
714
+ errors << "absolute volume paths like '#{disk[:path]}' not yet supported"
715
+ end
716
+ end
717
+
718
+ machine.config.vm.networks.each do |_type, opts|
719
+ if opts[:mac]
720
+ opts[:mac].downcase!
721
+ if opts[:mac] =~ /\A([0-9a-f]{12})\z/
722
+ opts[:mac] = opts[:mac].scan(/../).join(':')
723
+ end
724
+ unless opts[:mac] =~ /\A([0-9a-f]{2}:){5}([0-9a-f]{2})\z/
725
+ errors << "Configured NIC MAC '#{opts[:mac]}' is not in 'xx:xx:xx:xx:xx:xx' or 'xxxxxxxxxxxx' format"
726
+ end
727
+ end
728
+ end
729
+
730
+ { 'Libvirt Provider' => errors }
731
+ end
732
+
733
+ def merge(other)
734
+ super.tap do |result|
735
+ c = disks.dup
736
+ c += other.disks
737
+ result.disks = c
738
+
739
+ c = cdroms.dup
740
+ c += other.cdroms
741
+ result.cdroms = c
742
+ end
743
+ end
744
+ end
745
+ end
746
+ end