blob: 933d5902ae79bee6bf682f584d2fc4c98e2a7e14 [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
Dzmitry Stremkouski4783b6c2019-04-17 16:16:23 +0200555 dry_run=False,
smolaon1fb381d2016-03-09 11:10:58 +0100556 disk='default',
557 saltenv='base',
azvyagintseva4e802d2018-05-04 20:16:02 +0300558 rng=None,
Alexandru Avadanii00f187a2018-06-24 20:36:44 +0200559 loader=None,
560 machine=None,
561 cpu_mode=None,
Pavel Cizinskyf03c4e82018-09-10 14:56:11 +0200562 cpuset=None,
smolaon1fb381d2016-03-09 11:10:58 +0100563 **kwargs):
564 '''
565 Initialize a new vm
566
567 CLI Example:
568
569 .. code-block:: bash
570
571 salt 'hypervisor' virt.init vm_name 4 512 salt://path/to/image.raw
572 salt 'hypervisor' virt.init vm_name 4 512 nic=profile disk=profile
573 '''
Dennis Dmitrieva57463c2017-03-24 17:05:22 +0200574
azvyagintseva4e802d2018-05-04 20:16:02 +0300575 rng = rng or {'backend':'/dev/urandom'}
smolaon1fb381d2016-03-09 11:10:58 +0100576 hypervisor = __salt__['config.get']('libvirt:hypervisor', hypervisor)
Andrei Danin996e2092018-09-10 21:58:23 -0700577 if kwargs.get('seed') not in (False, True, None, 'qemu-nbd', 'cloud-init'):
578 log.warning(
579 "The seeding method '{0}' is not supported".format(kwargs.get('seed'))
580 )
smolaon1fb381d2016-03-09 11:10:58 +0100581
582 nicp = _nic_profile(nic, hypervisor, **kwargs)
583
Dennis Dmitrieva57463c2017-03-24 17:05:22 +0200584 diskp = _disk_profile(disk, hypervisor, **kwargs)
585
586 if image:
587 # Backward compatibility: if 'image' is specified in the VMs arguments
588 # instead of a disk arguments. In this case, 'image' will be assigned
589 # to the first disk for the VM.
smolaon1fb381d2016-03-09 11:10:58 +0100590 disk_name = next(diskp[0].iterkeys())
Dennis Dmitrieva57463c2017-03-24 17:05:22 +0200591 if not diskp[0][disk_name].get('image', None):
592 diskp[0][disk_name]['image'] = image
smolaon1fb381d2016-03-09 11:10:58 +0100593
Dennis Dmitrieva57463c2017-03-24 17:05:22 +0200594 # Create multiple disks, empty or from specified images.
595 for disk in diskp:
596 log.debug("Creating disk for VM [ {0} ]: {1}".format(name, disk))
smolaon1fb381d2016-03-09 11:10:58 +0100597
Dennis Dmitrieva57463c2017-03-24 17:05:22 +0200598 for disk_name, args in disk.items():
smolaon1fb381d2016-03-09 11:10:58 +0100599
Dennis Dmitrieva57463c2017-03-24 17:05:22 +0200600 if hypervisor in ['esxi', 'vmware']:
601 if 'image' in args:
602 # TODO: we should be copying the image file onto the ESX host
603 raise SaltInvocationError('virt.init does not support image '
604 'template template in conjunction '
605 'with esxi hypervisor')
606 else:
607 # assume libvirt manages disks for us
smolaon1fb381d2016-03-09 11:10:58 +0100608 xml = _gen_vol_xml(name,
609 disk_name,
610 args['size'],
Mateusz Los4c7cd2d2018-01-09 11:46:07 +0100611 hypervisor,
612 **kwargs)
smolaon1fb381d2016-03-09 11:10:58 +0100613 define_vol_xml_str(xml)
614
Dennis Dmitrieva57463c2017-03-24 17:05:22 +0200615 elif hypervisor in ['qemu', 'kvm']:
616
617 disk_type = args['format']
618 disk_file_name = '{0}.{1}'.format(disk_name, disk_type)
619 # disk size TCP cloud
620 disk_size = args['size']
621
Mateusz Los4c7cd2d2018-01-09 11:46:07 +0100622 if 'img_dest' in kwargs:
623 img_dir = kwargs['img_dest']
624 else:
625 img_dir = __salt__['config.option']('virt.images')
Dennis Dmitrieva57463c2017-03-24 17:05:22 +0200626 img_dest = os.path.join(
627 img_dir,
628 name,
629 disk_file_name
630 )
631 img_dir = os.path.dirname(img_dest)
632 if not os.path.isdir(img_dir):
633 os.makedirs(img_dir)
634
635 if 'image' in args:
Dzmitry Stremkouski4783b6c2019-04-17 16:16:23 +0200636 if dry_run:
637 continue
Dennis Dmitrieva57463c2017-03-24 17:05:22 +0200638 # Create disk from specified image
639 sfn = __salt__['cp.cache_file'](args['image'], saltenv)
640 try:
641 salt.utils.files.copyfile(sfn, img_dest)
642 mask = os.umask(0)
643 os.umask(mask)
644 # Apply umask and remove exec bit
645
646 # Resizing image TCP cloud
647 cmd = 'qemu-img resize ' + img_dest + ' ' + str(disk_size) + 'M'
648 subprocess.call(cmd, shell=True)
649
650 mode = (0o0777 ^ mask) & 0o0666
651 os.chmod(img_dest, mode)
652
653 except (IOError, OSError) as e:
654 raise CommandExecutionError('problem while copying image. {0} - {1}'.format(args['image'], e))
655
Andrei Danin996e2092018-09-10 21:58:23 -0700656 if kwargs.get('seed') in (True, 'qemu-nbd'):
657 install = kwargs.get('install', True)
658 seed_cmd = kwargs.get('seed_cmd', 'seedng.apply')
Dennis Dmitrieva57463c2017-03-24 17:05:22 +0200659
Andrei Danin996e2092018-09-10 21:58:23 -0700660 __salt__[seed_cmd](img_dest,
661 id_=name,
662 config=kwargs.get('config'),
663 install=install)
Dennis Dmitrieva57463c2017-03-24 17:05:22 +0200664 else:
665 # Create empty disk
666 try:
667 mask = os.umask(0)
668 os.umask(mask)
669 # Apply umask and remove exec bit
670
671 # Create empty image
672 cmd = 'qemu-img create -f ' + disk_type + ' ' + img_dest + ' ' + str(disk_size) + 'M'
673 subprocess.call(cmd, shell=True)
674
675 mode = (0o0777 ^ mask) & 0o0666
676 os.chmod(img_dest, mode)
677
678 except (IOError, OSError) as e:
679 raise CommandExecutionError('problem while creating volume {0} - {1}'.format(img_dest, e))
680
681 else:
682 # Unknown hypervisor
683 raise SaltInvocationError('Unsupported hypervisor when handling disk image: {0}'
684 .format(hypervisor))
685
Andrei Danin996e2092018-09-10 21:58:23 -0700686 cloud_init = kwargs.get('cloud_init', {})
687
688 # Seed Salt Minion config via Cloud-init if required.
689 if kwargs.get('seed') == 'cloud-init':
690 # Recursive dict update.
691 def rec_update(d, u):
692 for k, v in u.iteritems():
693 if isinstance(v, collections.Mapping):
694 d[k] = rec_update(d.get(k, {}), v)
695 else:
696 d[k] = v
697 return d
698
699 cloud_init_seed = {
700 "user_data": {
701 "salt_minion": {
702 "conf": {
703 "master": __salt__['config.option']('master'),
704 "id": name
705 }
706 }
707 }
708 }
709 cloud_init = rec_update(cloud_init_seed, cloud_init)
710
711 # Create a cloud-init config drive if defined.
712 if cloud_init:
713 if hypervisor not in ['qemu', 'kvm']:
714 raise SaltInvocationError('Unsupported hypervisor when '
715 'handling Cloud-Init disk '
716 'image: {0}'.format(hypervisor))
717 cfg_drive = os.path.join(img_dir, 'config-2.iso')
718 vm_hostname, vm_domainname = name.split('.', 1)
719
720 def OrderedDict_to_dict(instance):
721 if isinstance(instance, basestring):
722 return instance
723 elif isinstance(instance, collections.Sequence):
724 return map(OrderedDict_to_dict, instance)
725 elif isinstance(instance, collections.Mapping):
726 if isinstance(instance, OrderedDict):
727 instance = dict(instance)
728 for k, v in instance.iteritems():
729 instance[k] = OrderedDict_to_dict(v)
730 return instance
731 else:
732 return instance
733
734 # Yaml.dump dumps OrderedDict in the way to be incompatible with
735 # Cloud-init, hence all OrderedDicts have to be converted to dict first.
736 user_data = OrderedDict_to_dict(cloud_init.get('user_data', None))
737
738 __salt__["cfgdrive.generate"](
739 dst=cfg_drive,
740 hostname=vm_hostname,
741 domainname=vm_domainname,
742 user_data=user_data,
743 network_data=cloud_init.get('network_data', None),
744 )
745 diskp.append({
746 'config_2': {
747 'device': 'cdrom',
748 'driver_name': 'qemu',
749 'driver_type': 'raw',
750 'dev': 'hdc',
751 'bus': 'ide',
752 'filename': cfg_drive
753 }
754 })
755
smolaon1fb381d2016-03-09 11:10:58 +0100756 xml = _gen_xml(name, cpu, mem, diskp, nicp, hypervisor, **kwargs)
Dzmitry Stremkouski7ee23402018-04-10 00:43:48 +0200757
758 # TODO: Remove this code and refactor module, when salt-common would have updated libvirt_domain.jinja template
Andrei Danin996e2092018-09-10 21:58:23 -0700759 xml_doc = minidom.parseString(xml)
Pavel Cizinskyf03c4e82018-09-10 14:56:11 +0200760 if cpuset:
Pavel Cizinskyf03c4e82018-09-10 14:56:11 +0200761 xml_doc.getElementsByTagName("vcpu")[0].setAttribute('cpuset', cpuset)
Pavel Cizinskyf03c4e82018-09-10 14:56:11 +0200762
763 # TODO: Remove this code and refactor module, when salt-common would have updated libvirt_domain.jinja template
Alexandru Avadanii00f187a2018-06-24 20:36:44 +0200764 if cpu_mode:
Alexandru Avadanii00f187a2018-06-24 20:36:44 +0200765 cpu_xml = xml_doc.createElement("cpu")
766 cpu_xml.setAttribute('mode', cpu_mode)
767 xml_doc.getElementsByTagName("domain")[0].appendChild(cpu_xml)
Alexandru Avadanii00f187a2018-06-24 20:36:44 +0200768
769 # TODO: Remove this code and refactor module, when salt-common would have updated libvirt_domain.jinja template
770 if machine:
Alexandru Avadanii00f187a2018-06-24 20:36:44 +0200771 os_xml = xml_doc.getElementsByTagName("domain")[0].getElementsByTagName("os")[0]
772 os_xml.getElementsByTagName("type")[0].setAttribute('machine', machine)
Alexandru Avadanii00f187a2018-06-24 20:36:44 +0200773
774 # TODO: Remove this code and refactor module, when salt-common would have updated libvirt_domain.jinja template
775 if loader and 'path' not in loader:
776 log.info('`path` is a required property of `loader`, and cannot be found. Skipping loader configuration')
777 loader = None
778 elif loader:
Alexandru Avadanii00f187a2018-06-24 20:36:44 +0200779 loader_xml = xml_doc.createElement("loader")
780 for key, val in loader.items():
781 if key == 'path':
782 continue
783 loader_xml.setAttribute(key, val)
784 loader_path_xml = xml_doc.createTextNode(loader['path'])
785 loader_xml.appendChild(loader_path_xml)
786 xml_doc.getElementsByTagName("domain")[0].getElementsByTagName("os")[0].appendChild(loader_xml)
Alexandru Avadanii00f187a2018-06-24 20:36:44 +0200787
788 # TODO: Remove this code and refactor module, when salt-common would have updated libvirt_domain.jinja template
Dzmitry Stremkouskib8acf1f2018-06-28 12:56:23 +0200789 for _nic in nicp:
790 if _nic['virtualport']:
Dzmitry Stremkouskib8acf1f2018-06-28 12:56:23 +0200791 interfaces = xml_doc.getElementsByTagName("domain")[0].getElementsByTagName("devices")[0].getElementsByTagName("interface")
792 for interface in interfaces:
793 if interface.getElementsByTagName('mac')[0].getAttribute('address').lower() == _nic['mac'].lower():
794 vport_xml = xml_doc.createElement("virtualport")
795 vport_xml.setAttribute("type", _nic['virtualport']['type'])
796 interface.appendChild(vport_xml)
Dzmitry Stremkouskib8acf1f2018-06-28 12:56:23 +0200797
798 # TODO: Remove this code and refactor module, when salt-common would have updated libvirt_domain.jinja template
Dzmitry Stremkouski7ee23402018-04-10 00:43:48 +0200799 if rng:
800 rng_model = rng.get('model', 'random')
801 rng_backend = rng.get('backend', '/dev/urandom')
Dzmitry Stremkouski7ee23402018-04-10 00:43:48 +0200802 rng_xml = xml_doc.createElement("rng")
803 rng_xml.setAttribute("model", "virtio")
804 backend = xml_doc.createElement("backend")
805 backend.setAttribute("model", rng_model)
806 backend.appendChild(xml_doc.createTextNode(rng_backend))
807 rng_xml.appendChild(backend)
808 if 'rate' in rng:
809 rng_rate_period = rng['rate'].get('period', '2000')
810 rng_rate_bytes = rng['rate'].get('bytes', '1234')
811 rate = xml_doc.createElement("rate")
812 rate.setAttribute("period", rng_rate_period)
813 rate.setAttribute("bytes", rng_rate_bytes)
814 rng_xml.appendChild(rate)
815 xml_doc.getElementsByTagName("domain")[0].getElementsByTagName("devices")[0].appendChild(rng_xml)
Dzmitry Stremkouski7ee23402018-04-10 00:43:48 +0200816
Andrei Danin996e2092018-09-10 21:58:23 -0700817 xml = xml_doc.toxml()
smolaon1fb381d2016-03-09 11:10:58 +0100818 define_xml_str(xml)
819
Dzmitry Stremkouski4783b6c2019-04-17 16:16:23 +0200820 if start and not dry_run:
smolaon1fb381d2016-03-09 11:10:58 +0100821 create(name)
822
823 return True
824
825
826def list_vms():
827 '''
828 Return a list of virtual machine names on the minion
829
830 CLI Example:
831
832 .. code-block:: bash
833
Ales Komarekf8188332016-03-09 11:32:08 +0100834 salt '*' virtng.list_vms
smolaon1fb381d2016-03-09 11:10:58 +0100835 '''
836 vms = []
837 vms.extend(list_active_vms())
838 vms.extend(list_inactive_vms())
839 return vms
840
841
842def list_active_vms():
843 '''
844 Return a list of names for active virtual machine on the minion
845
846 CLI Example:
847
848 .. code-block:: bash
849
Ales Komarekf8188332016-03-09 11:32:08 +0100850 salt '*' virtng.list_active_vms
smolaon1fb381d2016-03-09 11:10:58 +0100851 '''
852 conn = __get_conn()
853 vms = []
854 for id_ in conn.listDomainsID():
855 vms.append(conn.lookupByID(id_).name())
856 return vms
857
858
859def list_inactive_vms():
860 '''
861 Return a list of names for inactive virtual machine on the minion
862
863 CLI Example:
864
865 .. code-block:: bash
866
Ales Komarekf8188332016-03-09 11:32:08 +0100867 salt '*' virtng.list_inactive_vms
smolaon1fb381d2016-03-09 11:10:58 +0100868 '''
869 conn = __get_conn()
870 vms = []
871 for id_ in conn.listDefinedDomains():
872 vms.append(id_)
873 return vms
874
875
876def vm_info(vm_=None):
877 '''
878 Return detailed information about the vms on this hyper in a
879 list of dicts:
880
881 .. code-block:: python
882
883 [
884 'your-vm': {
885 'cpu': <int>,
886 'maxMem': <int>,
887 'mem': <int>,
888 'state': '<state>',
889 'cputime' <int>
890 },
891 ...
892 ]
893
894 If you pass a VM name in as an argument then it will return info
895 for just the named VM, otherwise it will return all VMs.
896
897 CLI Example:
898
899 .. code-block:: bash
900
Ales Komarekf8188332016-03-09 11:32:08 +0100901 salt '*' virtng.vm_info
smolaon1fb381d2016-03-09 11:10:58 +0100902 '''
903 def _info(vm_):
904 dom = _get_dom(vm_)
905 raw = dom.info()
906 return {'cpu': raw[3],
907 'cputime': int(raw[4]),
908 'disks': get_disks(vm_),
909 'graphics': get_graphics(vm_),
910 'nics': get_nics(vm_),
911 'maxMem': int(raw[1]),
912 'mem': int(raw[2]),
913 'state': VIRT_STATE_NAME_MAP.get(raw[0], 'unknown')}
914 info = {}
915 if vm_:
916 info[vm_] = _info(vm_)
917 else:
918 for vm_ in list_vms():
919 info[vm_] = _info(vm_)
920 return info
921
922
923def vm_state(vm_=None):
924 '''
925 Return list of all the vms and their state.
926
927 If you pass a VM name in as an argument then it will return info
928 for just the named VM, otherwise it will return all VMs.
929
930 CLI Example:
931
932 .. code-block:: bash
933
Ales Komarekf8188332016-03-09 11:32:08 +0100934 salt '*' virtng.vm_state <vm name>
smolaon1fb381d2016-03-09 11:10:58 +0100935 '''
936 def _info(vm_):
937 state = ''
938 dom = _get_dom(vm_)
939 raw = dom.info()
940 state = VIRT_STATE_NAME_MAP.get(raw[0], 'unknown')
941 return state
942 info = {}
943 if vm_:
944 info[vm_] = _info(vm_)
945 else:
946 for vm_ in list_vms():
947 info[vm_] = _info(vm_)
948 return info
949
950
951def node_info():
952 '''
953 Return a dict with information about this node
954
955 CLI Example:
956
957 .. code-block:: bash
958
Ales Komarekf8188332016-03-09 11:32:08 +0100959 salt '*' virtng.node_info
smolaon1fb381d2016-03-09 11:10:58 +0100960 '''
961 conn = __get_conn()
962 raw = conn.getInfo()
963 info = {'cpucores': raw[6],
964 'cpumhz': raw[3],
965 'cpumodel': str(raw[0]),
966 'cpus': raw[2],
967 'cputhreads': raw[7],
968 'numanodes': raw[4],
969 'phymemory': raw[1],
970 'sockets': raw[5]}
971 return info
972
973
974def get_nics(vm_):
975 '''
976 Return info about the network interfaces of a named vm
977
978 CLI Example:
979
980 .. code-block:: bash
981
Ales Komarekf8188332016-03-09 11:32:08 +0100982 salt '*' virtng.get_nics <vm name>
smolaon1fb381d2016-03-09 11:10:58 +0100983 '''
984 nics = {}
985 doc = minidom.parse(_StringIO(get_xml(vm_)))
986 for node in doc.getElementsByTagName('devices'):
987 i_nodes = node.getElementsByTagName('interface')
988 for i_node in i_nodes:
989 nic = {}
990 nic['type'] = i_node.getAttribute('type')
991 for v_node in i_node.getElementsByTagName('*'):
992 if v_node.tagName == 'mac':
993 nic['mac'] = v_node.getAttribute('address')
994 if v_node.tagName == 'model':
995 nic['model'] = v_node.getAttribute('type')
996 if v_node.tagName == 'target':
997 nic['target'] = v_node.getAttribute('dev')
998 # driver, source, and match can all have optional attributes
999 if re.match('(driver|source|address)', v_node.tagName):
1000 temp = {}
1001 for key, value in v_node.attributes.items():
1002 temp[key] = value
1003 nic[str(v_node.tagName)] = temp
1004 # virtualport needs to be handled separately, to pick up the
1005 # type attribute of the virtualport itself
1006 if v_node.tagName == 'virtualport':
1007 temp = {}
1008 temp['type'] = v_node.getAttribute('type')
1009 for key, value in v_node.attributes.items():
1010 temp[key] = value
1011 nic['virtualport'] = temp
1012 if 'mac' not in nic:
1013 continue
1014 nics[nic['mac']] = nic
1015 return nics
1016
1017
1018def get_macs(vm_):
1019 '''
1020 Return a list off MAC addresses from the named vm
1021
1022 CLI Example:
1023
1024 .. code-block:: bash
1025
Ales Komarekf8188332016-03-09 11:32:08 +01001026 salt '*' virtng.get_macs <vm name>
smolaon1fb381d2016-03-09 11:10:58 +01001027 '''
1028 macs = []
1029 doc = minidom.parse(_StringIO(get_xml(vm_)))
1030 for node in doc.getElementsByTagName('devices'):
1031 i_nodes = node.getElementsByTagName('interface')
1032 for i_node in i_nodes:
1033 for v_node in i_node.getElementsByTagName('mac'):
1034 macs.append(v_node.getAttribute('address'))
1035 return macs
1036
1037
1038def get_graphics(vm_):
1039 '''
1040 Returns the information on vnc for a given vm
1041
1042 CLI Example:
1043
1044 .. code-block:: bash
1045
Ales Komarekf8188332016-03-09 11:32:08 +01001046 salt '*' virtng.get_graphics <vm name>
smolaon1fb381d2016-03-09 11:10:58 +01001047 '''
1048 out = {'autoport': 'None',
1049 'keymap': 'None',
1050 'listen': 'None',
1051 'port': 'None',
1052 'type': 'vnc'}
1053 xml = get_xml(vm_)
1054 ssock = _StringIO(xml)
1055 doc = minidom.parse(ssock)
1056 for node in doc.getElementsByTagName('domain'):
1057 g_nodes = node.getElementsByTagName('graphics')
1058 for g_node in g_nodes:
1059 for key, value in g_node.attributes.items():
1060 out[key] = value
1061 return out
1062
1063
1064def get_disks(vm_):
1065 '''
1066 Return the disks of a named vm
1067
1068 CLI Example:
1069
1070 .. code-block:: bash
1071
Ales Komarekf8188332016-03-09 11:32:08 +01001072 salt '*' virtng.get_disks <vm name>
smolaon1fb381d2016-03-09 11:10:58 +01001073 '''
1074 disks = {}
1075 doc = minidom.parse(_StringIO(get_xml(vm_)))
1076 for elem in doc.getElementsByTagName('disk'):
1077 sources = elem.getElementsByTagName('source')
1078 targets = elem.getElementsByTagName('target')
1079 if len(sources) > 0:
1080 source = sources[0]
1081 else:
1082 continue
1083 if len(targets) > 0:
1084 target = targets[0]
1085 else:
1086 continue
1087 if target.hasAttribute('dev'):
1088 qemu_target = ''
1089 if source.hasAttribute('file'):
1090 qemu_target = source.getAttribute('file')
1091 elif source.hasAttribute('dev'):
1092 qemu_target = source.getAttribute('dev')
1093 elif source.hasAttribute('protocol') and \
1094 source.hasAttribute('name'): # For rbd network
1095 qemu_target = '{0}:{1}'.format(
1096 source.getAttribute('protocol'),
1097 source.getAttribute('name'))
1098 if qemu_target:
1099 disks[target.getAttribute('dev')] = {
1100 'file': qemu_target}
1101 for dev in disks:
1102 try:
1103 hypervisor = __salt__['config.get']('libvirt:hypervisor', 'kvm')
1104 if hypervisor not in ['qemu', 'kvm']:
1105 break
1106
1107 output = []
1108 qemu_output = subprocess.Popen(['qemu-img', 'info',
1109 disks[dev]['file']],
1110 shell=False,
1111 stdout=subprocess.PIPE).communicate()[0]
1112 snapshots = False
1113 columns = None
1114 lines = qemu_output.strip().split('\n')
1115 for line in lines:
1116 if line.startswith('Snapshot list:'):
1117 snapshots = True
1118 continue
1119
1120 # If this is a copy-on-write image, then the backing file
1121 # represents the base image
1122 #
1123 # backing file: base.qcow2 (actual path: /var/shared/base.qcow2)
1124 elif line.startswith('backing file'):
1125 matches = re.match(r'.*\(actual path: (.*?)\)', line)
1126 if matches:
1127 output.append('backing file: {0}'.format(matches.group(1)))
1128 continue
1129
1130 elif snapshots:
1131 if line.startswith('ID'): # Do not parse table headers
1132 line = line.replace('VM SIZE', 'VMSIZE')
1133 line = line.replace('VM CLOCK', 'TIME VMCLOCK')
1134 columns = re.split(r'\s+', line)
1135 columns = [c.lower() for c in columns]
1136 output.append('snapshots:')
1137 continue
1138 fields = re.split(r'\s+', line)
1139 for i, field in enumerate(fields):
1140 sep = ' '
1141 if i == 0:
1142 sep = '-'
1143 output.append(
1144 '{0} {1}: "{2}"'.format(
1145 sep, columns[i], field
1146 )
1147 )
1148 continue
1149 output.append(line)
1150 output = '\n'.join(output)
1151 disks[dev].update(yaml.safe_load(output))
1152 except TypeError:
1153 disks[dev].update(yaml.safe_load('image: Does not exist'))
1154 return disks
1155
1156
1157def setmem(vm_, memory, config=False):
1158 '''
1159 Changes the amount of memory allocated to VM. The VM must be shutdown
1160 for this to work.
1161
1162 memory is to be specified in MB
1163 If config is True then we ask libvirt to modify the config as well
1164
1165 CLI Example:
1166
1167 .. code-block:: bash
1168
Ales Komarekf8188332016-03-09 11:32:08 +01001169 salt '*' virtng.setmem myvm 768
smolaon1fb381d2016-03-09 11:10:58 +01001170 '''
1171 if vm_state(vm_) != 'shutdown':
1172 return False
1173
1174 dom = _get_dom(vm_)
1175
1176 # libvirt has a funny bitwise system for the flags in that the flag
1177 # to affect the "current" setting is 0, which means that to set the
1178 # current setting we have to call it a second time with just 0 set
1179 flags = libvirt.VIR_DOMAIN_MEM_MAXIMUM
1180 if config:
1181 flags = flags | libvirt.VIR_DOMAIN_AFFECT_CONFIG
1182
1183 ret1 = dom.setMemoryFlags(memory * 1024, flags)
1184 ret2 = dom.setMemoryFlags(memory * 1024, libvirt.VIR_DOMAIN_AFFECT_CURRENT)
1185
1186 # return True if both calls succeeded
1187 return ret1 == ret2 == 0
1188
1189
1190def setvcpus(vm_, vcpus, config=False):
1191 '''
1192 Changes the amount of vcpus allocated to VM. The VM must be shutdown
1193 for this to work.
1194
1195 vcpus is an int representing the number to be assigned
1196 If config is True then we ask libvirt to modify the config as well
1197
1198 CLI Example:
1199
1200 .. code-block:: bash
1201
Ales Komarekf8188332016-03-09 11:32:08 +01001202 salt '*' virtng.setvcpus myvm 2
smolaon1fb381d2016-03-09 11:10:58 +01001203 '''
1204 if vm_state(vm_) != 'shutdown':
1205 return False
1206
1207 dom = _get_dom(vm_)
1208
1209 # see notes in setmem
1210 flags = libvirt.VIR_DOMAIN_VCPU_MAXIMUM
1211 if config:
1212 flags = flags | libvirt.VIR_DOMAIN_AFFECT_CONFIG
1213
1214 ret1 = dom.setVcpusFlags(vcpus, flags)
1215 ret2 = dom.setVcpusFlags(vcpus, libvirt.VIR_DOMAIN_AFFECT_CURRENT)
1216
1217 return ret1 == ret2 == 0
1218
1219
1220def freemem():
1221 '''
1222 Return an int representing the amount of memory that has not been given
1223 to virtual machines on this node
1224
1225 CLI Example:
1226
1227 .. code-block:: bash
1228
Ales Komarekf8188332016-03-09 11:32:08 +01001229 salt '*' virtng.freemem
smolaon1fb381d2016-03-09 11:10:58 +01001230 '''
1231 conn = __get_conn()
1232 mem = conn.getInfo()[1]
1233 # Take off just enough to sustain the hypervisor
1234 mem -= 256
1235 for vm_ in list_vms():
1236 dom = _get_dom(vm_)
1237 if dom.ID() > 0:
1238 mem -= dom.info()[2] / 1024
1239 return mem
1240
1241
1242def freecpu():
1243 '''
1244 Return an int representing the number of unallocated cpus on this
1245 hypervisor
1246
1247 CLI Example:
1248
1249 .. code-block:: bash
1250
Ales Komarekf8188332016-03-09 11:32:08 +01001251 salt '*' virtng.freecpu
smolaon1fb381d2016-03-09 11:10:58 +01001252 '''
1253 conn = __get_conn()
1254 cpus = conn.getInfo()[2]
1255 for vm_ in list_vms():
1256 dom = _get_dom(vm_)
1257 if dom.ID() > 0:
1258 cpus -= dom.info()[3]
1259 return cpus
1260
1261
1262def full_info():
1263 '''
1264 Return the node_info, vm_info and freemem
1265
1266 CLI Example:
1267
1268 .. code-block:: bash
1269
Ales Komarekf8188332016-03-09 11:32:08 +01001270 salt '*' virtng.full_info
smolaon1fb381d2016-03-09 11:10:58 +01001271 '''
1272 return {'freecpu': freecpu(),
1273 'freemem': freemem(),
1274 'node_info': node_info(),
1275 'vm_info': vm_info()}
1276
1277
1278def get_xml(vm_):
1279 '''
1280 Returns the XML for a given vm
1281
1282 CLI Example:
1283
1284 .. code-block:: bash
1285
Ales Komarekf8188332016-03-09 11:32:08 +01001286 salt '*' virtng.get_xml <vm name>
smolaon1fb381d2016-03-09 11:10:58 +01001287 '''
1288 dom = _get_dom(vm_)
1289 return dom.XMLDesc(0)
1290
1291
1292def get_profiles(hypervisor=None):
1293 '''
1294 Return the virt profiles for hypervisor.
1295
1296 Currently there are profiles for:
1297
1298 - nic
1299 - disk
1300
1301 CLI Example:
1302
1303 .. code-block:: bash
1304
Ales Komarekf8188332016-03-09 11:32:08 +01001305 salt '*' virtng.get_profiles
1306 salt '*' virtng.get_profiles hypervisor=esxi
smolaon1fb381d2016-03-09 11:10:58 +01001307 '''
1308 ret = {}
1309 if hypervisor:
1310 hypervisor = hypervisor
1311 else:
1312 hypervisor = __salt__['config.get']('libvirt:hypervisor', VIRT_DEFAULT_HYPER)
1313 virtconf = __salt__['config.get']('virt', {})
1314 for typ in ['disk', 'nic']:
1315 _func = getattr(sys.modules[__name__], '_{0}_profile'.format(typ))
1316 ret[typ] = {'default': _func('default', hypervisor)}
1317 if typ in virtconf:
1318 ret.setdefault(typ, {})
1319 for prf in virtconf[typ]:
1320 ret[typ][prf] = _func(prf, hypervisor)
1321 return ret
1322
1323
1324def shutdown(vm_):
1325 '''
1326 Send a soft shutdown signal to the named vm
1327
1328 CLI Example:
1329
1330 .. code-block:: bash
1331
Ales Komarekf8188332016-03-09 11:32:08 +01001332 salt '*' virtng.shutdown <vm name>
smolaon1fb381d2016-03-09 11:10:58 +01001333 '''
1334 dom = _get_dom(vm_)
1335 return dom.shutdown() == 0
1336
1337
1338def pause(vm_):
1339 '''
1340 Pause the named vm
1341
1342 CLI Example:
1343
1344 .. code-block:: bash
1345
Ales Komarekf8188332016-03-09 11:32:08 +01001346 salt '*' virtng.pause <vm name>
smolaon1fb381d2016-03-09 11:10:58 +01001347 '''
1348 dom = _get_dom(vm_)
1349 return dom.suspend() == 0
1350
1351
1352def resume(vm_):
1353 '''
1354 Resume the named vm
1355
1356 CLI Example:
1357
1358 .. code-block:: bash
1359
Ales Komarekf8188332016-03-09 11:32:08 +01001360 salt '*' virtng.resume <vm name>
smolaon1fb381d2016-03-09 11:10:58 +01001361 '''
1362 dom = _get_dom(vm_)
1363 return dom.resume() == 0
1364
1365
1366def create(vm_):
1367 '''
1368 Start a defined domain
1369
1370 CLI Example:
1371
1372 .. code-block:: bash
1373
Ales Komarekf8188332016-03-09 11:32:08 +01001374 salt '*' virtng.create <vm name>
smolaon1fb381d2016-03-09 11:10:58 +01001375 '''
1376 dom = _get_dom(vm_)
1377 return dom.create() == 0
1378
1379
1380def start(vm_):
1381 '''
1382 Alias for the obscurely named 'create' function
1383
1384 CLI Example:
1385
1386 .. code-block:: bash
1387
Ales Komarekf8188332016-03-09 11:32:08 +01001388 salt '*' virtng.start <vm name>
smolaon1fb381d2016-03-09 11:10:58 +01001389 '''
1390 return create(vm_)
1391
1392
1393def stop(vm_):
1394 '''
1395 Alias for the obscurely named 'destroy' function
1396
1397 CLI Example:
1398
1399 .. code-block:: bash
1400
Ales Komarekf8188332016-03-09 11:32:08 +01001401 salt '*' virtng.stop <vm name>
smolaon1fb381d2016-03-09 11:10:58 +01001402 '''
1403 return destroy(vm_)
1404
1405
1406def reboot(vm_):
1407 '''
1408 Reboot a domain via ACPI request
1409
1410 CLI Example:
1411
1412 .. code-block:: bash
1413
Ales Komarekf8188332016-03-09 11:32:08 +01001414 salt '*' virtng.reboot <vm name>
smolaon1fb381d2016-03-09 11:10:58 +01001415 '''
1416 dom = _get_dom(vm_)
1417
1418 # reboot has a few modes of operation, passing 0 in means the
1419 # hypervisor will pick the best method for rebooting
1420 return dom.reboot(0) == 0
1421
1422
1423def reset(vm_):
1424 '''
1425 Reset a VM by emulating the reset button on a physical machine
1426
1427 CLI Example:
1428
1429 .. code-block:: bash
1430
Ales Komarekf8188332016-03-09 11:32:08 +01001431 salt '*' virtng.reset <vm name>
smolaon1fb381d2016-03-09 11:10:58 +01001432 '''
1433 dom = _get_dom(vm_)
1434
1435 # reset takes a flag, like reboot, but it is not yet used
1436 # so we just pass in 0
1437 # see: http://libvirt.org/html/libvirt-libvirt.html#virDomainReset
1438 return dom.reset(0) == 0
1439
1440
1441def ctrl_alt_del(vm_):
1442 '''
1443 Sends CTRL+ALT+DEL to a VM
1444
1445 CLI Example:
1446
1447 .. code-block:: bash
1448
Ales Komarekf8188332016-03-09 11:32:08 +01001449 salt '*' virtng.ctrl_alt_del <vm name>
smolaon1fb381d2016-03-09 11:10:58 +01001450 '''
1451 dom = _get_dom(vm_)
1452 return dom.sendKey(0, 0, [29, 56, 111], 3, 0) == 0
1453
1454
1455def create_xml_str(xml):
1456 '''
1457 Start a domain based on the XML passed to the function
1458
1459 CLI Example:
1460
1461 .. code-block:: bash
1462
Ales Komarekf8188332016-03-09 11:32:08 +01001463 salt '*' virtng.create_xml_str <XML in string format>
smolaon1fb381d2016-03-09 11:10:58 +01001464 '''
1465 conn = __get_conn()
1466 return conn.createXML(xml, 0) is not None
1467
1468
1469def create_xml_path(path):
1470 '''
1471 Start a domain based on the XML-file path passed to the function
1472
1473 CLI Example:
1474
1475 .. code-block:: bash
1476
Ales Komarekf8188332016-03-09 11:32:08 +01001477 salt '*' virtng.create_xml_path <path to XML file on the node>
smolaon1fb381d2016-03-09 11:10:58 +01001478 '''
1479 if not os.path.isfile(path):
1480 return False
1481 return create_xml_str(salt.utils.fopen(path, 'r').read())
1482
1483
1484def define_xml_str(xml):
1485 '''
1486 Define a domain based on the XML passed to the function
1487
1488 CLI Example:
1489
1490 .. code-block:: bash
1491
Ales Komarekf8188332016-03-09 11:32:08 +01001492 salt '*' virtng.define_xml_str <XML in string format>
smolaon1fb381d2016-03-09 11:10:58 +01001493 '''
1494 conn = __get_conn()
1495 return conn.defineXML(xml) is not None
1496
1497
1498def define_xml_path(path):
1499 '''
1500 Define a domain based on the XML-file path passed to the function
1501
1502 CLI Example:
1503
1504 .. code-block:: bash
1505
Ales Komarekf8188332016-03-09 11:32:08 +01001506 salt '*' virtng.define_xml_path <path to XML file on the node>
smolaon1fb381d2016-03-09 11:10:58 +01001507
1508 '''
1509 if not os.path.isfile(path):
1510 return False
1511 return define_xml_str(salt.utils.fopen(path, 'r').read())
1512
1513
1514def define_vol_xml_str(xml):
1515 '''
1516 Define a volume based on the XML passed to the function
1517
1518 CLI Example:
1519
1520 .. code-block:: bash
1521
Ales Komarekf8188332016-03-09 11:32:08 +01001522 salt '*' virtng.define_vol_xml_str <XML in string format>
smolaon1fb381d2016-03-09 11:10:58 +01001523 '''
1524 poolname = __salt__['config.get']('libvirt:storagepool', 'default')
1525 conn = __get_conn()
1526 pool = conn.storagePoolLookupByName(str(poolname))
1527 return pool.createXML(xml, 0) is not None
1528
1529
1530def define_vol_xml_path(path):
1531 '''
1532 Define a volume based on the XML-file path passed to the function
1533
1534 CLI Example:
1535
1536 .. code-block:: bash
1537
Ales Komarekf8188332016-03-09 11:32:08 +01001538 salt '*' virtng.define_vol_xml_path <path to XML file on the node>
smolaon1fb381d2016-03-09 11:10:58 +01001539
1540 '''
1541 if not os.path.isfile(path):
1542 return False
1543 return define_vol_xml_str(salt.utils.fopen(path, 'r').read())
1544
1545
1546def migrate_non_shared(vm_, target, ssh=False):
1547 '''
1548 Attempt to execute non-shared storage "all" migration
1549
1550 CLI Example:
1551
1552 .. code-block:: bash
1553
Ales Komarekf8188332016-03-09 11:32:08 +01001554 salt '*' virtng.migrate_non_shared <vm name> <target hypervisor>
smolaon1fb381d2016-03-09 11:10:58 +01001555 '''
1556 cmd = _get_migrate_command() + ' --copy-storage-all ' + vm_\
1557 + _get_target(target, ssh)
1558
1559 return subprocess.Popen(cmd,
1560 shell=True,
1561 stdout=subprocess.PIPE).communicate()[0]
1562
1563
1564def migrate_non_shared_inc(vm_, target, ssh=False):
1565 '''
1566 Attempt to execute non-shared storage "all" migration
1567
1568 CLI Example:
1569
1570 .. code-block:: bash
1571
Ales Komarekf8188332016-03-09 11:32:08 +01001572 salt '*' virtng.migrate_non_shared_inc <vm name> <target hypervisor>
smolaon1fb381d2016-03-09 11:10:58 +01001573 '''
1574 cmd = _get_migrate_command() + ' --copy-storage-inc ' + vm_\
1575 + _get_target(target, ssh)
1576
1577 return subprocess.Popen(cmd,
1578 shell=True,
1579 stdout=subprocess.PIPE).communicate()[0]
1580
1581
1582def migrate(vm_, target, ssh=False):
1583 '''
1584 Shared storage migration
1585
1586 CLI Example:
1587
1588 .. code-block:: bash
1589
Ales Komarekf8188332016-03-09 11:32:08 +01001590 salt '*' virtng.migrate <vm name> <target hypervisor>
smolaon1fb381d2016-03-09 11:10:58 +01001591 '''
1592 cmd = _get_migrate_command() + ' ' + vm_\
1593 + _get_target(target, ssh)
1594
1595 return subprocess.Popen(cmd,
1596 shell=True,
1597 stdout=subprocess.PIPE).communicate()[0]
1598
1599
1600def seed_non_shared_migrate(disks, force=False):
1601 '''
1602 Non shared migration requires that the disks be present on the migration
1603 destination, pass the disks information via this function, to the
1604 migration destination before executing the migration.
1605
1606 CLI Example:
1607
1608 .. code-block:: bash
1609
Ales Komarekf8188332016-03-09 11:32:08 +01001610 salt '*' virtng.seed_non_shared_migrate <disks>
smolaon1fb381d2016-03-09 11:10:58 +01001611 '''
1612 for _, data in disks.items():
1613 fn_ = data['file']
1614 form = data['file format']
1615 size = data['virtual size'].split()[1][1:]
1616 if os.path.isfile(fn_) and not force:
1617 # the target exists, check to see if it is compatible
1618 pre = yaml.safe_load(subprocess.Popen('qemu-img info arch',
1619 shell=True,
1620 stdout=subprocess.PIPE).communicate()[0])
1621 if pre['file format'] != data['file format']\
1622 and pre['virtual size'] != data['virtual size']:
1623 return False
1624 if not os.path.isdir(os.path.dirname(fn_)):
1625 os.makedirs(os.path.dirname(fn_))
1626 if os.path.isfile(fn_):
1627 os.remove(fn_)
1628 cmd = 'qemu-img create -f ' + form + ' ' + fn_ + ' ' + size
1629 subprocess.call(cmd, shell=True)
1630 creds = _libvirt_creds()
1631 cmd = 'chown ' + creds['user'] + ':' + creds['group'] + ' ' + fn_
1632 subprocess.call(cmd, shell=True)
1633 return True
1634
1635
1636def set_autostart(vm_, state='on'):
1637 '''
1638 Set the autostart flag on a VM so that the VM will start with the host
1639 system on reboot.
1640
1641 CLI Example:
1642
1643 .. code-block:: bash
1644
1645 salt "*" virt.set_autostart <vm name> <on | off>
1646 '''
1647
1648 dom = _get_dom(vm_)
1649
1650 if state == 'on':
1651 return dom.setAutostart(1) == 0
1652
1653 elif state == 'off':
1654 return dom.setAutostart(0) == 0
1655
1656 else:
1657 # return False if state is set to something other then on or off
1658 return False
1659
1660
1661def destroy(vm_):
1662 '''
1663 Hard power down the virtual machine, this is equivalent to pulling the
1664 power
1665
1666 CLI Example:
1667
1668 .. code-block:: bash
1669
Ales Komarekf8188332016-03-09 11:32:08 +01001670 salt '*' virtng.destroy <vm name>
smolaon1fb381d2016-03-09 11:10:58 +01001671 '''
1672 dom = _get_dom(vm_)
1673 return dom.destroy() == 0
1674
1675
1676def undefine(vm_):
1677 '''
1678 Remove a defined vm, this does not purge the virtual machine image, and
1679 this only works if the vm is powered down
1680
1681 CLI Example:
1682
1683 .. code-block:: bash
1684
Ales Komarekf8188332016-03-09 11:32:08 +01001685 salt '*' virtng.undefine <vm name>
smolaon1fb381d2016-03-09 11:10:58 +01001686 '''
1687 dom = _get_dom(vm_)
Alexandru Avadanii00f187a2018-06-24 20:36:44 +02001688 if getattr(libvirt, 'VIR_DOMAIN_UNDEFINE_NVRAM', False):
1689 # This one is only in 1.2.8+
1690 return dom.undefineFlags(libvirt.VIR_DOMAIN_UNDEFINE_NVRAM) == 0
1691 else:
1692 return dom.undefine() == 0
smolaon1fb381d2016-03-09 11:10:58 +01001693
1694
1695def purge(vm_, dirs=False):
1696 '''
1697 Recursively destroy and delete a virtual machine, pass True for dir's to
1698 also delete the directories containing the virtual machine disk images -
1699 USE WITH EXTREME CAUTION!
1700
1701 CLI Example:
1702
1703 .. code-block:: bash
1704
Ales Komarekf8188332016-03-09 11:32:08 +01001705 salt '*' virtng.purge <vm name>
smolaon1fb381d2016-03-09 11:10:58 +01001706 '''
1707 disks = get_disks(vm_)
1708 try:
1709 if not destroy(vm_):
1710 return False
1711 except libvirt.libvirtError:
1712 # This is thrown if the machine is already shut down
1713 pass
1714 directories = set()
1715 for disk in disks:
1716 os.remove(disks[disk]['file'])
1717 directories.add(os.path.dirname(disks[disk]['file']))
1718 if dirs:
1719 for dir_ in directories:
1720 shutil.rmtree(dir_)
1721 undefine(vm_)
1722 return True
1723
1724
1725def virt_type():
1726 '''
1727 Returns the virtual machine type as a string
1728
1729 CLI Example:
1730
1731 .. code-block:: bash
1732
Ales Komarekf8188332016-03-09 11:32:08 +01001733 salt '*' virtng.virt_type
smolaon1fb381d2016-03-09 11:10:58 +01001734 '''
1735 return __grains__['virtual']
1736
1737
1738def is_kvm_hyper():
1739 '''
1740 Returns a bool whether or not this node is a KVM hypervisor
1741
1742 CLI Example:
1743
1744 .. code-block:: bash
1745
Ales Komarekf8188332016-03-09 11:32:08 +01001746 salt '*' virtng.is_kvm_hyper
smolaon1fb381d2016-03-09 11:10:58 +01001747 '''
1748 try:
1749 if 'kvm_' not in salt.utils.fopen('/proc/modules').read():
1750 return False
1751 except IOError:
1752 # No /proc/modules? Are we on Windows? Or Solaris?
1753 return False
Oleh Hryhorov7c5dfd32018-01-19 15:43:44 +02001754 return 'libvirtd' in __salt__['cmd.shell'](__grains__['ps'])
smolaon1fb381d2016-03-09 11:10:58 +01001755
1756
1757def is_xen_hyper():
1758 '''
1759 Returns a bool whether or not this node is a XEN hypervisor
1760
1761 CLI Example:
1762
1763 .. code-block:: bash
1764
Ales Komarekf8188332016-03-09 11:32:08 +01001765 salt '*' virtng.is_xen_hyper
smolaon1fb381d2016-03-09 11:10:58 +01001766 '''
1767 try:
1768 if __grains__['virtual_subtype'] != 'Xen Dom0':
1769 return False
1770 except KeyError:
1771 # virtual_subtype isn't set everywhere.
1772 return False
1773 try:
1774 if 'xen_' not in salt.utils.fopen('/proc/modules').read():
1775 return False
1776 except IOError:
1777 # No /proc/modules? Are we on Windows? Or Solaris?
1778 return False
Oleh Hryhorov7c5dfd32018-01-19 15:43:44 +02001779 return 'libvirtd' in __salt__['cmd.shell'](__grains__['ps'])
smolaon1fb381d2016-03-09 11:10:58 +01001780
1781
1782def is_hyper():
1783 '''
1784 Returns a bool whether or not this node is a hypervisor of any kind
1785
1786 CLI Example:
1787
1788 .. code-block:: bash
1789
Ales Komarekf8188332016-03-09 11:32:08 +01001790 salt '*' virtng.is_hyper
smolaon1fb381d2016-03-09 11:10:58 +01001791 '''
1792 try:
1793 import libvirt # pylint: disable=import-error
1794 except ImportError:
1795 # not a usable hypervisor without libvirt module
1796 return False
1797 return is_xen_hyper() or is_kvm_hyper()
1798
1799
1800def vm_cputime(vm_=None):
1801 '''
1802 Return cputime used by the vms on this hyper in a
1803 list of dicts:
1804
1805 .. code-block:: python
1806
1807 [
1808 'your-vm': {
1809 'cputime' <int>
1810 'cputime_percent' <int>
1811 },
1812 ...
1813 ]
1814
1815 If you pass a VM name in as an argument then it will return info
1816 for just the named VM, otherwise it will return all VMs.
1817
1818 CLI Example:
1819
1820 .. code-block:: bash
1821
Ales Komarekf8188332016-03-09 11:32:08 +01001822 salt '*' virtng.vm_cputime
smolaon1fb381d2016-03-09 11:10:58 +01001823 '''
1824 host_cpus = __get_conn().getInfo()[2]
1825
1826 def _info(vm_):
1827 dom = _get_dom(vm_)
1828 raw = dom.info()
1829 vcpus = int(raw[3])
1830 cputime = int(raw[4])
1831 cputime_percent = 0
1832 if cputime:
1833 # Divide by vcpus to always return a number between 0 and 100
1834 cputime_percent = (1.0e-7 * cputime / host_cpus) / vcpus
1835 return {
1836 'cputime': int(raw[4]),
1837 'cputime_percent': int('{0:.0f}'.format(cputime_percent))
1838 }
1839 info = {}
1840 if vm_:
1841 info[vm_] = _info(vm_)
1842 else:
1843 for vm_ in list_vms():
1844 info[vm_] = _info(vm_)
1845 return info
1846
1847
1848def vm_netstats(vm_=None):
1849 '''
1850 Return combined network counters used by the vms on this hyper in a
1851 list of dicts:
1852
1853 .. code-block:: python
1854
1855 [
1856 'your-vm': {
1857 'rx_bytes' : 0,
1858 'rx_packets' : 0,
1859 'rx_errs' : 0,
1860 'rx_drop' : 0,
1861 'tx_bytes' : 0,
1862 'tx_packets' : 0,
1863 'tx_errs' : 0,
1864 'tx_drop' : 0
1865 },
1866 ...
1867 ]
1868
1869 If you pass a VM name in as an argument then it will return info
1870 for just the named VM, otherwise it will return all VMs.
1871
1872 CLI Example:
1873
1874 .. code-block:: bash
1875
Ales Komarekf8188332016-03-09 11:32:08 +01001876 salt '*' virtng.vm_netstats
smolaon1fb381d2016-03-09 11:10:58 +01001877 '''
1878 def _info(vm_):
1879 dom = _get_dom(vm_)
1880 nics = get_nics(vm_)
1881 ret = {
1882 'rx_bytes': 0,
1883 'rx_packets': 0,
1884 'rx_errs': 0,
1885 'rx_drop': 0,
1886 'tx_bytes': 0,
1887 'tx_packets': 0,
1888 'tx_errs': 0,
1889 'tx_drop': 0
1890 }
1891 for attrs in six.itervalues(nics):
1892 if 'target' in attrs:
1893 dev = attrs['target']
1894 stats = dom.interfaceStats(dev)
1895 ret['rx_bytes'] += stats[0]
1896 ret['rx_packets'] += stats[1]
1897 ret['rx_errs'] += stats[2]
1898 ret['rx_drop'] += stats[3]
1899 ret['tx_bytes'] += stats[4]
1900 ret['tx_packets'] += stats[5]
1901 ret['tx_errs'] += stats[6]
1902 ret['tx_drop'] += stats[7]
1903
1904 return ret
1905 info = {}
1906 if vm_:
1907 info[vm_] = _info(vm_)
1908 else:
1909 for vm_ in list_vms():
1910 info[vm_] = _info(vm_)
1911 return info
1912
1913
1914def vm_diskstats(vm_=None):
1915 '''
1916 Return disk usage counters used by the vms on this hyper in a
1917 list of dicts:
1918
1919 .. code-block:: python
1920
1921 [
1922 'your-vm': {
1923 'rd_req' : 0,
1924 'rd_bytes' : 0,
1925 'wr_req' : 0,
1926 'wr_bytes' : 0,
1927 'errs' : 0
1928 },
1929 ...
1930 ]
1931
1932 If you pass a VM name in as an argument then it will return info
1933 for just the named VM, otherwise it will return all VMs.
1934
1935 CLI Example:
1936
1937 .. code-block:: bash
1938
Ales Komarekf8188332016-03-09 11:32:08 +01001939 salt '*' virtng.vm_blockstats
smolaon1fb381d2016-03-09 11:10:58 +01001940 '''
1941 def get_disk_devs(vm_):
1942 doc = minidom.parse(_StringIO(get_xml(vm_)))
1943 disks = []
1944 for elem in doc.getElementsByTagName('disk'):
1945 targets = elem.getElementsByTagName('target')
1946 target = targets[0]
1947 disks.append(target.getAttribute('dev'))
1948 return disks
1949
1950 def _info(vm_):
1951 dom = _get_dom(vm_)
1952 # Do not use get_disks, since it uses qemu-img and is very slow
1953 # and unsuitable for any sort of real time statistics
1954 disks = get_disk_devs(vm_)
1955 ret = {'rd_req': 0,
1956 'rd_bytes': 0,
1957 'wr_req': 0,
1958 'wr_bytes': 0,
1959 'errs': 0
1960 }
1961 for disk in disks:
1962 stats = dom.blockStats(disk)
1963 ret['rd_req'] += stats[0]
1964 ret['rd_bytes'] += stats[1]
1965 ret['wr_req'] += stats[2]
1966 ret['wr_bytes'] += stats[3]
1967 ret['errs'] += stats[4]
1968
1969 return ret
1970 info = {}
1971 if vm_:
1972 info[vm_] = _info(vm_)
1973 else:
1974 # Can not run function blockStats on inactive VMs
1975 for vm_ in list_active_vms():
1976 info[vm_] = _info(vm_)
1977 return info