-
Notifications
You must be signed in to change notification settings - Fork 364
/
vms.py
2249 lines (1890 loc) · 82.4 KB
/
vms.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#
# Project Kimchi
#
# Copyright IBM Corp, 2015-2017
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
import copy
import os
import platform
import pwd
import random
import signal
import socket
import string
import subprocess
import threading
import time
import uuid
from xml.etree import ElementTree
import libvirt
import lxml.etree as ET
import paramiko
from lxml import etree
from lxml import objectify
from lxml.builder import E
from wok import websocket
from wok.asynctask import AsyncTask
from wok.config import config
from wok.exception import InvalidOperation
from wok.exception import InvalidParameter
from wok.exception import NotFoundError
from wok.exception import OperationFailed
from wok.model.tasks import TaskModel
from wok.plugins.kimchi import model
from wok.plugins.kimchi import serialconsole
from wok.plugins.kimchi.config import config as kimchi_config
from wok.plugins.kimchi.config import get_kimchi_version
from wok.plugins.kimchi.config import READONLY_POOL_TYPE
from wok.plugins.kimchi.kvmusertests import UserTests
from wok.plugins.kimchi.model.config import CapabilitiesModel
from wok.plugins.kimchi.model.cpuinfo import CPUInfoModel
from wok.plugins.kimchi.model.featuretests import FeatureTests
from wok.plugins.kimchi.model.templates import PPC_MEM_ALIGN
from wok.plugins.kimchi.model.templates import TemplateModel
from wok.plugins.kimchi.model.templates import validate_memory
from wok.plugins.kimchi.model.utils import get_ascii_nonascii_name
from wok.plugins.kimchi.model.utils import get_metadata_node
from wok.plugins.kimchi.model.utils import get_vm_name
from wok.plugins.kimchi.model.utils import remove_metadata_node
from wok.plugins.kimchi.model.utils import set_metadata_node
from wok.plugins.kimchi.osinfo import defaults
from wok.plugins.kimchi.osinfo import MEM_DEV_SLOTS
from wok.plugins.kimchi.screenshot import VMScreenshot
from wok.plugins.kimchi.utils import get_next_clone_name
from wok.plugins.kimchi.utils import is_s390x
from wok.plugins.kimchi.utils import template_name_from_uri
from wok.plugins.kimchi.xmlutils.bootorder import get_bootmenu_node
from wok.plugins.kimchi.xmlutils.bootorder import get_bootorder_node
from wok.plugins.kimchi.xmlutils.cpu import get_topology_xml
from wok.plugins.kimchi.xmlutils.disk import get_vm_disk_info
from wok.plugins.kimchi.xmlutils.disk import get_vm_disks
from wok.rollbackcontext import RollbackContext
from wok.utils import convert_data_size
from wok.utils import import_class
from wok.utils import run_command
from wok.utils import run_setfacl_set_attr
from wok.utils import wok_log
from wok.xmlutils.utils import dictize
from wok.xmlutils.utils import xml_item_insert
from wok.xmlutils.utils import xml_item_remove
from wok.xmlutils.utils import xml_item_update
from wok.xmlutils.utils import xpath_get_text
from .utils import has_cpu_numa
from .utils import set_numa_memory
DOM_STATE_MAP = {
0: 'nostate',
1: 'running',
2: 'blocked',
3: 'paused',
4: 'shutdown',
5: 'shutoff',
6: 'crashed',
7: 'pmsuspended',
}
# update parameters which are updatable when the VM is online
VM_ONLINE_UPDATE_PARAMS = [
'cpu_info',
'graphics',
'groups',
'memory',
'users',
'autostart',
]
# update parameters which are updatable when the VM is offline
VM_OFFLINE_UPDATE_PARAMS = [
'cpu_info',
'graphics',
'groups',
'memory',
'name',
'users',
'bootorder',
'bootmenu',
'description',
'title',
'console',
'autostart',
]
XPATH_DOMAIN_DISK = "/domain/devices/disk[@device='disk']/source/@file"
XPATH_DOMAIN_DISK_BY_FILE = "./devices/disk[@device='disk']/source[@file='%s']"
XPATH_DOMAIN_NAME = '/domain/name'
XPATH_DOMAIN_MAC = '/domain/devices/interface/mac/@address'
XPATH_DOMAIN_MAC_BY_ADDRESS = "./devices/interface/mac[@address='%s']"
XPATH_DOMAIN_MEMORY = '/domain/memory'
XPATH_DOMAIN_MEMORY_UNIT = '/domain/memory/@unit'
XPATH_DOMAIN_UUID = '/domain/uuid'
XPATH_DOMAIN_DEV_CPU_ID = '/domain/devices/spapr-cpu-socket/@id'
XPATH_DOMAIN_CONSOLE_TARGET = '/domain/devices/console/target/@type'
XPATH_BOOT = 'os/boot/@dev'
XPATH_BOOTMENU = 'os/bootmenu/@enable'
XPATH_CPU = './cpu'
XPATH_DESCRIPTION = './description'
XPATH_MEMORY = './memory'
XPATH_NAME = './name'
XPATH_NUMA_CELL = './cpu/numa/cell'
XPATH_SNAP_VM_NAME = './domain/name'
XPATH_SNAP_VM_UUID = './domain/uuid'
XPATH_TITLE = './title'
XPATH_TOPOLOGY = './cpu/topology'
XPATH_VCPU = './vcpu'
XPATH_MAX_MEMORY = './maxMemory'
XPATH_CONSOLE_TARGET = './devices/console/target'
# key: VM name; value: lock object
vm_locks = {}
class VMsModel(object):
def __init__(self, **kargs):
self.conn = kargs['conn']
self.objstore = kargs['objstore']
self.caps = CapabilitiesModel(**kargs)
self.task = TaskModel(**kargs)
def create(self, params):
t_name = template_name_from_uri(params['template'])
vm_list = self.get_list()
name = get_vm_name(params.get('name'), t_name, vm_list)
# incoming text, from js json, is unicode, do not need decode
if name in vm_list:
raise InvalidOperation('KCHVM0001E', {'name': name})
vm_overrides = dict()
pool_uri = params.get('storagepool')
if pool_uri:
vm_overrides['storagepool'] = pool_uri
vm_overrides['fc_host_support'] = self.caps.fc_host_support
t = TemplateModel.get_template(
t_name, self.objstore, self.conn, vm_overrides)
if not self.caps.qemu_stream and t.info.get('iso_stream', False):
raise InvalidOperation('KCHVM0005E')
t.validate()
data = {
'name': name,
'template': t,
'graphics': params.get('graphics', {}),
'title': params.get('title', ''),
'description': params.get('description', ''),
}
taskid = AsyncTask(
f'/plugins/kimchi/vms/{name}', self._create_task, data).id
return self.task.lookup(taskid)
def _create_task(self, cb, params):
"""
params: A dict with the following values:
- vm_uuid: The UUID of the VM being created
- template: The template being used to create the VM
- name: The name for the new VM
"""
vm_uuid = str(uuid.uuid4())
title = params.get('title', '')
description = params.get('description', '')
t = params['template']
name, nonascii_name = get_ascii_nonascii_name(params['name'])
conn = self.conn.get()
cb('Storing VM icon')
# Store the icon for displaying later
icon = t.info.get('icon')
if icon:
try:
with self.objstore as session:
session.store('vm', vm_uuid, {
'icon': icon}, get_kimchi_version())
except Exception as e:
# It is possible to continue Kimchi executions without store
# vm icon info
wok_log.error(
f'Error trying to update database with guest '
f'icon information due error: {e}'
)
cb('Provisioning storages for new VM')
vol_list = t.fork_vm_storage(vm_uuid)
graphics = params.get('graphics', {})
stream_protocols = self.caps.libvirt_stream_protocols
xml = t.to_vm_xml(
name,
vm_uuid,
libvirt_stream_protocols=stream_protocols,
graphics=graphics,
mem_hotplug_support=self.caps.mem_hotplug_support,
title=title,
description=description,
)
cb('Defining new VM')
try:
conn.defineXML(xml)
except libvirt.libvirtError as e:
for v in vol_list:
vol = conn.storageVolLookupByPath(v['path'])
vol.delete(0)
raise OperationFailed(
'KCHVM0007E', {'name': name, 'err': e.get_error_message()}
)
cb('Updating VM metadata')
meta_elements = []
distro = t.info.get('os_distro')
version = t.info.get('os_version')
if distro is not None:
meta_elements.append(E.os({'distro': distro, 'version': version}))
if nonascii_name is not None:
meta_elements.append(E.name(nonascii_name))
set_metadata_node(VMModel.get_vm(name, self.conn), meta_elements)
cb('OK', True)
def get_list(self):
return VMsModel.get_vms(self.conn)
@staticmethod
def get_vms(conn):
conn_ = conn.get()
names = []
for dom in conn_.listAllDomains(0):
nonascii_xml = get_metadata_node(dom, 'name')
if nonascii_xml:
nonascii_node = ET.fromstring(nonascii_xml)
names.append(nonascii_node.text)
else:
names.append(dom.name())
names = sorted(names, key=str.lower)
return names
class VMModel(object):
def __init__(self, **kargs):
self.conn = kargs['conn']
self.objstore = kargs['objstore']
self.caps = CapabilitiesModel(**kargs)
self.vmscreenshot = VMScreenshotModel(**kargs)
self.users = import_class(
'wok.plugins.kimchi.model.users.UsersModel')(**kargs)
self.groups = import_class('wok.plugins.kimchi.model.groups.GroupsModel')(
**kargs
)
self.vms = VMsModel(**kargs)
self.task = TaskModel(**kargs)
self.storagepool = model.storagepools.StoragePoolModel(**kargs)
self.storagevolume = model.storagevolumes.StorageVolumeModel(**kargs)
self.storagevolumes = model.storagevolumes.StorageVolumesModel(**kargs)
cls = import_class(
'wok.plugins.kimchi.model.vmsnapshots.VMSnapshotModel')
self.vmsnapshot = cls(**kargs)
cls = import_class(
'wok.plugins.kimchi.model.vmsnapshots.VMSnapshotsModel')
self.vmsnapshots = cls(**kargs)
self.stats = {}
self._serial_procs = []
def has_topology(self, dom):
xml = dom.XMLDesc(0)
sockets = xpath_get_text(xml, XPATH_TOPOLOGY + '/@sockets')
cores = xpath_get_text(xml, XPATH_TOPOLOGY + '/@cores')
threads = xpath_get_text(xml, XPATH_TOPOLOGY + '/@threads')
return sockets and cores and threads
def update(self, name, params):
if platform.machine() not in ['s390x', 's390'] and 'console' in params:
raise InvalidParameter('KCHVM0087E')
lock = vm_locks.get(name)
if lock is None:
lock = threading.Lock()
vm_locks[name] = lock
with lock:
dom = self.get_vm(name, self.conn)
if 'autostart' in params:
dom.setAutostart(1 if params['autostart'] is True else 0)
# You can only change <maxMemory> offline, updating guest XML
if (
('memory' in params)
and ('maxmemory' in params['memory'])
and (DOM_STATE_MAP[dom.info()[0]] != 'shutoff')
):
raise InvalidParameter('KCHVM0080E')
if DOM_STATE_MAP[dom.info()[0]] == 'shutoff':
ext_params = set(params.keys()) - set(VM_OFFLINE_UPDATE_PARAMS)
if len(ext_params) > 0:
raise InvalidParameter(
'KCHVM0073E', {'params': ', '.join(ext_params)}
)
else:
ext_params = set(params.keys()) - set(VM_ONLINE_UPDATE_PARAMS)
if len(ext_params) > 0:
raise InvalidParameter(
'KCHVM0074E', {'params': ', '.join(ext_params)}
)
# METADATA can be updated offline or online
self._vm_update_access_metadata(dom, params)
# GRAPHICS can be updated offline or online
if 'graphics' in params:
# some parameters cannot change while vm is running
if DOM_STATE_MAP[dom.info()[0]] != 'shutoff':
if 'type' in params['graphics']:
raise InvalidParameter(
'KCHVM0074E', {'params': 'graphics type'}
)
dom = self._update_graphics(dom, params)
# Live updates
if dom.isActive():
self._live_vm_update(dom, params)
vm_name = name
if DOM_STATE_MAP[dom.info()[0]] == 'shutoff':
vm_name, dom = self._static_vm_update(name, dom, params)
return vm_name
def clone(self, name):
"""Clone a virtual machine based on an existing one.
The new virtual machine will have the exact same configuration as the
original VM, except for the name, UUID, MAC addresses and disks. The
name will have the form "<name>-clone-<number>", with <number> starting
at 1; the UUID will be generated randomly; the MAC addresses will be
generated randomly with no conflicts within the original and the new
VM; and the disks will be new volumes [mostly] on the same storage
pool, with the same content as the original disks. The storage pool
'default' will always be used when cloning SCSI and iSCSI disks and
when the original storage pool cannot hold the new volume.
An exception will be raised if the virtual machine <name> is not
shutoff, if there is no available space to copy a new volume to the
storage pool 'default' (when there was also no space to copy it to the
original storage pool) and if one of the virtual machine's disks belong
to a storage pool not supported by Kimchi.
Parameters:
name -- The name of the existing virtual machine to be cloned.
Return:
A Task running the clone operation.
"""
# VM must be shutoff in order to clone it
info = self.lookup(name)
if info['state'] != 'shutoff':
raise InvalidParameter('KCHVM0033E', {'name': name})
# the new VM's name will be used as the Task's 'target_uri' so it needs
# to be defined now.
vms_being_created = []
# lookup names of VMs being created right now
with self.objstore as session:
task_names = session.get_list('task')
for tn in task_names:
t = session.get('task', tn)
if t['target_uri'].startswith('/plugins/kimchi/vms/'):
uri_name = t['target_uri'].lstrip('/plugins/kimchi/vms/')
vms_being_created.append(uri_name)
current_vm_names = self.vms.get_list() + vms_being_created
new_name = get_next_clone_name(current_vm_names, name, ts=True)
# create a task with the actual clone function
taskid = AsyncTask(
f'/plugins/kimchi/vms/{new_name}/clone',
self._clone_task,
{'name': name, 'new_name': new_name},
).id
return self.task.lookup(taskid)
def _clone_task(self, cb, params):
"""Asynchronous function which performs the clone operation.
Parameters:
cb -- A callback function to signal the Task's progress.
params -- A dict with the following values:
"name": the name of the original VM.
"new_name": the name of the new VM.
"""
name = params['name']
new_name = params['new_name']
# fetch base XML
cb('reading source VM XML')
try:
vir_dom = self.get_vm(name, self.conn)
flags = libvirt.VIR_DOMAIN_XML_SECURE
xml = vir_dom.XMLDesc(flags)
except libvirt.libvirtError as e:
raise OperationFailed('KCHVM0035E', {'name': name, 'err': str(e)})
# update UUID
cb('updating VM UUID')
old_uuid = xpath_get_text(xml, XPATH_DOMAIN_UUID)[0]
new_uuid = str(uuid.uuid4())
xml = xml_item_update(xml, './uuid', new_uuid)
# update MAC addresses
cb('updating VM MAC addresses')
xml = self._clone_update_mac_addresses(xml)
with RollbackContext() as rollback:
# copy disks
cb('copying VM disks')
xml = self._clone_update_disks(xml, rollback)
# update objstore entry
cb('updating object store')
self._clone_update_objstore(old_uuid, new_uuid, rollback)
# update name
cb('updating VM name')
new_name, nonascii_name = get_ascii_nonascii_name(new_name)
xml = xml_item_update(xml, './name', new_name)
# create new guest
cb('defining new VM')
try:
vir_conn = self.conn.get()
dom = vir_conn.defineXML(xml)
self._update_metadata_name(dom, nonascii_name)
except libvirt.libvirtError as e:
raise OperationFailed(
'KCHVM0035E', {'name': name, 'err': str(e)})
rollback.commitAll()
cb('OK', True)
@staticmethod
def _clone_update_mac_addresses(xml):
"""Update the MAC addresses with new values in the XML descriptor of a
cloning domain.
The new MAC addresses will be generated randomly, and their values are
guaranteed to be distinct from the ones in the original VM.
Arguments:
xml -- The XML descriptor of the original domain.
Return:
The XML descriptor <xml> with the new MAC addresses instead of the
old ones.
"""
old_macs = xpath_get_text(xml, XPATH_DOMAIN_MAC)
new_macs = []
for mac in old_macs:
while True:
new_mac = model.vmifaces.VMIfacesModel.random_mac()
# make sure the new MAC doesn't conflict with the original VM
# and with the new values on the new VM.
if new_mac not in (old_macs + new_macs):
new_macs.append(new_mac)
break
xml = xml_item_update(
xml, XPATH_DOMAIN_MAC_BY_ADDRESS % mac, new_mac, 'address'
)
return xml
def _clone_update_disks(self, xml, rollback):
"""Clone disks from a virtual machine. The disks are copied as new
volumes and the new VM's XML is updated accordingly.
Arguments:
xml -- The XML descriptor of the original VM + new value for
"/domain/uuid".
rollback -- A rollback context so the new volumes can be removed if an
error occurs during the cloning operation.
Return:
The XML descriptor <xml> with the new disk paths instead of the
old ones.
"""
# the UUID will be used to create the disk paths
uuid = xpath_get_text(xml, XPATH_DOMAIN_UUID)[0]
all_paths = xpath_get_text(xml, XPATH_DOMAIN_DISK)
vir_conn = self.conn.get()
domain_name = xpath_get_text(xml, XPATH_DOMAIN_NAME)[0]
for i, path in enumerate(all_paths):
try:
vir_orig_vol = vir_conn.storageVolLookupByPath(path)
vir_pool = vir_orig_vol.storagePoolLookupByVolume()
orig_pool_name = vir_pool.name()
orig_vol_name = vir_orig_vol.name()
except libvirt.libvirtError as e:
raise OperationFailed(
'KCHVM0035E', {'name': domain_name, 'err': str(e)}
)
orig_pool = self.storagepool.lookup(orig_pool_name)
orig_vol = self.storagevolume.lookup(orig_pool_name, orig_vol_name)
new_pool_name = orig_pool_name
new_pool = orig_pool
if orig_pool['type'] in ['dir', 'netfs', 'logical']:
# if a volume in a pool 'dir', 'netfs' or 'logical' cannot hold
# a new volume with the same size, the pool 'default' should
# be used
if orig_vol['capacity'] > orig_pool['available']:
wok_log.warning(
f"storage pool '{orig_pool_name}' doesn't have "
f'enough free space to store image '
f"'{path}'; falling back to 'default'"
)
new_pool_name = 'default'
new_pool = self.storagepool.lookup('default')
# ...and if even the pool 'default' cannot hold a new
# volume, raise an exception
if orig_vol['capacity'] > new_pool['available']:
domain_name = xpath_get_text(xml, XPATH_DOMAIN_NAME)[0]
raise InvalidOperation(
'KCHVM0034E', {'name': domain_name})
elif orig_pool['type'] in ['scsi', 'iscsi']:
# SCSI and iSCSI always fall back to the storage pool 'default'
wok_log.warning(
f'cannot create new volume for clone in '
f"storage pool '{orig_pool_name}'; falling back to "
f"'default'"
)
new_pool_name = 'default'
new_pool = self.storagepool.lookup('default')
# if the pool 'default' cannot hold a new volume, raise
# an exception
if orig_vol['capacity'] > new_pool['available']:
domain_name = xpath_get_text(xml, XPATH_DOMAIN_NAME)[0]
raise InvalidOperation('KCHVM0034E', {'name': domain_name})
else:
# unexpected storage pool type
raise InvalidOperation(
'KCHPOOL0014E', {'type': orig_pool['type']})
# new volume name: <UUID>-<loop-index>.<original extension>
# e.g. 1234-5678-9012-3456-0.img
ext = os.path.splitext(path)[1]
new_vol_name = f'{uuid}-{i}{ext}'
task = self.storagevolume.clone(
orig_pool_name, orig_vol_name, new_name=new_vol_name
)
self.task.wait(task['id'], 3600) # 1 h
# get the new volume path and update the XML descriptor
new_vol = self.storagevolume.lookup(new_pool_name, new_vol_name)
xml = xml_item_update(
xml, XPATH_DOMAIN_DISK_BY_FILE % path, new_vol['path'], 'file'
)
# remove the new volume should an error occur later
rollback.prependDefer(
self.storagevolume.delete, new_pool_name, new_vol_name
)
return xml
def _clone_update_objstore(self, old_uuid, new_uuid, rollback):
"""Update Kimchi's object store with the cloning VM.
Arguments:
old_uuid -- The UUID of the original VM.
new_uuid -- The UUID of the new, clonning VM.
rollback -- A rollback context so the object store entry can be removed
if an error occurs during the cloning operation.
"""
with self.objstore as session:
try:
vm = session.get('vm', old_uuid)
icon = vm['icon']
session.store('vm', new_uuid, {
'icon': icon}, get_kimchi_version())
except NotFoundError:
# if we cannot find an object store entry for the original VM,
# don't store one with an empty value.
pass
else:
# we need to define a custom function to prepend to the
# rollback context because the object store session needs to be
# opened and closed correctly (i.e. "prependDefer" only
# accepts one command at a time but we need more than one to
# handle an object store).
def _rollback_objstore():
with self.objstore as session_rb:
session_rb.delete('vm', new_uuid, ignore_missing=True)
# remove the new object store entry should an error occur later
rollback.prependDefer(_rollback_objstore)
def _build_access_elem(self, dom, users, groups):
auth = config.get('authentication', 'method')
access_xml = get_metadata_node(dom, 'access')
auth_elem = None
if not access_xml:
# there is no metadata element 'access'
access_elem = E.access()
else:
access_elem = ET.fromstring(access_xml)
same_auth = access_elem.xpath(f'./auth[@type="{auth}"]')
if len(same_auth) > 0:
# there is already a sub-element 'auth' with the same type;
# update it.
auth_elem = same_auth[0]
if users is not None:
for u in auth_elem.findall('user'):
auth_elem.remove(u)
if groups is not None:
for g in auth_elem.findall('group'):
auth_elem.remove(g)
if auth_elem is None:
# there is no sub-element 'auth' with the same type
# (or no 'auth' at all); create it.
auth_elem = E.auth(type=auth)
access_elem.append(auth_elem)
if users is not None:
for u in users:
auth_elem.append(E.user(u))
if groups is not None:
for g in groups:
auth_elem.append(E.group(g))
return access_elem
def _vm_update_access_metadata(self, dom, params):
users = groups = None
if 'users' in params:
users = params['users']
for user in users:
if not self.users.validate(user):
raise InvalidParameter('KCHVM0027E', {'users': user})
if 'groups' in params:
groups = params['groups']
for group in groups:
if not self.groups.validate(group):
raise InvalidParameter('KCHVM0028E', {'groups': group})
if users is None and groups is None:
return
node = self._build_access_elem(dom, users, groups)
set_metadata_node(dom, [node])
def _get_access_info(self, dom):
users = groups = list()
access_xml = get_metadata_node(
dom, 'access') or """<access></access>"""
access_info = dictize(access_xml)
auth = config.get('authentication', 'method')
if 'auth' in access_info['access'] and (
'type' in access_info['access']['auth']
or len(access_info['access']['auth']) > 1
):
users = xpath_get_text(
access_xml, f"/access/auth[@type='{auth}']/user")
groups = xpath_get_text(
access_xml, f"/access/auth[@type='{auth}']/group")
elif auth == 'pam':
# Compatible to old permission tagging
users = xpath_get_text(access_xml, '/access/user')
groups = xpath_get_text(access_xml, '/access/group')
return users, groups
@staticmethod
def vm_get_os_metadata(dom):
os_xml = get_metadata_node(dom, 'os') or """<os></os>"""
os_elem = ET.fromstring(os_xml)
return (os_elem.attrib.get('version'), os_elem.attrib.get('distro'))
def _update_graphics(self, dom, params):
root = objectify.fromstring(dom.XMLDesc(0))
graphics = root.devices.find('graphics')
if graphics is None:
return dom
password = params['graphics'].get('passwd')
if password is not None and len(password.strip()) == 0:
password = ''.join(random.sample(
string.ascii_letters + string.digits, 8))
if password is not None:
graphics.attrib['passwd'] = password
expire = params['graphics'].get('passwdValidTo')
to = graphics.attrib.get('passwdValidTo')
if to is not None:
if time.mktime(time.strptime(to, '%Y-%m-%dT%H:%M:%S')) - time.time() <= 0:
expire = expire if expire is not None else 30
if expire is not None:
expire_time = time.gmtime(time.time() + float(expire))
valid_to = time.strftime('%Y-%m-%dT%H:%M:%S', expire_time)
graphics.attrib['passwdValidTo'] = valid_to
gtype = params['graphics'].get('type')
if gtype is not None:
graphics.attrib['type'] = gtype
conn = self.conn.get()
if not dom.isActive():
return conn.defineXML(ET.tostring(root, encoding='unicode'))
xml = dom.XMLDesc(libvirt.VIR_DOMAIN_XML_SECURE)
dom.updateDeviceFlags(
etree.tostring(graphics).decode(
'utf-8'), libvirt.VIR_DOMAIN_AFFECT_LIVE
)
return conn.defineXML(xml)
def _backup_snapshots(self, snap, all_info):
""" Append "snap" and the children of "snap" to the list "all_info".
The list *must* always contain the parent snapshots before their
children so the function "_redefine_snapshots" can work correctly.
Arguments:
snap -- a native domain snapshot.
all_info -- a list of dict keys:
"{'xml': <snap XML>, 'current': <is snap current?>'}"
"""
all_info.append({'xml': snap.getXMLDesc(
0), 'current': snap.isCurrent(0)})
for child in snap.listAllChildren(0):
self._backup_snapshots(child, all_info)
def _redefine_snapshots(self, dom, all_info):
""" Restore the snapshots stored in "all_info" to the domain "dom".
Arguments:
dom -- the domain which will have its snapshots restored.
all_info -- a list of dict keys, as described in "_backup_snapshots",
containing the original snapshot information.
"""
for info in all_info:
flags = libvirt.VIR_DOMAIN_SNAPSHOT_CREATE_REDEFINE
if info['current']:
flags |= libvirt.VIR_DOMAIN_SNAPSHOT_CREATE_CURRENT
# Snapshot XML contains the VM xml from the time it was created.
# Thus VM name and uuid must be updated to current ones. Otherwise,
# when reverted, the vm name will be inconsistent.
name = dom.name()
uuid = dom.UUIDString()
xml = xml_item_update(info['xml'], XPATH_SNAP_VM_NAME, name, None)
xml = xml_item_update(xml, XPATH_SNAP_VM_UUID, uuid, None)
dom.snapshotCreateXML(xml, flags)
def _update_metadata_name(self, dom, nonascii_name):
if nonascii_name is not None:
set_metadata_node(dom, [E.name(nonascii_name)])
else:
remove_metadata_node(dom, 'name')
def _update_bootorder(self, xml, params):
# get element tree from xml
et = ET.fromstring(xml)
# get machine type
os = et.find('os')
# add new bootorder
if 'bootorder' in params:
# remove old order
[os.remove(device) for device in os.findall('boot')]
for device in get_bootorder_node(params['bootorder']):
os.append(device)
# update bootmenu
if params.get('bootmenu') is False:
[os.remove(bm) for bm in os.findall('bootmenu')]
elif params.get('bootmenu') is True:
os.append(get_bootmenu_node())
# update <os>
return ET.tostring(et, encoding='unicode')
def _update_s390x_console(self, xml, params):
if xpath_get_text(xml, XPATH_DOMAIN_CONSOLE_TARGET):
# if console is defined, update console
return xml_item_update(
xml, XPATH_CONSOLE_TARGET, params.get('console'), 'type'
)
# if console is not defined earlier, add console
console = E.console(type='pty')
console.append(E.target(type=params.get('console'), port='0'))
et = ET.fromstring(xml)
devices = et.find('devices')
devices.append(console)
return ET.tostring(et, encoding='unicode')
def _update_title(self, new_xml, title):
if len(xpath_get_text(new_xml, XPATH_TITLE)) > 0:
new_xml = xml_item_update(
new_xml, XPATH_TITLE, title, None)
else:
et = ET.fromstring(new_xml)
et.append(E.title(title))
new_xml = ET.tostring(et, encoding='unicode')
return new_xml
def _update_description(self, new_xml, description):
if len(xpath_get_text(new_xml, XPATH_DESCRIPTION)) > 0:
new_xml = xml_item_update(
new_xml, XPATH_DESCRIPTION, description, None
)
else:
et = ET.fromstring(new_xml)
et.append(E.description(description))
new_xml = ET.tostring(et, encoding='unicode')
return new_xml
def _update_topology(self, dom, new_xml, topology):
sockets = str(topology['sockets'])
cores = str(topology['cores'])
threads = str(topology['threads'])
if self.has_topology(dom):
# topology is being updated
xpath = XPATH_TOPOLOGY
new_xml = xml_item_update(new_xml, xpath, sockets, 'sockets')
new_xml = xml_item_update(new_xml, xpath, cores, 'cores')
new_xml = xml_item_update(new_xml, xpath, threads, 'threads')
else:
# topology is being added
new_xml = xml_item_insert(
new_xml, XPATH_CPU, get_topology_xml(topology)
)
return new_xml
def _static_vm_update(self, vm_name, dom, params):
old_xml = new_xml = dom.XMLDesc(libvirt.VIR_DOMAIN_XML_SECURE)
params = copy.deepcopy(params)
# Update name
name = params.get('name')
nonascii_name = None
if name is not None:
name, nonascii_name = get_ascii_nonascii_name(name)
new_xml = xml_item_update(new_xml, XPATH_NAME, name, None)
if 'title' in params:
new_xml = self._update_title(new_xml, params['title'])
if 'description' in params:
new_xml = self._update_description(new_xml, params['description'])
# Update CPU info
cpu_info = params.get('cpu_info', {})
cpu_info = self._update_cpu_info(new_xml, dom, cpu_info)
vcpus = str(cpu_info['vcpus'])
new_xml = xml_item_update(new_xml, XPATH_VCPU, vcpus, 'current')
maxvcpus = str(cpu_info['maxvcpus'])
new_xml = xml_item_update(new_xml, XPATH_VCPU, maxvcpus, None)
topology = cpu_info['topology']
if topology:
new_xml = self._update_topology(dom, new_xml, topology)
elif self.has_topology(dom):
# topology is being undefined: remove it
new_xml = xml_item_remove(new_xml, XPATH_TOPOLOGY)
# Updating memory
if 'memory' in params and params['memory'] != {}:
new_xml = self._update_memory_config(new_xml, params, dom)
# update bootorder or bootmenu
if 'bootorder' in params or 'bootmenu' in params:
new_xml = self._update_bootorder(new_xml, params)
if platform.machine() in ['s390', 's390x'] and params.get('console'):
new_xml = self._update_s390x_console(new_xml, params)
snapshots_info = []
conn = self.conn.get()
try:
if 'name' in params:
lflags = libvirt.VIR_DOMAIN_SNAPSHOT_LIST_ROOTS
dflags = (
libvirt.VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN
| libvirt.VIR_DOMAIN_SNAPSHOT_DELETE_METADATA_ONLY
)
for virt_snap in dom.listAllSnapshots(lflags):
snapshots_info.append(
{
'xml': virt_snap.getXMLDesc(0),
'current': virt_snap.isCurrent(0),
}
)
self._backup_snapshots(virt_snap, snapshots_info)
virt_snap.delete(dflags)
# Undefine old vm, only if name is going to change
dom.undefine()
new_xml = new_xml.decode(
'utf-8') if isinstance(new_xml, bytes) else new_xml
dom = conn.defineXML(new_xml)
self._update_metadata_name(dom, nonascii_name)
if 'name' in params:
self._redefine_snapshots(dom, snapshots_info)
except libvirt.libvirtError as e:
dom = conn.defineXML(old_xml)
if 'name' in params:
self._redefine_snapshots(dom, snapshots_info)
raise OperationFailed(
'KCHVM0008E', {'name': vm_name, 'err': e.get_error_message()}
)
if name is not None:
vm_name = name
return nonascii_name if nonascii_name is not None else vm_name, dom
def _get_new_memory(self, root, newMem, oldMem, memDevs):
memDevsAmount = self._get_mem_dev_total_size(