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