blob: 2da9d36cb855ad7778ceecdcc2c35d0eee9204c1 [file] [log] [blame]
smolaon1fb381d2016-03-09 11:10:58 +01001# -*- coding: utf-8 -*-
2'''
3Work with virtual machines managed by libvirt
4
5:depends: libvirt Python module
6'''
7# Special Thanks to Michael Dehann, many of the concepts, and a few structures
8# of his in the virt func module have been used
9
10# Import python libs
11from __future__ import absolute_import
Andrei Danin996e2092018-09-10 21:58:23 -070012import collections
Dennis Dmitrievf5dba8c2017-10-10 19:05:20 +030013import copy
smolaon1fb381d2016-03-09 11:10:58 +010014import os
15import re
16import sys
17import shutil
18import subprocess
19import string # pylint: disable=deprecated-module
20import logging
21
22# Import third party libs
23import yaml
24import jinja2
25import jinja2.exceptions
26import salt.ext.six as six
27from salt.ext.six.moves import StringIO as _StringIO # pylint: disable=import-error
Andrei Danin996e2092018-09-10 21:58:23 -070028from salt.utils.odict import OrderedDict
smolaon1fb381d2016-03-09 11:10:58 +010029from xml.dom import minidom
Andrei Danin996e2092018-09-10 21:58:23 -070030from xml.etree import ElementTree
smolaon1fb381d2016-03-09 11:10:58 +010031try:
32 import libvirt # pylint: disable=import-error
33 HAS_ALL_IMPORTS = True
34except ImportError:
35 HAS_ALL_IMPORTS = False
36
37# Import salt libs
38import salt.utils
39import salt.utils.files
40import salt.utils.templates
41import salt.utils.validate.net
42from salt.exceptions import CommandExecutionError, SaltInvocationError
43
44log = logging.getLogger(__name__)
45
46# Set up template environment
47JINJA = jinja2.Environment(
48 loader=jinja2.FileSystemLoader(
49 os.path.join(salt.utils.templates.TEMPLATE_DIRNAME, 'virt')
50 )
51)
52
53VIRT_STATE_NAME_MAP = {0: 'running',
54 1: 'running',
55 2: 'running',
56 3: 'paused',
57 4: 'shutdown',
58 5: 'shutdown',
59 6: 'crashed'}
60
61VIRT_DEFAULT_HYPER = 'kvm'
62
63
64def __virtual__():
65 if not HAS_ALL_IMPORTS:
66 return False
Ales Komarekf8188332016-03-09 11:32:08 +010067 return 'virtng'
smolaon1fb381d2016-03-09 11:10:58 +010068
69
70def __get_conn():
71 '''
72 Detects what type of dom this node is and attempts to connect to the
73 correct hypervisor via libvirt.
74 '''
75 # This has only been tested on kvm and xen, it needs to be expanded to
76 # support all vm layers supported by libvirt
77
78 def __esxi_uri():
79 '''
80 Connect to an ESXi host with a configuration like so:
81
82 .. code-block:: yaml
83
84 libvirt:
85 hypervisor: esxi
86 connection: esx01
87
88 The connection setting can either be an explicit libvirt URI,
89 or a libvirt URI alias as in this example. No, it cannot be
90 just a hostname.
91
92
93 Example libvirt `/etc/libvirt/libvirt.conf`:
94
95 .. code-block::
96
97 uri_aliases = [
98 "esx01=esx://10.1.1.101/?no_verify=1&auto_answer=1",
99 "esx02=esx://10.1.1.102/?no_verify=1&auto_answer=1",
100 ]
101
102 Reference:
103
104 - http://libvirt.org/drvesx.html#uriformat
105 - http://libvirt.org/uri.html#URI_config
106 '''
107 connection = __salt__['config.get']('libvirt:connection', 'esx')
108 return connection
109
110 def __esxi_auth():
111 '''
112 We rely on that the credentials is provided to libvirt through
113 its built in mechanisms.
114
115 Example libvirt `/etc/libvirt/auth.conf`:
116
117 .. code-block::
118
119 [credentials-myvirt]
120 username=user
121 password=secret
122
123 [auth-esx-10.1.1.101]
124 credentials=myvirt
125
126 [auth-esx-10.1.1.102]
127 credentials=myvirt
128
129 Reference:
130
131 - http://libvirt.org/auth.html#Auth_client_config
132 '''
133 return [[libvirt.VIR_CRED_EXTERNAL], lambda: 0, None]
134
135 if 'virt.connect' in __opts__:
136 conn_str = __opts__['virt.connect']
137 else:
138 conn_str = 'qemu:///system'
139
140 conn_func = {
141 'esxi': [libvirt.openAuth, [__esxi_uri(),
142 __esxi_auth(),
143 0]],
144 'qemu': [libvirt.open, [conn_str]],
145 }
146
147 hypervisor = __salt__['config.get']('libvirt:hypervisor', 'qemu')
148
149 try:
150 conn = conn_func[hypervisor][0](*conn_func[hypervisor][1])
151 except Exception:
152 raise CommandExecutionError(
153 'Sorry, {0} failed to open a connection to the hypervisor '
154 'software at {1}'.format(
155 __grains__['fqdn'],
156 conn_func[hypervisor][1][0]
157 )
158 )
159 return conn
160
161
162def _get_dom(vm_):
163 '''
164 Return a domain object for the named vm
165 '''
166 conn = __get_conn()
167 if vm_ not in list_vms():
168 raise CommandExecutionError('The specified vm is not present')
169 return conn.lookupByName(vm_)
170
171
172def _libvirt_creds():
173 '''
174 Returns the user and group that the disk images should be owned by
175 '''
176 g_cmd = 'grep ^\\s*group /etc/libvirt/qemu.conf'
177 u_cmd = 'grep ^\\s*user /etc/libvirt/qemu.conf'
178 try:
179 group = subprocess.Popen(g_cmd,
180 shell=True,
181 stdout=subprocess.PIPE).communicate()[0].split('"')[1]
182 except IndexError:
183 group = 'root'
184 try:
185 user = subprocess.Popen(u_cmd,
186 shell=True,
187 stdout=subprocess.PIPE).communicate()[0].split('"')[1]
188 except IndexError:
189 user = 'root'
190 return {'user': user, 'group': group}
191
192
193def _get_migrate_command():
194 '''
195 Returns the command shared by the different migration types
196 '''
197 if __salt__['config.option']('virt.tunnel'):
198 return ('virsh migrate --p2p --tunnelled --live --persistent '
199 '--undefinesource ')
200 return 'virsh migrate --live --persistent --undefinesource '
201
202
203def _get_target(target, ssh):
204 proto = 'qemu'
205 if ssh:
206 proto += '+ssh'
207 return ' {0}://{1}/{2}'.format(proto, target, 'system')
208
209
210def _gen_xml(name,
211 cpu,
212 mem,
213 diskp,
214 nicp,
215 hypervisor,
216 **kwargs):
217 '''
218 Generate the XML string to define a libvirt vm
219 '''
220 hypervisor = 'vmware' if hypervisor == 'esxi' else hypervisor
221 mem = mem * 1024 # MB
222 context = {
223 'hypervisor': hypervisor,
224 'name': name,
225 'cpu': str(cpu),
226 'mem': str(mem),
227 }
228 if hypervisor in ['qemu', 'kvm']:
229 context['controller_model'] = False
230 elif hypervisor in ['esxi', 'vmware']:
231 # TODO: make bus and model parameterized, this works for 64-bit Linux
232 context['controller_model'] = 'lsilogic'
233
234 if 'boot_dev' in kwargs:
235 context['boot_dev'] = []
236 for dev in kwargs['boot_dev'].split():
237 context['boot_dev'].append(dev)
238 else:
239 context['boot_dev'] = ['hd']
240
Martin Horak0d183cb2018-09-14 16:11:08 +0200241 if 'enable_vnc' in kwargs:
242 context['enable_vnc'] = kwargs['enable_vnc']
243 log.info('VNC enabled: {0}.'.format(kwargs['enable_vnc']))
smolaon1fb381d2016-03-09 11:10:58 +0100244 if 'serial_type' in kwargs:
245 context['serial_type'] = kwargs['serial_type']
246 if 'serial_type' in context and context['serial_type'] == 'tcp':
247 if 'telnet_port' in kwargs:
248 context['telnet_port'] = kwargs['telnet_port']
249 else:
250 context['telnet_port'] = 23023 # FIXME: use random unused port
251 if 'serial_type' in context:
252 if 'console' in kwargs:
253 context['console'] = kwargs['console']
254 else:
255 context['console'] = True
256
257 context['disks'] = {}
Andrei Danin996e2092018-09-10 21:58:23 -0700258 context['cdrom'] = []
smolaon1fb381d2016-03-09 11:10:58 +0100259 for i, disk in enumerate(diskp):
260 for disk_name, args in disk.items():
Andrei Danin996e2092018-09-10 21:58:23 -0700261 if args.get('device', 'disk') == 'cdrom':
262 context['cdrom'].append(args)
263 continue
smolaon1fb381d2016-03-09 11:10:58 +0100264 context['disks'][disk_name] = {}
265 fn_ = '{0}.{1}'.format(disk_name, args['format'])
266 context['disks'][disk_name]['file_name'] = fn_
267 context['disks'][disk_name]['source_file'] = os.path.join(args['pool'],
268 name,
269 fn_)
270 if hypervisor in ['qemu', 'kvm']:
271 context['disks'][disk_name]['target_dev'] = 'vd{0}'.format(string.ascii_lowercase[i])
272 context['disks'][disk_name]['address'] = False
273 context['disks'][disk_name]['driver'] = True
274 elif hypervisor in ['esxi', 'vmware']:
275 context['disks'][disk_name]['target_dev'] = 'sd{0}'.format(string.ascii_lowercase[i])
276 context['disks'][disk_name]['address'] = True
277 context['disks'][disk_name]['driver'] = False
278 context['disks'][disk_name]['disk_bus'] = args['model']
279 context['disks'][disk_name]['type'] = args['format']
280 context['disks'][disk_name]['index'] = str(i)
281
282 context['nics'] = nicp
283
284 fn_ = 'libvirt_domain.jinja'
285 try:
286 template = JINJA.get_template(fn_)
287 except jinja2.exceptions.TemplateNotFound:
288 log.error('Could not load template {0}'.format(fn_))
289 return ''
290
Andrei Danin996e2092018-09-10 21:58:23 -0700291 xml = template.render(**context)
smolaon1fb381d2016-03-09 11:10:58 +0100292
Andrei Danin996e2092018-09-10 21:58:23 -0700293 # Add cdrom devices separately because a current template doesn't support them.
294 if context['cdrom']:
295 xml_doc = ElementTree.fromstring(xml)
296 xml_devs = xml_doc.find('.//devices')
297 cdrom_xml_tmpl = """<disk type='file' device='cdrom'>
298 <driver name='{driver_name}' type='{driver_type}'/>
299 <source file='{filename}'/>
300 <target dev='{dev}' bus='{bus}'/>
301 <readonly/>
302 </disk>"""
303 for disk in context['cdrom']:
304 cdrom_elem = ElementTree.fromstring(cdrom_xml_tmpl.format(**disk))
305 xml_devs.append(cdrom_elem)
306 xml = ElementTree.tostring(xml_doc)
307 return xml
smolaon1fb381d2016-03-09 11:10:58 +0100308
309def _gen_vol_xml(vmname,
310 diskname,
311 size,
312 hypervisor,
313 **kwargs):
314 '''
315 Generate the XML string to define a libvirt storage volume
316 '''
317 size = int(size) * 1024 # MB
318 disk_info = _get_image_info(hypervisor, vmname, **kwargs)
319 context = {
320 'name': vmname,
321 'filename': '{0}.{1}'.format(diskname, disk_info['disktype']),
322 'volname': diskname,
323 'disktype': disk_info['disktype'],
324 'size': str(size),
325 'pool': disk_info['pool'],
326 }
327 fn_ = 'libvirt_volume.jinja'
328 try:
329 template = JINJA.get_template(fn_)
330 except jinja2.exceptions.TemplateNotFound:
331 log.error('Could not load template {0}'.format(fn_))
332 return ''
333 return template.render(**context)
334
335
336def _qemu_image_info(path):
337 '''
338 Detect information for the image at path
339 '''
340 ret = {}
Oleh Hryhorov7c5dfd32018-01-19 15:43:44 +0200341 out = __salt__['cmd.shell']('qemu-img info {0}'.format(path))
smolaon1fb381d2016-03-09 11:10:58 +0100342
343 match_map = {'size': r'virtual size: \w+ \((\d+) byte[s]?\)',
344 'format': r'file format: (\w+)'}
345
346 for info, search in match_map.items():
347 try:
348 ret[info] = re.search(search, out).group(1)
349 except AttributeError:
350 continue
351 return ret
352
353
354# TODO: this function is deprecated, should be replaced with
355# _qemu_image_info()
356def _image_type(vda):
357 '''
358 Detect what driver needs to be used for the given image
359 '''
Oleh Hryhorov7c5dfd32018-01-19 15:43:44 +0200360 out = __salt__['cmd.shell']('qemu-img info {0}'.format(vda))
smolaon1fb381d2016-03-09 11:10:58 +0100361 if 'file format: qcow2' in out:
362 return 'qcow2'
363 else:
364 return 'raw'
365
366
367# TODO: this function is deprecated, should be merged and replaced
368# with _disk_profile()
369def _get_image_info(hypervisor, name, **kwargs):
370 '''
371 Determine disk image info, such as filename, image format and
372 storage pool, based on which hypervisor is used
373 '''
374 ret = {}
375 if hypervisor in ['esxi', 'vmware']:
376 ret['disktype'] = 'vmdk'
377 ret['filename'] = '{0}{1}'.format(name, '.vmdk')
378 ret['pool'] = '[{0}] '.format(kwargs.get('pool', '0'))
379 elif hypervisor in ['kvm', 'qemu']:
380 ret['disktype'] = 'qcow2'
381 ret['filename'] = '{0}{1}'.format(name, '.qcow2')
Mateusz Los4c7cd2d2018-01-09 11:46:07 +0100382 if 'img_dest' in kwargs:
383 ret['pool'] = kwargs['img_dest']
384 else:
385 ret['pool'] = __salt__['config.option']('virt.images')
smolaon1fb381d2016-03-09 11:10:58 +0100386 return ret
387
388
389def _disk_profile(profile, hypervisor, **kwargs):
390 '''
391 Gather the disk profile from the config or apply the default based
392 on the active hypervisor
393
394 This is the ``default`` profile for KVM/QEMU, which can be
395 overridden in the configuration:
396
397 .. code-block:: yaml
398
399 virt:
400 disk:
401 default:
402 - system:
403 size: 8192
404 format: qcow2
405 model: virtio
406
Dennis Dmitrieva57463c2017-03-24 17:05:22 +0200407 Example profile for KVM/QEMU with two disks, first is created
408 from specified image, the second is empty:
409
410 .. code-block:: yaml
411
412 virt:
413 disk:
414 two_disks:
415 - system:
416 size: 8192
417 format: qcow2
418 model: virtio
419 image: http://path/to/image.qcow2
420 - lvm:
421 size: 32768
422 format: qcow2
423 model: virtio
424
smolaon1fb381d2016-03-09 11:10:58 +0100425 The ``format`` and ``model`` parameters are optional, and will
426 default to whatever is best suitable for the active hypervisor.
427 '''
428 default = [
429 {'system':
430 {'size': '8192'}
431 }
432 ]
433 if hypervisor in ['esxi', 'vmware']:
434 overlay = {'format': 'vmdk',
435 'model': 'scsi',
436 'pool': '[{0}] '.format(kwargs.get('pool', '0'))
437 }
438 elif hypervisor in ['qemu', 'kvm']:
Mateusz Los4c7cd2d2018-01-09 11:46:07 +0100439 if 'img_dest' in kwargs:
440 pool = kwargs['img_dest']
441 else:
442 pool = __salt__['config.option']('virt.images')
443 overlay = {'format': 'qcow2', 'model': 'virtio', 'pool': pool}
smolaon1fb381d2016-03-09 11:10:58 +0100444 else:
445 overlay = {}
446
Dennis Dmitrievf5dba8c2017-10-10 19:05:20 +0300447 disklist = copy.deepcopy(__salt__['config.get']('virt:disk', {}).get(profile, default))
smolaon1fb381d2016-03-09 11:10:58 +0100448 for key, val in overlay.items():
449 for i, disks in enumerate(disklist):
450 for disk in disks:
451 if key not in disks[disk]:
452 disklist[i][disk][key] = val
453 return disklist
454
455
456def _nic_profile(profile_name, hypervisor, **kwargs):
457
smolaon1fb381d2016-03-09 11:10:58 +0100458 def append_dict_profile_to_interface_list(profile_dict):
459 for interface_name, attributes in profile_dict.items():
460 attributes['name'] = interface_name
461 interfaces.append(attributes)
462
smolaon1fb381d2016-03-09 11:10:58 +0100463 def _normalize_net_types(attributes):
464 '''
465 Guess which style of definition:
466
467 bridge: br0
468
469 or
470
471 network: net0
472
473 or
474
475 type: network
476 source: net0
477 '''
478 for type_ in ['bridge', 'network']:
479 if type_ in attributes:
480 attributes['type'] = type_
481 # we want to discard the original key
482 attributes['source'] = attributes.pop(type_)
483
484 attributes['type'] = attributes.get('type', None)
485 attributes['source'] = attributes.get('source', None)
Dzmitry Stremkouskib8acf1f2018-06-28 12:56:23 +0200486 attributes['virtualport'] = attributes.get('virtualport', None)
smolaon1fb381d2016-03-09 11:10:58 +0100487
488 def _apply_default_overlay(attributes):
489 for key, value in overlays[hypervisor].items():
490 if key not in attributes or not attributes[key]:
491 attributes[key] = value
492
493 def _assign_mac(attributes):
494 dmac = '{0}_mac'.format(attributes['name'])
495 if dmac in kwargs:
496 dmac = kwargs[dmac]
497 if salt.utils.validate.net.mac(dmac):
498 attributes['mac'] = dmac
499 else:
500 msg = 'Malformed MAC address: {0}'.format(dmac)
501 raise CommandExecutionError(msg)
502 else:
503 attributes['mac'] = salt.utils.gen_mac()
504
Dzmitry Stremkouskib8acf1f2018-06-28 12:56:23 +0200505
506 default = [{'eth0': {}}]
507 vmware_overlay = {'type': 'bridge', 'source': 'DEFAULT', 'model': 'e1000'}
508 kvm_overlay = {'type': 'bridge', 'source': 'br0', 'model': 'virtio'}
509 overlays = {
510 'kvm': kvm_overlay,
511 'qemu': kvm_overlay,
512 'esxi': vmware_overlay,
513 'vmware': vmware_overlay,
514 }
515
516 # support old location
517 config_data = __salt__['config.option']('virt.nic', {}).get(
518 profile_name, None
519 )
520
521 if config_data is None:
522 config_data = __salt__['config.get']('virt:nic', {}).get(
523 profile_name, default
524 )
525
526 interfaces = []
527
528 if isinstance(config_data, dict):
529 append_dict_profile_to_interface_list(config_data)
530
531 elif isinstance(config_data, list):
532 for interface in config_data:
533 if isinstance(interface, dict):
534 if len(interface) == 1:
535 append_dict_profile_to_interface_list(interface)
536 else:
537 interfaces.append(interface)
538
smolaon1fb381d2016-03-09 11:10:58 +0100539 for interface in interfaces:
540 _normalize_net_types(interface)
541 _assign_mac(interface)
542 if hypervisor in overlays:
543 _apply_default_overlay(interface)
544
545 return interfaces
546
547
548def init(name,
549 cpu,
550 mem,
551 image=None,
552 nic='default',
553 hypervisor=VIRT_DEFAULT_HYPER,
554 start=True, # pylint: disable=redefined-outer-name
555 disk='default',
556 saltenv='base',
azvyagintseva4e802d2018-05-04 20:16:02 +0300557 rng=None,
Alexandru Avadanii00f187a2018-06-24 20:36:44 +0200558 loader=None,
559 machine=None,
560 cpu_mode=None,
Pavel Cizinskyf03c4e82018-09-10 14:56:11 +0200561 cpuset=None,
smolaon1fb381d2016-03-09 11:10:58 +0100562 **kwargs):
563 '''
564 Initialize a new vm
565
566 CLI Example:
567
568 .. code-block:: bash
569
570 salt 'hypervisor' virt.init vm_name 4 512 salt://path/to/image.raw
571 salt 'hypervisor' virt.init vm_name 4 512 nic=profile disk=profile
572 '''
Dennis Dmitrieva57463c2017-03-24 17:05:22 +0200573
azvyagintseva4e802d2018-05-04 20:16:02 +0300574 rng = rng or {'backend':'/dev/urandom'}
smolaon1fb381d2016-03-09 11:10:58 +0100575 hypervisor = __salt__['config.get']('libvirt:hypervisor', hypervisor)
Andrei Danin996e2092018-09-10 21:58:23 -0700576 if kwargs.get('seed') not in (False, True, None, 'qemu-nbd', 'cloud-init'):
577 log.warning(
578 "The seeding method '{0}' is not supported".format(kwargs.get('seed'))
579 )
smolaon1fb381d2016-03-09 11:10:58 +0100580
581 nicp = _nic_profile(nic, hypervisor, **kwargs)
582
Dennis Dmitrieva57463c2017-03-24 17:05:22 +0200583 diskp = _disk_profile(disk, hypervisor, **kwargs)
584
585 if image:
586 # Backward compatibility: if 'image' is specified in the VMs arguments
587 # instead of a disk arguments. In this case, 'image' will be assigned
588 # to the first disk for the VM.
smolaon1fb381d2016-03-09 11:10:58 +0100589 disk_name = next(diskp[0].iterkeys())
Dennis Dmitrieva57463c2017-03-24 17:05:22 +0200590 if not diskp[0][disk_name].get('image', None):
591 diskp[0][disk_name]['image'] = image
smolaon1fb381d2016-03-09 11:10:58 +0100592
Dennis Dmitrieva57463c2017-03-24 17:05:22 +0200593 # Create multiple disks, empty or from specified images.
594 for disk in diskp:
595 log.debug("Creating disk for VM [ {0} ]: {1}".format(name, disk))
smolaon1fb381d2016-03-09 11:10:58 +0100596
Dennis Dmitrieva57463c2017-03-24 17:05:22 +0200597 for disk_name, args in disk.items():
smolaon1fb381d2016-03-09 11:10:58 +0100598
Dennis Dmitrieva57463c2017-03-24 17:05:22 +0200599 if hypervisor in ['esxi', 'vmware']:
600 if 'image' in args:
601 # TODO: we should be copying the image file onto the ESX host
602 raise SaltInvocationError('virt.init does not support image '
603 'template template in conjunction '
604 'with esxi hypervisor')
605 else:
606 # assume libvirt manages disks for us
smolaon1fb381d2016-03-09 11:10:58 +0100607 xml = _gen_vol_xml(name,
608 disk_name,
609 args['size'],
Mateusz Los4c7cd2d2018-01-09 11:46:07 +0100610 hypervisor,
611 **kwargs)
smolaon1fb381d2016-03-09 11:10:58 +0100612 define_vol_xml_str(xml)
613
Dennis Dmitrieva57463c2017-03-24 17:05:22 +0200614 elif hypervisor in ['qemu', 'kvm']:
615
616 disk_type = args['format']
617 disk_file_name = '{0}.{1}'.format(disk_name, disk_type)
618 # disk size TCP cloud
619 disk_size = args['size']
620
Mateusz Los4c7cd2d2018-01-09 11:46:07 +0100621 if 'img_dest' in kwargs:
622 img_dir = kwargs['img_dest']
623 else:
624 img_dir = __salt__['config.option']('virt.images')
Dennis Dmitrieva57463c2017-03-24 17:05:22 +0200625 img_dest = os.path.join(
626 img_dir,
627 name,
628 disk_file_name
629 )
630 img_dir = os.path.dirname(img_dest)
631 if not os.path.isdir(img_dir):
632 os.makedirs(img_dir)
633
634 if 'image' in args:
635 # Create disk from specified image
636 sfn = __salt__['cp.cache_file'](args['image'], saltenv)
637 try:
638 salt.utils.files.copyfile(sfn, img_dest)
639 mask = os.umask(0)
640 os.umask(mask)
641 # Apply umask and remove exec bit
642
643 # Resizing image TCP cloud
644 cmd = 'qemu-img resize ' + img_dest + ' ' + str(disk_size) + 'M'
645 subprocess.call(cmd, shell=True)
646
647 mode = (0o0777 ^ mask) & 0o0666
648 os.chmod(img_dest, mode)
649
650 except (IOError, OSError) as e:
651 raise CommandExecutionError('problem while copying image. {0} - {1}'.format(args['image'], e))
652
Andrei Danin996e2092018-09-10 21:58:23 -0700653 if kwargs.get('seed') in (True, 'qemu-nbd'):
654 install = kwargs.get('install', True)
655 seed_cmd = kwargs.get('seed_cmd', 'seedng.apply')
Dennis Dmitrieva57463c2017-03-24 17:05:22 +0200656
Andrei Danin996e2092018-09-10 21:58:23 -0700657 __salt__[seed_cmd](img_dest,
658 id_=name,
659 config=kwargs.get('config'),
660 install=install)
Dennis Dmitrieva57463c2017-03-24 17:05:22 +0200661 else:
662 # Create empty disk
663 try:
664 mask = os.umask(0)
665 os.umask(mask)
666 # Apply umask and remove exec bit
667
668 # Create empty image
669 cmd = 'qemu-img create -f ' + disk_type + ' ' + img_dest + ' ' + str(disk_size) + 'M'
670 subprocess.call(cmd, shell=True)
671
672 mode = (0o0777 ^ mask) & 0o0666
673 os.chmod(img_dest, mode)
674
675 except (IOError, OSError) as e:
676 raise CommandExecutionError('problem while creating volume {0} - {1}'.format(img_dest, e))
677
678 else:
679 # Unknown hypervisor
680 raise SaltInvocationError('Unsupported hypervisor when handling disk image: {0}'
681 .format(hypervisor))
682
Andrei Danin996e2092018-09-10 21:58:23 -0700683 cloud_init = kwargs.get('cloud_init', {})
684
685 # Seed Salt Minion config via Cloud-init if required.
686 if kwargs.get('seed') == 'cloud-init':
687 # Recursive dict update.
688 def rec_update(d, u):
689 for k, v in u.iteritems():
690 if isinstance(v, collections.Mapping):
691 d[k] = rec_update(d.get(k, {}), v)
692 else:
693 d[k] = v
694 return d
695
696 cloud_init_seed = {
697 "user_data": {
698 "salt_minion": {
699 "conf": {
700 "master": __salt__['config.option']('master'),
701 "id": name
702 }
703 }
704 }
705 }
706 cloud_init = rec_update(cloud_init_seed, cloud_init)
707
708 # Create a cloud-init config drive if defined.
709 if cloud_init:
710 if hypervisor not in ['qemu', 'kvm']:
711 raise SaltInvocationError('Unsupported hypervisor when '
712 'handling Cloud-Init disk '
713 'image: {0}'.format(hypervisor))
714 cfg_drive = os.path.join(img_dir, 'config-2.iso')
715 vm_hostname, vm_domainname = name.split('.', 1)
716
717 def OrderedDict_to_dict(instance):
718 if isinstance(instance, basestring):
719 return instance
720 elif isinstance(instance, collections.Sequence):
721 return map(OrderedDict_to_dict, instance)
722 elif isinstance(instance, collections.Mapping):
723 if isinstance(instance, OrderedDict):
724 instance = dict(instance)
725 for k, v in instance.iteritems():
726 instance[k] = OrderedDict_to_dict(v)
727 return instance
728 else:
729 return instance
730
731 # Yaml.dump dumps OrderedDict in the way to be incompatible with
732 # Cloud-init, hence all OrderedDicts have to be converted to dict first.
733 user_data = OrderedDict_to_dict(cloud_init.get('user_data', None))
734
735 __salt__["cfgdrive.generate"](
736 dst=cfg_drive,
737 hostname=vm_hostname,
738 domainname=vm_domainname,
739 user_data=user_data,
740 network_data=cloud_init.get('network_data', None),
741 )
742 diskp.append({
743 'config_2': {
744 'device': 'cdrom',
745 'driver_name': 'qemu',
746 'driver_type': 'raw',
747 'dev': 'hdc',
748 'bus': 'ide',
749 'filename': cfg_drive
750 }
751 })
752
smolaon1fb381d2016-03-09 11:10:58 +0100753 xml = _gen_xml(name, cpu, mem, diskp, nicp, hypervisor, **kwargs)
Dzmitry Stremkouski7ee23402018-04-10 00:43:48 +0200754
755 # TODO: Remove this code and refactor module, when salt-common would have updated libvirt_domain.jinja template
Andrei Danin996e2092018-09-10 21:58:23 -0700756 xml_doc = minidom.parseString(xml)
Pavel Cizinskyf03c4e82018-09-10 14:56:11 +0200757 if cpuset:
Pavel Cizinskyf03c4e82018-09-10 14:56:11 +0200758 xml_doc.getElementsByTagName("vcpu")[0].setAttribute('cpuset', cpuset)
Pavel Cizinskyf03c4e82018-09-10 14:56:11 +0200759
760 # TODO: Remove this code and refactor module, when salt-common would have updated libvirt_domain.jinja template
Alexandru Avadanii00f187a2018-06-24 20:36:44 +0200761 if cpu_mode:
Alexandru Avadanii00f187a2018-06-24 20:36:44 +0200762 cpu_xml = xml_doc.createElement("cpu")
763 cpu_xml.setAttribute('mode', cpu_mode)
764 xml_doc.getElementsByTagName("domain")[0].appendChild(cpu_xml)
Alexandru Avadanii00f187a2018-06-24 20:36:44 +0200765
766 # TODO: Remove this code and refactor module, when salt-common would have updated libvirt_domain.jinja template
767 if machine:
Alexandru Avadanii00f187a2018-06-24 20:36:44 +0200768 os_xml = xml_doc.getElementsByTagName("domain")[0].getElementsByTagName("os")[0]
769 os_xml.getElementsByTagName("type")[0].setAttribute('machine', machine)
Alexandru Avadanii00f187a2018-06-24 20:36:44 +0200770
771 # TODO: Remove this code and refactor module, when salt-common would have updated libvirt_domain.jinja template
772 if loader and 'path' not in loader:
773 log.info('`path` is a required property of `loader`, and cannot be found. Skipping loader configuration')
774 loader = None
775 elif loader:
Alexandru Avadanii00f187a2018-06-24 20:36:44 +0200776 loader_xml = xml_doc.createElement("loader")
777 for key, val in loader.items():
778 if key == 'path':
779 continue
780 loader_xml.setAttribute(key, val)
781 loader_path_xml = xml_doc.createTextNode(loader['path'])
782 loader_xml.appendChild(loader_path_xml)
783 xml_doc.getElementsByTagName("domain")[0].getElementsByTagName("os")[0].appendChild(loader_xml)
Alexandru Avadanii00f187a2018-06-24 20:36:44 +0200784
785 # TODO: Remove this code and refactor module, when salt-common would have updated libvirt_domain.jinja template
Dzmitry Stremkouskib8acf1f2018-06-28 12:56:23 +0200786 for _nic in nicp:
787 if _nic['virtualport']:
Dzmitry Stremkouskib8acf1f2018-06-28 12:56:23 +0200788 interfaces = xml_doc.getElementsByTagName("domain")[0].getElementsByTagName("devices")[0].getElementsByTagName("interface")
789 for interface in interfaces:
790 if interface.getElementsByTagName('mac')[0].getAttribute('address').lower() == _nic['mac'].lower():
791 vport_xml = xml_doc.createElement("virtualport")
792 vport_xml.setAttribute("type", _nic['virtualport']['type'])
793 interface.appendChild(vport_xml)
Dzmitry Stremkouskib8acf1f2018-06-28 12:56:23 +0200794
795 # TODO: Remove this code and refactor module, when salt-common would have updated libvirt_domain.jinja template
Dzmitry Stremkouski7ee23402018-04-10 00:43:48 +0200796 if rng:
797 rng_model = rng.get('model', 'random')
798 rng_backend = rng.get('backend', '/dev/urandom')
Dzmitry Stremkouski7ee23402018-04-10 00:43:48 +0200799 rng_xml = xml_doc.createElement("rng")
800 rng_xml.setAttribute("model", "virtio")
801 backend = xml_doc.createElement("backend")
802 backend.setAttribute("model", rng_model)
803 backend.appendChild(xml_doc.createTextNode(rng_backend))
804 rng_xml.appendChild(backend)
805 if 'rate' in rng:
806 rng_rate_period = rng['rate'].get('period', '2000')
807 rng_rate_bytes = rng['rate'].get('bytes', '1234')
808 rate = xml_doc.createElement("rate")
809 rate.setAttribute("period", rng_rate_period)
810 rate.setAttribute("bytes", rng_rate_bytes)
811 rng_xml.appendChild(rate)
812 xml_doc.getElementsByTagName("domain")[0].getElementsByTagName("devices")[0].appendChild(rng_xml)
Dzmitry Stremkouski7ee23402018-04-10 00:43:48 +0200813
Andrei Danin996e2092018-09-10 21:58:23 -0700814 xml = xml_doc.toxml()
smolaon1fb381d2016-03-09 11:10:58 +0100815 define_xml_str(xml)
816
smolaon1fb381d2016-03-09 11:10:58 +0100817 if start:
818 create(name)
819
820 return True
821
822
823def list_vms():
824 '''
825 Return a list of virtual machine names on the minion
826
827 CLI Example:
828
829 .. code-block:: bash
830
Ales Komarekf8188332016-03-09 11:32:08 +0100831 salt '*' virtng.list_vms
smolaon1fb381d2016-03-09 11:10:58 +0100832 '''
833 vms = []
834 vms.extend(list_active_vms())
835 vms.extend(list_inactive_vms())
836 return vms
837
838
839def list_active_vms():
840 '''
841 Return a list of names for active virtual machine on the minion
842
843 CLI Example:
844
845 .. code-block:: bash
846
Ales Komarekf8188332016-03-09 11:32:08 +0100847 salt '*' virtng.list_active_vms
smolaon1fb381d2016-03-09 11:10:58 +0100848 '''
849 conn = __get_conn()
850 vms = []
851 for id_ in conn.listDomainsID():
852 vms.append(conn.lookupByID(id_).name())
853 return vms
854
855
856def list_inactive_vms():
857 '''
858 Return a list of names for inactive virtual machine on the minion
859
860 CLI Example:
861
862 .. code-block:: bash
863
Ales Komarekf8188332016-03-09 11:32:08 +0100864 salt '*' virtng.list_inactive_vms
smolaon1fb381d2016-03-09 11:10:58 +0100865 '''
866 conn = __get_conn()
867 vms = []
868 for id_ in conn.listDefinedDomains():
869 vms.append(id_)
870 return vms
871
872
873def vm_info(vm_=None):
874 '''
875 Return detailed information about the vms on this hyper in a
876 list of dicts:
877
878 .. code-block:: python
879
880 [
881 'your-vm': {
882 'cpu': <int>,
883 'maxMem': <int>,
884 'mem': <int>,
885 'state': '<state>',
886 'cputime' <int>
887 },
888 ...
889 ]
890
891 If you pass a VM name in as an argument then it will return info
892 for just the named VM, otherwise it will return all VMs.
893
894 CLI Example:
895
896 .. code-block:: bash
897
Ales Komarekf8188332016-03-09 11:32:08 +0100898 salt '*' virtng.vm_info
smolaon1fb381d2016-03-09 11:10:58 +0100899 '''
900 def _info(vm_):
901 dom = _get_dom(vm_)
902 raw = dom.info()
903 return {'cpu': raw[3],
904 'cputime': int(raw[4]),
905 'disks': get_disks(vm_),
906 'graphics': get_graphics(vm_),
907 'nics': get_nics(vm_),
908 'maxMem': int(raw[1]),
909 'mem': int(raw[2]),
910 'state': VIRT_STATE_NAME_MAP.get(raw[0], 'unknown')}
911 info = {}
912 if vm_:
913 info[vm_] = _info(vm_)
914 else:
915 for vm_ in list_vms():
916 info[vm_] = _info(vm_)
917 return info
918
919
920def vm_state(vm_=None):
921 '''
922 Return list of all the vms and their state.
923
924 If you pass a VM name in as an argument then it will return info
925 for just the named VM, otherwise it will return all VMs.
926
927 CLI Example:
928
929 .. code-block:: bash
930
Ales Komarekf8188332016-03-09 11:32:08 +0100931 salt '*' virtng.vm_state <vm name>
smolaon1fb381d2016-03-09 11:10:58 +0100932 '''
933 def _info(vm_):
934 state = ''
935 dom = _get_dom(vm_)
936 raw = dom.info()
937 state = VIRT_STATE_NAME_MAP.get(raw[0], 'unknown')
938 return state
939 info = {}
940 if vm_:
941 info[vm_] = _info(vm_)
942 else:
943 for vm_ in list_vms():
944 info[vm_] = _info(vm_)
945 return info
946
947
948def node_info():
949 '''
950 Return a dict with information about this node
951
952 CLI Example:
953
954 .. code-block:: bash
955
Ales Komarekf8188332016-03-09 11:32:08 +0100956 salt '*' virtng.node_info
smolaon1fb381d2016-03-09 11:10:58 +0100957 '''
958 conn = __get_conn()
959 raw = conn.getInfo()
960 info = {'cpucores': raw[6],
961 'cpumhz': raw[3],
962 'cpumodel': str(raw[0]),
963 'cpus': raw[2],
964 'cputhreads': raw[7],
965 'numanodes': raw[4],
966 'phymemory': raw[1],
967 'sockets': raw[5]}
968 return info
969
970
971def get_nics(vm_):
972 '''
973 Return info about the network interfaces of a named vm
974
975 CLI Example:
976
977 .. code-block:: bash
978
Ales Komarekf8188332016-03-09 11:32:08 +0100979 salt '*' virtng.get_nics <vm name>
smolaon1fb381d2016-03-09 11:10:58 +0100980 '''
981 nics = {}
982 doc = minidom.parse(_StringIO(get_xml(vm_)))
983 for node in doc.getElementsByTagName('devices'):
984 i_nodes = node.getElementsByTagName('interface')
985 for i_node in i_nodes:
986 nic = {}
987 nic['type'] = i_node.getAttribute('type')
988 for v_node in i_node.getElementsByTagName('*'):
989 if v_node.tagName == 'mac':
990 nic['mac'] = v_node.getAttribute('address')
991 if v_node.tagName == 'model':
992 nic['model'] = v_node.getAttribute('type')
993 if v_node.tagName == 'target':
994 nic['target'] = v_node.getAttribute('dev')
995 # driver, source, and match can all have optional attributes
996 if re.match('(driver|source|address)', v_node.tagName):
997 temp = {}
998 for key, value in v_node.attributes.items():
999 temp[key] = value
1000 nic[str(v_node.tagName)] = temp
1001 # virtualport needs to be handled separately, to pick up the
1002 # type attribute of the virtualport itself
1003 if v_node.tagName == 'virtualport':
1004 temp = {}
1005 temp['type'] = v_node.getAttribute('type')
1006 for key, value in v_node.attributes.items():
1007 temp[key] = value
1008 nic['virtualport'] = temp
1009 if 'mac' not in nic:
1010 continue
1011 nics[nic['mac']] = nic
1012 return nics
1013
1014
1015def get_macs(vm_):
1016 '''
1017 Return a list off MAC addresses from the named vm
1018
1019 CLI Example:
1020
1021 .. code-block:: bash
1022
Ales Komarekf8188332016-03-09 11:32:08 +01001023 salt '*' virtng.get_macs <vm name>
smolaon1fb381d2016-03-09 11:10:58 +01001024 '''
1025 macs = []
1026 doc = minidom.parse(_StringIO(get_xml(vm_)))
1027 for node in doc.getElementsByTagName('devices'):
1028 i_nodes = node.getElementsByTagName('interface')
1029 for i_node in i_nodes:
1030 for v_node in i_node.getElementsByTagName('mac'):
1031 macs.append(v_node.getAttribute('address'))
1032 return macs
1033
1034
1035def get_graphics(vm_):
1036 '''
1037 Returns the information on vnc for a given vm
1038
1039 CLI Example:
1040
1041 .. code-block:: bash
1042
Ales Komarekf8188332016-03-09 11:32:08 +01001043 salt '*' virtng.get_graphics <vm name>
smolaon1fb381d2016-03-09 11:10:58 +01001044 '''
1045 out = {'autoport': 'None',
1046 'keymap': 'None',
1047 'listen': 'None',
1048 'port': 'None',
1049 'type': 'vnc'}
1050 xml = get_xml(vm_)
1051 ssock = _StringIO(xml)
1052 doc = minidom.parse(ssock)
1053 for node in doc.getElementsByTagName('domain'):
1054 g_nodes = node.getElementsByTagName('graphics')
1055 for g_node in g_nodes:
1056 for key, value in g_node.attributes.items():
1057 out[key] = value
1058 return out
1059
1060
1061def get_disks(vm_):
1062 '''
1063 Return the disks of a named vm
1064
1065 CLI Example:
1066
1067 .. code-block:: bash
1068
Ales Komarekf8188332016-03-09 11:32:08 +01001069 salt '*' virtng.get_disks <vm name>
smolaon1fb381d2016-03-09 11:10:58 +01001070 '''
1071 disks = {}
1072 doc = minidom.parse(_StringIO(get_xml(vm_)))
1073 for elem in doc.getElementsByTagName('disk'):
1074 sources = elem.getElementsByTagName('source')
1075 targets = elem.getElementsByTagName('target')
1076 if len(sources) > 0:
1077 source = sources[0]
1078 else:
1079 continue
1080 if len(targets) > 0:
1081 target = targets[0]
1082 else:
1083 continue
1084 if target.hasAttribute('dev'):
1085 qemu_target = ''
1086 if source.hasAttribute('file'):
1087 qemu_target = source.getAttribute('file')
1088 elif source.hasAttribute('dev'):
1089 qemu_target = source.getAttribute('dev')
1090 elif source.hasAttribute('protocol') and \
1091 source.hasAttribute('name'): # For rbd network
1092 qemu_target = '{0}:{1}'.format(
1093 source.getAttribute('protocol'),
1094 source.getAttribute('name'))
1095 if qemu_target:
1096 disks[target.getAttribute('dev')] = {
1097 'file': qemu_target}
1098 for dev in disks:
1099 try:
1100 hypervisor = __salt__['config.get']('libvirt:hypervisor', 'kvm')
1101 if hypervisor not in ['qemu', 'kvm']:
1102 break
1103
1104 output = []
1105 qemu_output = subprocess.Popen(['qemu-img', 'info',
1106 disks[dev]['file']],
1107 shell=False,
1108 stdout=subprocess.PIPE).communicate()[0]
1109 snapshots = False
1110 columns = None
1111 lines = qemu_output.strip().split('\n')
1112 for line in lines:
1113 if line.startswith('Snapshot list:'):
1114 snapshots = True
1115 continue
1116
1117 # If this is a copy-on-write image, then the backing file
1118 # represents the base image
1119 #
1120 # backing file: base.qcow2 (actual path: /var/shared/base.qcow2)
1121 elif line.startswith('backing file'):
1122 matches = re.match(r'.*\(actual path: (.*?)\)', line)
1123 if matches:
1124 output.append('backing file: {0}'.format(matches.group(1)))
1125 continue
1126
1127 elif snapshots:
1128 if line.startswith('ID'): # Do not parse table headers
1129 line = line.replace('VM SIZE', 'VMSIZE')
1130 line = line.replace('VM CLOCK', 'TIME VMCLOCK')
1131 columns = re.split(r'\s+', line)
1132 columns = [c.lower() for c in columns]
1133 output.append('snapshots:')
1134 continue
1135 fields = re.split(r'\s+', line)
1136 for i, field in enumerate(fields):
1137 sep = ' '
1138 if i == 0:
1139 sep = '-'
1140 output.append(
1141 '{0} {1}: "{2}"'.format(
1142 sep, columns[i], field
1143 )
1144 )
1145 continue
1146 output.append(line)
1147 output = '\n'.join(output)
1148 disks[dev].update(yaml.safe_load(output))
1149 except TypeError:
1150 disks[dev].update(yaml.safe_load('image: Does not exist'))
1151 return disks
1152
1153
1154def setmem(vm_, memory, config=False):
1155 '''
1156 Changes the amount of memory allocated to VM. The VM must be shutdown
1157 for this to work.
1158
1159 memory is to be specified in MB
1160 If config is True then we ask libvirt to modify the config as well
1161
1162 CLI Example:
1163
1164 .. code-block:: bash
1165
Ales Komarekf8188332016-03-09 11:32:08 +01001166 salt '*' virtng.setmem myvm 768
smolaon1fb381d2016-03-09 11:10:58 +01001167 '''
1168 if vm_state(vm_) != 'shutdown':
1169 return False
1170
1171 dom = _get_dom(vm_)
1172
1173 # libvirt has a funny bitwise system for the flags in that the flag
1174 # to affect the "current" setting is 0, which means that to set the
1175 # current setting we have to call it a second time with just 0 set
1176 flags = libvirt.VIR_DOMAIN_MEM_MAXIMUM
1177 if config:
1178 flags = flags | libvirt.VIR_DOMAIN_AFFECT_CONFIG
1179
1180 ret1 = dom.setMemoryFlags(memory * 1024, flags)
1181 ret2 = dom.setMemoryFlags(memory * 1024, libvirt.VIR_DOMAIN_AFFECT_CURRENT)
1182
1183 # return True if both calls succeeded
1184 return ret1 == ret2 == 0
1185
1186
1187def setvcpus(vm_, vcpus, config=False):
1188 '''
1189 Changes the amount of vcpus allocated to VM. The VM must be shutdown
1190 for this to work.
1191
1192 vcpus is an int representing the number to be assigned
1193 If config is True then we ask libvirt to modify the config as well
1194
1195 CLI Example:
1196
1197 .. code-block:: bash
1198
Ales Komarekf8188332016-03-09 11:32:08 +01001199 salt '*' virtng.setvcpus myvm 2
smolaon1fb381d2016-03-09 11:10:58 +01001200 '''
1201 if vm_state(vm_) != 'shutdown':
1202 return False
1203
1204 dom = _get_dom(vm_)
1205
1206 # see notes in setmem
1207 flags = libvirt.VIR_DOMAIN_VCPU_MAXIMUM
1208 if config:
1209 flags = flags | libvirt.VIR_DOMAIN_AFFECT_CONFIG
1210
1211 ret1 = dom.setVcpusFlags(vcpus, flags)
1212 ret2 = dom.setVcpusFlags(vcpus, libvirt.VIR_DOMAIN_AFFECT_CURRENT)
1213
1214 return ret1 == ret2 == 0
1215
1216
1217def freemem():
1218 '''
1219 Return an int representing the amount of memory that has not been given
1220 to virtual machines on this node
1221
1222 CLI Example:
1223
1224 .. code-block:: bash
1225
Ales Komarekf8188332016-03-09 11:32:08 +01001226 salt '*' virtng.freemem
smolaon1fb381d2016-03-09 11:10:58 +01001227 '''
1228 conn = __get_conn()
1229 mem = conn.getInfo()[1]
1230 # Take off just enough to sustain the hypervisor
1231 mem -= 256
1232 for vm_ in list_vms():
1233 dom = _get_dom(vm_)
1234 if dom.ID() > 0:
1235 mem -= dom.info()[2] / 1024
1236 return mem
1237
1238
1239def freecpu():
1240 '''
1241 Return an int representing the number of unallocated cpus on this
1242 hypervisor
1243
1244 CLI Example:
1245
1246 .. code-block:: bash
1247
Ales Komarekf8188332016-03-09 11:32:08 +01001248 salt '*' virtng.freecpu
smolaon1fb381d2016-03-09 11:10:58 +01001249 '''
1250 conn = __get_conn()
1251 cpus = conn.getInfo()[2]
1252 for vm_ in list_vms():
1253 dom = _get_dom(vm_)
1254 if dom.ID() > 0:
1255 cpus -= dom.info()[3]
1256 return cpus
1257
1258
1259def full_info():
1260 '''
1261 Return the node_info, vm_info and freemem
1262
1263 CLI Example:
1264
1265 .. code-block:: bash
1266
Ales Komarekf8188332016-03-09 11:32:08 +01001267 salt '*' virtng.full_info
smolaon1fb381d2016-03-09 11:10:58 +01001268 '''
1269 return {'freecpu': freecpu(),
1270 'freemem': freemem(),
1271 'node_info': node_info(),
1272 'vm_info': vm_info()}
1273
1274
1275def get_xml(vm_):
1276 '''
1277 Returns the XML for a given vm
1278
1279 CLI Example:
1280
1281 .. code-block:: bash
1282
Ales Komarekf8188332016-03-09 11:32:08 +01001283 salt '*' virtng.get_xml <vm name>
smolaon1fb381d2016-03-09 11:10:58 +01001284 '''
1285 dom = _get_dom(vm_)
1286 return dom.XMLDesc(0)
1287
1288
1289def get_profiles(hypervisor=None):
1290 '''
1291 Return the virt profiles for hypervisor.
1292
1293 Currently there are profiles for:
1294
1295 - nic
1296 - disk
1297
1298 CLI Example:
1299
1300 .. code-block:: bash
1301
Ales Komarekf8188332016-03-09 11:32:08 +01001302 salt '*' virtng.get_profiles
1303 salt '*' virtng.get_profiles hypervisor=esxi
smolaon1fb381d2016-03-09 11:10:58 +01001304 '''
1305 ret = {}
1306 if hypervisor:
1307 hypervisor = hypervisor
1308 else:
1309 hypervisor = __salt__['config.get']('libvirt:hypervisor', VIRT_DEFAULT_HYPER)
1310 virtconf = __salt__['config.get']('virt', {})
1311 for typ in ['disk', 'nic']:
1312 _func = getattr(sys.modules[__name__], '_{0}_profile'.format(typ))
1313 ret[typ] = {'default': _func('default', hypervisor)}
1314 if typ in virtconf:
1315 ret.setdefault(typ, {})
1316 for prf in virtconf[typ]:
1317 ret[typ][prf] = _func(prf, hypervisor)
1318 return ret
1319
1320
1321def shutdown(vm_):
1322 '''
1323 Send a soft shutdown signal to the named vm
1324
1325 CLI Example:
1326
1327 .. code-block:: bash
1328
Ales Komarekf8188332016-03-09 11:32:08 +01001329 salt '*' virtng.shutdown <vm name>
smolaon1fb381d2016-03-09 11:10:58 +01001330 '''
1331 dom = _get_dom(vm_)
1332 return dom.shutdown() == 0
1333
1334
1335def pause(vm_):
1336 '''
1337 Pause the named vm
1338
1339 CLI Example:
1340
1341 .. code-block:: bash
1342
Ales Komarekf8188332016-03-09 11:32:08 +01001343 salt '*' virtng.pause <vm name>
smolaon1fb381d2016-03-09 11:10:58 +01001344 '''
1345 dom = _get_dom(vm_)
1346 return dom.suspend() == 0
1347
1348
1349def resume(vm_):
1350 '''
1351 Resume the named vm
1352
1353 CLI Example:
1354
1355 .. code-block:: bash
1356
Ales Komarekf8188332016-03-09 11:32:08 +01001357 salt '*' virtng.resume <vm name>
smolaon1fb381d2016-03-09 11:10:58 +01001358 '''
1359 dom = _get_dom(vm_)
1360 return dom.resume() == 0
1361
1362
1363def create(vm_):
1364 '''
1365 Start a defined domain
1366
1367 CLI Example:
1368
1369 .. code-block:: bash
1370
Ales Komarekf8188332016-03-09 11:32:08 +01001371 salt '*' virtng.create <vm name>
smolaon1fb381d2016-03-09 11:10:58 +01001372 '''
1373 dom = _get_dom(vm_)
1374 return dom.create() == 0
1375
1376
1377def start(vm_):
1378 '''
1379 Alias for the obscurely named 'create' function
1380
1381 CLI Example:
1382
1383 .. code-block:: bash
1384
Ales Komarekf8188332016-03-09 11:32:08 +01001385 salt '*' virtng.start <vm name>
smolaon1fb381d2016-03-09 11:10:58 +01001386 '''
1387 return create(vm_)
1388
1389
1390def stop(vm_):
1391 '''
1392 Alias for the obscurely named 'destroy' function
1393
1394 CLI Example:
1395
1396 .. code-block:: bash
1397
Ales Komarekf8188332016-03-09 11:32:08 +01001398 salt '*' virtng.stop <vm name>
smolaon1fb381d2016-03-09 11:10:58 +01001399 '''
1400 return destroy(vm_)
1401
1402
1403def reboot(vm_):
1404 '''
1405 Reboot a domain via ACPI request
1406
1407 CLI Example:
1408
1409 .. code-block:: bash
1410
Ales Komarekf8188332016-03-09 11:32:08 +01001411 salt '*' virtng.reboot <vm name>
smolaon1fb381d2016-03-09 11:10:58 +01001412 '''
1413 dom = _get_dom(vm_)
1414
1415 # reboot has a few modes of operation, passing 0 in means the
1416 # hypervisor will pick the best method for rebooting
1417 return dom.reboot(0) == 0
1418
1419
1420def reset(vm_):
1421 '''
1422 Reset a VM by emulating the reset button on a physical machine
1423
1424 CLI Example:
1425
1426 .. code-block:: bash
1427
Ales Komarekf8188332016-03-09 11:32:08 +01001428 salt '*' virtng.reset <vm name>
smolaon1fb381d2016-03-09 11:10:58 +01001429 '''
1430 dom = _get_dom(vm_)
1431
1432 # reset takes a flag, like reboot, but it is not yet used
1433 # so we just pass in 0
1434 # see: http://libvirt.org/html/libvirt-libvirt.html#virDomainReset
1435 return dom.reset(0) == 0
1436
1437
1438def ctrl_alt_del(vm_):
1439 '''
1440 Sends CTRL+ALT+DEL to a VM
1441
1442 CLI Example:
1443
1444 .. code-block:: bash
1445
Ales Komarekf8188332016-03-09 11:32:08 +01001446 salt '*' virtng.ctrl_alt_del <vm name>
smolaon1fb381d2016-03-09 11:10:58 +01001447 '''
1448 dom = _get_dom(vm_)
1449 return dom.sendKey(0, 0, [29, 56, 111], 3, 0) == 0
1450
1451
1452def create_xml_str(xml):
1453 '''
1454 Start a domain based on the XML passed to the function
1455
1456 CLI Example:
1457
1458 .. code-block:: bash
1459
Ales Komarekf8188332016-03-09 11:32:08 +01001460 salt '*' virtng.create_xml_str <XML in string format>
smolaon1fb381d2016-03-09 11:10:58 +01001461 '''
1462 conn = __get_conn()
1463 return conn.createXML(xml, 0) is not None
1464
1465
1466def create_xml_path(path):
1467 '''
1468 Start a domain based on the XML-file path passed to the function
1469
1470 CLI Example:
1471
1472 .. code-block:: bash
1473
Ales Komarekf8188332016-03-09 11:32:08 +01001474 salt '*' virtng.create_xml_path <path to XML file on the node>
smolaon1fb381d2016-03-09 11:10:58 +01001475 '''
1476 if not os.path.isfile(path):
1477 return False
1478 return create_xml_str(salt.utils.fopen(path, 'r').read())
1479
1480
1481def define_xml_str(xml):
1482 '''
1483 Define a domain based on the XML passed to the function
1484
1485 CLI Example:
1486
1487 .. code-block:: bash
1488
Ales Komarekf8188332016-03-09 11:32:08 +01001489 salt '*' virtng.define_xml_str <XML in string format>
smolaon1fb381d2016-03-09 11:10:58 +01001490 '''
1491 conn = __get_conn()
1492 return conn.defineXML(xml) is not None
1493
1494
1495def define_xml_path(path):
1496 '''
1497 Define a domain based on the XML-file path passed to the function
1498
1499 CLI Example:
1500
1501 .. code-block:: bash
1502
Ales Komarekf8188332016-03-09 11:32:08 +01001503 salt '*' virtng.define_xml_path <path to XML file on the node>
smolaon1fb381d2016-03-09 11:10:58 +01001504
1505 '''
1506 if not os.path.isfile(path):
1507 return False
1508 return define_xml_str(salt.utils.fopen(path, 'r').read())
1509
1510
1511def define_vol_xml_str(xml):
1512 '''
1513 Define a volume based on the XML passed to the function
1514
1515 CLI Example:
1516
1517 .. code-block:: bash
1518
Ales Komarekf8188332016-03-09 11:32:08 +01001519 salt '*' virtng.define_vol_xml_str <XML in string format>
smolaon1fb381d2016-03-09 11:10:58 +01001520 '''
1521 poolname = __salt__['config.get']('libvirt:storagepool', 'default')
1522 conn = __get_conn()
1523 pool = conn.storagePoolLookupByName(str(poolname))
1524 return pool.createXML(xml, 0) is not None
1525
1526
1527def define_vol_xml_path(path):
1528 '''
1529 Define a volume based on the XML-file path passed to the function
1530
1531 CLI Example:
1532
1533 .. code-block:: bash
1534
Ales Komarekf8188332016-03-09 11:32:08 +01001535 salt '*' virtng.define_vol_xml_path <path to XML file on the node>
smolaon1fb381d2016-03-09 11:10:58 +01001536
1537 '''
1538 if not os.path.isfile(path):
1539 return False
1540 return define_vol_xml_str(salt.utils.fopen(path, 'r').read())
1541
1542
1543def migrate_non_shared(vm_, target, ssh=False):
1544 '''
1545 Attempt to execute non-shared storage "all" migration
1546
1547 CLI Example:
1548
1549 .. code-block:: bash
1550
Ales Komarekf8188332016-03-09 11:32:08 +01001551 salt '*' virtng.migrate_non_shared <vm name> <target hypervisor>
smolaon1fb381d2016-03-09 11:10:58 +01001552 '''
1553 cmd = _get_migrate_command() + ' --copy-storage-all ' + vm_\
1554 + _get_target(target, ssh)
1555
1556 return subprocess.Popen(cmd,
1557 shell=True,
1558 stdout=subprocess.PIPE).communicate()[0]
1559
1560
1561def migrate_non_shared_inc(vm_, target, ssh=False):
1562 '''
1563 Attempt to execute non-shared storage "all" migration
1564
1565 CLI Example:
1566
1567 .. code-block:: bash
1568
Ales Komarekf8188332016-03-09 11:32:08 +01001569 salt '*' virtng.migrate_non_shared_inc <vm name> <target hypervisor>
smolaon1fb381d2016-03-09 11:10:58 +01001570 '''
1571 cmd = _get_migrate_command() + ' --copy-storage-inc ' + vm_\
1572 + _get_target(target, ssh)
1573
1574 return subprocess.Popen(cmd,
1575 shell=True,
1576 stdout=subprocess.PIPE).communicate()[0]
1577
1578
1579def migrate(vm_, target, ssh=False):
1580 '''
1581 Shared storage migration
1582
1583 CLI Example:
1584
1585 .. code-block:: bash
1586
Ales Komarekf8188332016-03-09 11:32:08 +01001587 salt '*' virtng.migrate <vm name> <target hypervisor>
smolaon1fb381d2016-03-09 11:10:58 +01001588 '''
1589 cmd = _get_migrate_command() + ' ' + vm_\
1590 + _get_target(target, ssh)
1591
1592 return subprocess.Popen(cmd,
1593 shell=True,
1594 stdout=subprocess.PIPE).communicate()[0]
1595
1596
1597def seed_non_shared_migrate(disks, force=False):
1598 '''
1599 Non shared migration requires that the disks be present on the migration
1600 destination, pass the disks information via this function, to the
1601 migration destination before executing the migration.
1602
1603 CLI Example:
1604
1605 .. code-block:: bash
1606
Ales Komarekf8188332016-03-09 11:32:08 +01001607 salt '*' virtng.seed_non_shared_migrate <disks>
smolaon1fb381d2016-03-09 11:10:58 +01001608 '''
1609 for _, data in disks.items():
1610 fn_ = data['file']
1611 form = data['file format']
1612 size = data['virtual size'].split()[1][1:]
1613 if os.path.isfile(fn_) and not force:
1614 # the target exists, check to see if it is compatible
1615 pre = yaml.safe_load(subprocess.Popen('qemu-img info arch',
1616 shell=True,
1617 stdout=subprocess.PIPE).communicate()[0])
1618 if pre['file format'] != data['file format']\
1619 and pre['virtual size'] != data['virtual size']:
1620 return False
1621 if not os.path.isdir(os.path.dirname(fn_)):
1622 os.makedirs(os.path.dirname(fn_))
1623 if os.path.isfile(fn_):
1624 os.remove(fn_)
1625 cmd = 'qemu-img create -f ' + form + ' ' + fn_ + ' ' + size
1626 subprocess.call(cmd, shell=True)
1627 creds = _libvirt_creds()
1628 cmd = 'chown ' + creds['user'] + ':' + creds['group'] + ' ' + fn_
1629 subprocess.call(cmd, shell=True)
1630 return True
1631
1632
1633def set_autostart(vm_, state='on'):
1634 '''
1635 Set the autostart flag on a VM so that the VM will start with the host
1636 system on reboot.
1637
1638 CLI Example:
1639
1640 .. code-block:: bash
1641
1642 salt "*" virt.set_autostart <vm name> <on | off>
1643 '''
1644
1645 dom = _get_dom(vm_)
1646
1647 if state == 'on':
1648 return dom.setAutostart(1) == 0
1649
1650 elif state == 'off':
1651 return dom.setAutostart(0) == 0
1652
1653 else:
1654 # return False if state is set to something other then on or off
1655 return False
1656
1657
1658def destroy(vm_):
1659 '''
1660 Hard power down the virtual machine, this is equivalent to pulling the
1661 power
1662
1663 CLI Example:
1664
1665 .. code-block:: bash
1666
Ales Komarekf8188332016-03-09 11:32:08 +01001667 salt '*' virtng.destroy <vm name>
smolaon1fb381d2016-03-09 11:10:58 +01001668 '''
1669 dom = _get_dom(vm_)
1670 return dom.destroy() == 0
1671
1672
1673def undefine(vm_):
1674 '''
1675 Remove a defined vm, this does not purge the virtual machine image, and
1676 this only works if the vm is powered down
1677
1678 CLI Example:
1679
1680 .. code-block:: bash
1681
Ales Komarekf8188332016-03-09 11:32:08 +01001682 salt '*' virtng.undefine <vm name>
smolaon1fb381d2016-03-09 11:10:58 +01001683 '''
1684 dom = _get_dom(vm_)
Alexandru Avadanii00f187a2018-06-24 20:36:44 +02001685 if getattr(libvirt, 'VIR_DOMAIN_UNDEFINE_NVRAM', False):
1686 # This one is only in 1.2.8+
1687 return dom.undefineFlags(libvirt.VIR_DOMAIN_UNDEFINE_NVRAM) == 0
1688 else:
1689 return dom.undefine() == 0
smolaon1fb381d2016-03-09 11:10:58 +01001690
1691
1692def purge(vm_, dirs=False):
1693 '''
1694 Recursively destroy and delete a virtual machine, pass True for dir's to
1695 also delete the directories containing the virtual machine disk images -
1696 USE WITH EXTREME CAUTION!
1697
1698 CLI Example:
1699
1700 .. code-block:: bash
1701
Ales Komarekf8188332016-03-09 11:32:08 +01001702 salt '*' virtng.purge <vm name>
smolaon1fb381d2016-03-09 11:10:58 +01001703 '''
1704 disks = get_disks(vm_)
1705 try:
1706 if not destroy(vm_):
1707 return False
1708 except libvirt.libvirtError:
1709 # This is thrown if the machine is already shut down
1710 pass
1711 directories = set()
1712 for disk in disks:
1713 os.remove(disks[disk]['file'])
1714 directories.add(os.path.dirname(disks[disk]['file']))
1715 if dirs:
1716 for dir_ in directories:
1717 shutil.rmtree(dir_)
1718 undefine(vm_)
1719 return True
1720
1721
1722def virt_type():
1723 '''
1724 Returns the virtual machine type as a string
1725
1726 CLI Example:
1727
1728 .. code-block:: bash
1729
Ales Komarekf8188332016-03-09 11:32:08 +01001730 salt '*' virtng.virt_type
smolaon1fb381d2016-03-09 11:10:58 +01001731 '''
1732 return __grains__['virtual']
1733
1734
1735def is_kvm_hyper():
1736 '''
1737 Returns a bool whether or not this node is a KVM hypervisor
1738
1739 CLI Example:
1740
1741 .. code-block:: bash
1742
Ales Komarekf8188332016-03-09 11:32:08 +01001743 salt '*' virtng.is_kvm_hyper
smolaon1fb381d2016-03-09 11:10:58 +01001744 '''
1745 try:
1746 if 'kvm_' not in salt.utils.fopen('/proc/modules').read():
1747 return False
1748 except IOError:
1749 # No /proc/modules? Are we on Windows? Or Solaris?
1750 return False
Oleh Hryhorov7c5dfd32018-01-19 15:43:44 +02001751 return 'libvirtd' in __salt__['cmd.shell'](__grains__['ps'])
smolaon1fb381d2016-03-09 11:10:58 +01001752
1753
1754def is_xen_hyper():
1755 '''
1756 Returns a bool whether or not this node is a XEN hypervisor
1757
1758 CLI Example:
1759
1760 .. code-block:: bash
1761
Ales Komarekf8188332016-03-09 11:32:08 +01001762 salt '*' virtng.is_xen_hyper
smolaon1fb381d2016-03-09 11:10:58 +01001763 '''
1764 try:
1765 if __grains__['virtual_subtype'] != 'Xen Dom0':
1766 return False
1767 except KeyError:
1768 # virtual_subtype isn't set everywhere.
1769 return False
1770 try:
1771 if 'xen_' not in salt.utils.fopen('/proc/modules').read():
1772 return False
1773 except IOError:
1774 # No /proc/modules? Are we on Windows? Or Solaris?
1775 return False
Oleh Hryhorov7c5dfd32018-01-19 15:43:44 +02001776 return 'libvirtd' in __salt__['cmd.shell'](__grains__['ps'])
smolaon1fb381d2016-03-09 11:10:58 +01001777
1778
1779def is_hyper():
1780 '''
1781 Returns a bool whether or not this node is a hypervisor of any kind
1782
1783 CLI Example:
1784
1785 .. code-block:: bash
1786
Ales Komarekf8188332016-03-09 11:32:08 +01001787 salt '*' virtng.is_hyper
smolaon1fb381d2016-03-09 11:10:58 +01001788 '''
1789 try:
1790 import libvirt # pylint: disable=import-error
1791 except ImportError:
1792 # not a usable hypervisor without libvirt module
1793 return False
1794 return is_xen_hyper() or is_kvm_hyper()
1795
1796
1797def vm_cputime(vm_=None):
1798 '''
1799 Return cputime used by the vms on this hyper in a
1800 list of dicts:
1801
1802 .. code-block:: python
1803
1804 [
1805 'your-vm': {
1806 'cputime' <int>
1807 'cputime_percent' <int>
1808 },
1809 ...
1810 ]
1811
1812 If you pass a VM name in as an argument then it will return info
1813 for just the named VM, otherwise it will return all VMs.
1814
1815 CLI Example:
1816
1817 .. code-block:: bash
1818
Ales Komarekf8188332016-03-09 11:32:08 +01001819 salt '*' virtng.vm_cputime
smolaon1fb381d2016-03-09 11:10:58 +01001820 '''
1821 host_cpus = __get_conn().getInfo()[2]
1822
1823 def _info(vm_):
1824 dom = _get_dom(vm_)
1825 raw = dom.info()
1826 vcpus = int(raw[3])
1827 cputime = int(raw[4])
1828 cputime_percent = 0
1829 if cputime:
1830 # Divide by vcpus to always return a number between 0 and 100
1831 cputime_percent = (1.0e-7 * cputime / host_cpus) / vcpus
1832 return {
1833 'cputime': int(raw[4]),
1834 'cputime_percent': int('{0:.0f}'.format(cputime_percent))
1835 }
1836 info = {}
1837 if vm_:
1838 info[vm_] = _info(vm_)
1839 else:
1840 for vm_ in list_vms():
1841 info[vm_] = _info(vm_)
1842 return info
1843
1844
1845def vm_netstats(vm_=None):
1846 '''
1847 Return combined network counters used by the vms on this hyper in a
1848 list of dicts:
1849
1850 .. code-block:: python
1851
1852 [
1853 'your-vm': {
1854 'rx_bytes' : 0,
1855 'rx_packets' : 0,
1856 'rx_errs' : 0,
1857 'rx_drop' : 0,
1858 'tx_bytes' : 0,
1859 'tx_packets' : 0,
1860 'tx_errs' : 0,
1861 'tx_drop' : 0
1862 },
1863 ...
1864 ]
1865
1866 If you pass a VM name in as an argument then it will return info
1867 for just the named VM, otherwise it will return all VMs.
1868
1869 CLI Example:
1870
1871 .. code-block:: bash
1872
Ales Komarekf8188332016-03-09 11:32:08 +01001873 salt '*' virtng.vm_netstats
smolaon1fb381d2016-03-09 11:10:58 +01001874 '''
1875 def _info(vm_):
1876 dom = _get_dom(vm_)
1877 nics = get_nics(vm_)
1878 ret = {
1879 'rx_bytes': 0,
1880 'rx_packets': 0,
1881 'rx_errs': 0,
1882 'rx_drop': 0,
1883 'tx_bytes': 0,
1884 'tx_packets': 0,
1885 'tx_errs': 0,
1886 'tx_drop': 0
1887 }
1888 for attrs in six.itervalues(nics):
1889 if 'target' in attrs:
1890 dev = attrs['target']
1891 stats = dom.interfaceStats(dev)
1892 ret['rx_bytes'] += stats[0]
1893 ret['rx_packets'] += stats[1]
1894 ret['rx_errs'] += stats[2]
1895 ret['rx_drop'] += stats[3]
1896 ret['tx_bytes'] += stats[4]
1897 ret['tx_packets'] += stats[5]
1898 ret['tx_errs'] += stats[6]
1899 ret['tx_drop'] += stats[7]
1900
1901 return ret
1902 info = {}
1903 if vm_:
1904 info[vm_] = _info(vm_)
1905 else:
1906 for vm_ in list_vms():
1907 info[vm_] = _info(vm_)
1908 return info
1909
1910
1911def vm_diskstats(vm_=None):
1912 '''
1913 Return disk usage counters used by the vms on this hyper in a
1914 list of dicts:
1915
1916 .. code-block:: python
1917
1918 [
1919 'your-vm': {
1920 'rd_req' : 0,
1921 'rd_bytes' : 0,
1922 'wr_req' : 0,
1923 'wr_bytes' : 0,
1924 'errs' : 0
1925 },
1926 ...
1927 ]
1928
1929 If you pass a VM name in as an argument then it will return info
1930 for just the named VM, otherwise it will return all VMs.
1931
1932 CLI Example:
1933
1934 .. code-block:: bash
1935
Ales Komarekf8188332016-03-09 11:32:08 +01001936 salt '*' virtng.vm_blockstats
smolaon1fb381d2016-03-09 11:10:58 +01001937 '''
1938 def get_disk_devs(vm_):
1939 doc = minidom.parse(_StringIO(get_xml(vm_)))
1940 disks = []
1941 for elem in doc.getElementsByTagName('disk'):
1942 targets = elem.getElementsByTagName('target')
1943 target = targets[0]
1944 disks.append(target.getAttribute('dev'))
1945 return disks
1946
1947 def _info(vm_):
1948 dom = _get_dom(vm_)
1949 # Do not use get_disks, since it uses qemu-img and is very slow
1950 # and unsuitable for any sort of real time statistics
1951 disks = get_disk_devs(vm_)
1952 ret = {'rd_req': 0,
1953 'rd_bytes': 0,
1954 'wr_req': 0,
1955 'wr_bytes': 0,
1956 'errs': 0
1957 }
1958 for disk in disks:
1959 stats = dom.blockStats(disk)
1960 ret['rd_req'] += stats[0]
1961 ret['rd_bytes'] += stats[1]
1962 ret['wr_req'] += stats[2]
1963 ret['wr_bytes'] += stats[3]
1964 ret['errs'] += stats[4]
1965
1966 return ret
1967 info = {}
1968 if vm_:
1969 info[vm_] = _info(vm_)
1970 else:
1971 # Can not run function blockStats on inactive VMs
1972 for vm_ in list_active_vms():
1973 info[vm_] = _info(vm_)
1974 return info