-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgeexmox-installer.py
executable file
·1537 lines (1329 loc) · 60.2 KB
/
geexmox-installer.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
#!/usr/bin/python
import sys
import os
import csv
import subprocess
import StringIO
import re
import pprint
import shlex
import urllib
import contextlib
import traceback
import glob
import collections
import copy
import stat
ROOT_EUID = 0
# Console ESC flags
BOLD = '\x1b[1m'
DIMMED = '\x1b[2m'
# Console ESC colors
RED_COLOR = '\x1b[31m'
LIGHT_RED_COLOR = '\x1b[91m'
YELLOW_COLOR = '\x1b[93m'
GREEN_COLOR = '\x1b[92m'
# Reset all console ESC flags
RESET_ALL = '\x1b[0m'
APT_CONFIGS = [
('https://dendygeeks.github.io/geexmox-pve-overrides/etc/apt/preferences.d/geexmox',
'/etc/apt/preferences.d/geexmox'),
('https://dendygeeks.github.io/geexmox-pve-overrides/etc/apt/sources.list.d/geexmox.list',
'/etc/apt/sources.list.d/geexmox.list')
]
MAX_PASSTHROUGH = 4
class ElephantArt:
# unpacks mascot ASCII art from embedded text;
# for more see decoration/html2ascii.py
DATA = '''
tar
QlpoOTFBWSZTWcxvvLEAA13//9+5+OTLV/eeXBgQQf+hHgQIcXAABAASTABAAASH////0AYesx4qRc6OtQ4DdnQGmiENAU2qej1IxAabUxAADEAAA9QAD1MT
Jk9EDU8mkTUoNHpAHqaaADIAANAAAAAAAANDjQ0aNBo0DQAAAAAMgAAADQGQAGDT0iSmFPRHqGgbUAPUGgAAAAAAAAAAASEiCCaoeTU9QzKaemoaAAAPUAAG
QAAAB6ntKc2/y9XCyBeFgJJcT63HhyMrZngJK50g9/xj3uU8Tp+ZBShw1dLBSBR64R8U8ZCKoUQtkgH1GaEDExkYwc8XwiAJAnVghDKbGdCiVGXDDzkM+EMv
CIGBSmzbNSoVJoKI3rJgSLRnhgxiaydPY3zDs8hqwzS+jokHZRI2468U7JW6JzvmZmFpBg2A22gG443eJKPpa4RiKGZVCmCI5N9rY++q11kyNMccGZscVUSo
nURBra6GWY2kCANuJJ4ndTiwgX9JuVWq4dOzsKQBNppU6lVLQJU7ph00U5ASAECBJis5myGQCA0mes/HnNLHGZDLEK4vivAKCmpZw0uMQ2h2iBatV5Q2Nhdr
T4Ok4NfgUh4Zx6O9v4M0zNO1mn+AVCBZwEBBbS3nVCvY7pWRtqPtdm5p7rxZhmwNLBvQxRgpG4KmsyobpV23u7LlNDa14xDaVDE2RobQ2dPqQI7c4ZdFoIWF
GEwZLp0g6LEq9fvibQIQxoGpdEKgFCBJYBQwJpyJSQhAKAxRBEiAHews0zYsREYiGRFmG/NEOC8iuwZ7daoU0E7dwK3iF2kmwTtFs79CPZ4PVaK9HlNLKnKC
ZDrELycxhaQufoWs+RRAMXlihLFmCtw66mRMzSkps1oRIshXW56lJhWZGUVkMxUyrK5ZHCwF93EqpCAwmMIdJeAJkO2Cbg5V64ErFCBO9/X32t3RUbOym7t/
wTRXcef7096qG9LjiFZUp3tDHa2jesXT39PHHf/1tkJudIz7rnOmy4cKnLknVuzC5Y3Pjl2T0WKOvRwKK7z3GpRbcXWXTy5D65nrGuq+HhAmcORg4+eMDKHk
PRSpcCSGDciyiYimxFNBGWrTlvQqThwgyy7PMAR0FSAg6VUcFmSGCOHXwza9MUnNVKuagU1VStF6EOMpfBiRFScRgXdMV1UiSFiNTayrabMwaJBEuLpCWhIr
9WjFibEmHYh+qPGaCSk6qbKxkEM95DKgJSRxvUWq8Yasm3aUTMMDGzIwhki8TE4XcOTSDSLEXlitVVI1gBm01dRBlK1lEhm6sdbghBIdYIpBDBwUXmCCVN2F
MoCpZDNFVSGk5iszESEwVDgWNGWaURoiKIkEOIGLxxitjJeS8xYNjhMKd2ViNGAxFMLOzCzwqZ5QpqzSWTpqDC1Va6GwDJgderl6SP91rHIk05VTLWbHocrX
pgsGWvFoZcxsFGTVpAs0qMahV4LB4lSXoxtYxjGqdU4WZoSEvzYG3sKVR6yKCfyuxRYa3RCujG2B2cBZfr0ZZNA1z55r59RoRoM8hZgw+3Iy936hZo+I0I3D
7Do59yUbTc2NA2xJoWwPu5t42v9uyG+MWIZROSV2muGUoAXYJUbg287NvDNySgigiASUv6Gs0BIeQEJUSlQJCI0GgThvIKJIIJh0NMbXDDhqhxgUDUWt2ajB
Ngm02mJJgAw7TQ9j6jj4XZbzbJfhc1unWqGLDCsmroiQQIICSMVjXjM8+agIzfwVkn8nNtP8CmmNsnOOcT0YQLIp5cfPL54lKi0jZdo7thzaypWgccLTXLOh
3j5ItzkyL2IHPEQpwWFOXsPrQLkTdNxCgWcC8SzqIuPX3u2G3i4KYyqKcLOVv3IjowIBTbEdXrsGDEL/xdyRThQkMxvvLEA=
'''
ANSI_ESC_COLOR = '\x1b[40;38;5;%dm%s'
ANSI_RESET_COLOR = '\x1b[0m'
@classmethod
def get_mascot(cls):
image = cls.unpack_mascot()
try:
terminal_width = int(subprocess.check_output(['tput', 'cols']).strip()) - 1
except (subprocess.CalledProcessError, ValueError):
pass
else:
text_width = sum(len(text) for (color, text) in image[0])
if text_width > terminal_width:
strip_left = int(text_width - terminal_width) / 2
strip_right = text_width - terminal_width - strip_left
stripped = []
for row in image:
remain = strip_left
while row and remain > 0:
if len(row[0][1]) < remain:
remain -= len(row[0][1])
del row[0]
else:
row[0] = (row[0][0], row[0][1][remain:])
break
remain = strip_right
while row and remain > 0:
if len(row[-1][1]) < remain:
remain -= len(row[-1][1])
del row[-1]
else:
row[-1] = (row[-1][0], row[-1][1][:-remain])
break
stripped.append(row)
image = stripped
result = []
for img_row in image:
row = []
for color, text in img_row:
row.append(cls.ANSI_ESC_COLOR % (color, text))
result.append(''.join(row))
result.append(cls.ANSI_RESET_COLOR)
return '\n'.join(result)
@classmethod
def unpack_mascot(cls):
import base64
import zipfile
import tarfile
import struct
import StringIO
lines = [line.strip() for line in cls.DATA.splitlines() if line.strip()]
if lines[0].lower() not in ('zip', 'tar'):
raise ValueError('Unsupported format')
use_zip = lines[0].lower() == 'zip'
buf = StringIO.StringIO(base64.b64decode(''.join(lines[1:])))
if use_zip:
with zipfile.ZipFile(buf, 'r') as zf:
data = zf.open('ascii-ansi.bin').read()
else:
with tarfile.open(fileobj=buf) as tf:
data = tf.extractfile('ascii-ansi.bin').read()
image = []
for line in data.splitlines():
row, offset = [], 0
line = line.strip()
while offset < len(line):
fmt = '<Bb'
color, size = struct.unpack_from(fmt, line, offset)
offset += struct.calcsize(fmt)
fmt = '<%ds' % (1 if size < 0 else size)
text, = struct.unpack_from(fmt, line, offset)
offset += struct.calcsize(fmt)
if size < 0:
text = text[0] * (-size)
row.append((color, text))
image.append(row)
return image
class PrintEscControl:
current = [RESET_ALL]
@classmethod
def __switch_color(cls):
color = ''.join(cls.current)
for handle in (sys.stdout, sys.stderr):
handle.write(color)
handle.flush()
def __init__(self, begin_seq):
self.begin_seq = begin_seq
def __enter__(self, *a, **kw):
self.current.append(self.begin_seq)
self.__switch_color()
def __exit__(self, *a, **kw):
prev_color = self.current.pop()
assert prev_color == self.begin_seq
self.__switch_color()
class CalledProcessError(subprocess.CalledProcessError):
def __init__(self, returncode, cmd, output=None, errout=None):
subprocess.CalledProcessError.__init__(self, returncode, cmd, output)
self.errout = errout
def __str__(self):
if self.returncode != 0:
return subprocess.CalledProcessError.__str__(self)
return 'Command "%s" reported error:\n%s' % (subprocess.list2cmdline(self.cmd), self.errout or 'unknown')
def __repr__(self):
if self.returncode != 0:
return subprocess.CalledProcessError.__repr__(self)
return 'Command "%s" reported error:\n%s' % (subprocess.list2cmdline(self.cmd), self.errout or 'unknown')
def call_cmd(cmd, need_output=True, need_empty_stderr=True):
with PrintEscControl(DIMMED):
if not need_output:
print '$ %s' % subprocess.list2cmdline(cmd)
proc = subprocess.Popen(cmd,
stdout=subprocess.PIPE if need_output else None,
stderr=subprocess.PIPE if need_output else None)
proc.wait()
out, err = proc.communicate()
if proc.returncode != 0 or (need_output and need_empty_stderr and err.strip()):
raise CalledProcessError(proc.returncode, cmd, out, err)
return out
class CpuVendor:
INTEL = 'intel'
OTHER = 'other'
MAPPING = {'GenuineIntel': INTEL}
@classmethod
def os_collect(cls):
with open('/proc/cpuinfo') as f:
for line in f:
if 'vendor_id' in line:
vendor_name = line.split(':', 1)[1].strip()
return cls.MAPPING.get(vendor_name, cls.OTHER)
class PciDevice:
BRACKETS_HEX = re.compile(r'\s*(.*?)\s*\[([0-9a-f]+)\]$', re.IGNORECASE)
USB_CONTROLLER = '0c03'
VGA_CONTROLLER = '0300'
VFIO_DRIVER = 'vfio-pci'
def __init__(self, slot, class_name, class_id, vendor_name, vendor_id, device_name, device_id, driver, module):
self.slot, self.class_name, self.class_id = slot, class_name, class_id
self.vendor_name, self.vendor_id = vendor_name, vendor_id
self.device_name, self.device_id = device_name, device_id
self.driver, self.module = driver, module
self.full_slot = slot
if slot.endswith('.0'):
self.is_function = False
#self.slot = slot[:-2]
else:
self.is_function = True
@classmethod
def parse_pci_dict(cls, dct):
try:
vendor_name, vendor_id = cls.BRACKETS_HEX.match(dct['vendor']).groups()
device_name, device_id = cls.BRACKETS_HEX.match(dct['device']).groups()
class_name, class_id = cls.BRACKETS_HEX.match(dct['class']).groups()
except ValueError:
raise ValueError('incorrect pci dict')
return cls(dct['slot'], class_name, class_id.lower(), vendor_name, vendor_id.lower(),
device_name, device_id.lower(), dct.get('driver', ''), dct.get('module', ''))
def __str__(self):
subst = dict(self.__dict__)
subst['class_str'] = ('%(class_name)s (%(class_id)s) %(device_name)s' % self.__dict__).ljust(70)
if self.device_name.lower() == 'device':
subst['device_name'] = ''
subst['vendor_name'] = ('%(vendor_name)s' % self.__dict__).ljust(30)
return '%(slot)s %(class_str)s %(vendor_name)s [%(vendor_id)s:%(device_id)s]' % subst
def is_same_addr(self, slot):
return self.full_slot.startswith(slot)
def can_passthru(self):
return bool(self.module)
def is_driven_by_vfio(self):
return self.driver == self.VFIO_DRIVER
class PciDeviceList:
_cache = None
class LspciDialect(csv.excel):
delimiter = ' '
@classmethod
def _os_collect(cls):
pci = call_cmd(['lspci', '-k', '-nn', '-vmm'])
item = {}
for line in pci.splitlines():
line = line.strip()
if not line:
if item:
yield PciDevice.parse_pci_dict(item)
item = {}
continue
key, value = line.split(':', 1)
item[key.strip().lower()] = (item.get(key.strip().lower(), '') + ' ' + value.strip()).strip()
if item:
yield PciDevice.parse_pci_dict(item)
@classmethod
def os_collect(cls):
if not cls._cache:
cls._cache = list(cls._os_collect())
for item in cls._cache:
yield item
@classmethod
def get_functions(cls, device):
slot_no_function = device.slot.split('.')[0]
result = []
for dev in cls.os_collect():
if dev.is_function and dev.is_same_addr(slot_no_function):
result.append(dev)
return result
class QemuConfig:
ValidateResult = collections.namedtuple('ValidateResult', 'problem solution have_to_stop')
class QemuConfigEntry:
def __init__(self, value):
self.value = value.split(',')
def __str__(self):
return str(self.value)
def __repr__(self):
return repr(self.value)
class QemuConfigArgs(QemuConfigEntry):
def __init__(self, value):
self.value = shlex.split(value)
class QemuConfigDescription(QemuConfigEntry):
def __init__(self, value):
self.value = urllib.unquote(value)
class QemuSubvalueWrapper:
def __init__(self):
self.__dict = {}
self.value = self
def __setitem__(self, key, value):
self.__dict[key] = value
def __getitem__(self, key):
return self.__dict[key].value
def __delitem__(self, key):
del self.__dict[key]
def get(self, key, default=None):
try:
result = self.__dict[key]
except KeyError:
return default
return result.value
def items(self):
for key, value in self.__dict.items():
yield key, value.value
def __len__(self):
return len(self.__dict)
QEMU_CONFIG_NAME_TO_VALUE = {
'args': QemuConfigArgs,
'description': QemuConfigDescription,
}
ENDING_DIGITS = re.compile(r'^(.*)(\d+)$')
PCI_SLOT_ADDR = re.compile(r'^\d+(:\d+(\.\d+)?)?')
def __init__(self, vmid):
self.vmid = vmid
self.__config = {}
@property
def empty(self):
return not self.__config
def parse_line(self, line):
key, value = line.split(':', 1)
key = key.strip().lower()
line_class = self.QEMU_CONFIG_NAME_TO_VALUE.get(key, self.QemuConfigEntry)
value = line_class(value.strip())
if self.ENDING_DIGITS.match(key):
key, number = self.ENDING_DIGITS.match(key).groups()
self.__config.setdefault(key, self.QemuSubvalueWrapper())[number] = value
else:
self.__config[key] = value
def __getitem__(self, name):
return self.__config[name].value
def __setitem__(self, name, value):
self.__config[name] = value
def get(self, name, default=None):
try:
result = self.__config[name]
except KeyError:
return default
return result.value
@classmethod
def translate_hostpci_to_devices(cls, hostpci_entry):
for item in hostpci_entry:
if cls.PCI_SLOT_ADDR.match(item):
for dev in PciDeviceList.os_collect():
if dev.is_same_addr(item):
yield dev
def get_hostpci_devices(self):
for _, passthru_cfg in self.get('hostpci', {}).items():
for dev in self.translate_hostpci_to_devices(passthru_cfg):
yield dev
def validate(self):
# check that OVMF bios has EFI disk
issues = []
if self.get('bios', [None])[0] == 'ovmf':
if not self.get('efidisk', {}).get('0', None):
issues.append(self.ValidateResult(
problem='Missing EFI disk with OVMF bios selected',
solution='Please add EFI disk using ProxMox Hardware menu',
have_to_stop=True))
# check that if we're passing something thru we use OVMF and don't use ballooning
if self.get('hostpci'):
if self.get('bios', [None])[0] != 'ovmf':
issues.append(self.ValidateResult(
problem='Passing throught devices on non-OVMF bios is unsupported',
solution='Switch BIOS to OVMF using ProxMox Options menu or do not pass PCI devices to it',
have_to_stop=True))
if 'q35' not in self.get('machine', [''])[0]:
issues.append(self.ValidateResult(
problem='Passing through devices on OVMF requires machine to be q35-descendant',
solution='Please fix qemu config for %s vmid' % self.vmid,
have_to_stop=True))
if self.get('balloon', [None])[0] != '0':
issues.append(self.ValidateResult(
problem='Cannot enable memory ballooning when passing through PCI devices',
solution='Disable memory ballooning using ProxMox Hardware menu or do not pass PCI devices to it',
have_to_stop=True))
if len(self['hostpci']) > MAX_PASSTHROUGH:
issues.append(self.ValidateResult(
problem='Cannot have more than %d PCI devices passed through' % MAX_PASSTHROUGH,
solution='Pass fewer PCI devices',
have_to_stop=False))
nums = [int(number) for number, _ in self['hostpci'].items()]
if min(nums) < 0 or max(nums) >= MAX_PASSTHROUGH:
issues.append(self.ValidateResult(
problem='Cannot have hostpci number < 0 or >= %d' % MAX_PASSTHROUGH,
solution='Please fix qemu config for %s vmid' % self.vmid,
have_to_stop=True))
# check that PCI passed through are driven by vfio
for dev in self.get_hostpci_devices():
if not dev.can_passthru():
issues.append(self.ValidateResult(
problem='Cannot pass through device at %s: not driven by a kernel module' % dev.slot,
solution='Run "%s --reconf", select correct devices and reboot OR do bot pass this device through' % os.path.basename(sys.argv[0]),
have_to_stop=False))
if not dev.is_driven_by_vfio():
issues.append(self.ValidateResult(
problem='Bad driver for device at %s, should be %s for passing through' % (dev.slot, PciDevice.VFIO_DRIVER),
solution='Run "%s --reconf", select correct devices and reboot OR do not pass this device through' % os.path.basename(sys.argv[0]),
have_to_stop=False))
# check that if '-cpu' is present in 'args' it matches global 'cpu'
if self.get('args') and self.get('cpu'):
cpu_index = self['args'].index('-cpu')
if cpu_index > 0 and self.get('cpu'):
if cpu_index + 1 >= len(self['args']):
issues.append(self.ValidateResult(
problem='No cpu value present for -cpu argument: %s' % self['args'],
solution='Please fix qemu config for %s vmid' % self.vmid,
have_to_stop=True))
if self['args'][cpu_index + 1].split(',')[0] != self['cpu'][0]:
issues.append(self.ValidateResult(
problem='CPU type in args differs from global CPU type',
solution='Please select matching CPU type or fix -cpu argument',
have_to_stop=True))
return issues
class VmNode:
STOPPED = 'stopped'
RUNNING = 'running'
def __init__(self, vmid, name, status, mem, bootdisk, pid):
self.vmid, self.name, self.status, self.mem, self.bootdisk, self.pid = \
vmid, name, status, mem, bootdisk, pid
self.config = QemuConfig(vmid)
@classmethod
def parse_qmlist_dict(cls, dct):
return cls(dct['VMID'], dct['NAME'], dct['STATUS'], dct['MEM(MB)'], dct['BOOTDISK(GB)'], dct['PID'])
def __str__(self):
subst = dict(self.__dict__)
return '%(vmid)s %(name)s %(status)s %(mem)s %(bootdisk)s %(pid)s' % subst
def parse_config(self):
print '%s config for "%s"...' % ('Getting' if self.config.empty else 'Refreshing', self.name)
self.config = QemuConfig(self.vmid)
lines = call_cmd(['qm', 'config', str(self.vmid)]).splitlines()
for line in lines:
self.config.parse_line(line)
class VmNodeList:
class QmDialect(csv.excel):
delimiter = ' '
skipinitialspace=True
@classmethod
def os_collect(cls):
print 'Getting list of VMs...'
qm = call_cmd(['qm', 'list'])
buf = StringIO.StringIO(qm)
csv.register_dialect('qm', cls.QmDialect)
reader = csv.DictReader(buf, dialect='qm')
csv.unregister_dialect('qm')
for item in reader:
yield VmNode.parse_qmlist_dict(item)
def print_devices(enabler, show_disabled=True):
printed_devs = []
def perform_grouping(label, predicate):
title_shown = False
for dev in PciDeviceList.os_collect():
if not predicate(dev):
continue
if enabler(dev):
printed_devs.append(dev)
if not title_shown:
with PrintEscControl(BOLD):
print label
title_shown = True
print "%2d. %s" % (len(printed_devs), dev)
elif show_disabled:
if not title_shown:
with PrintEscControl(BOLD):
print label
title_shown = True
with PrintEscControl(RED_COLOR):
print ' %s' % dev
if title_shown:
print
perform_grouping("VGA CONTROLLERS (videocards)",
lambda dev: dev.class_id == PciDevice.VGA_CONTROLLER)
perform_grouping("USB CONTROLLERS",
lambda dev: dev.class_id == PciDevice.USB_CONTROLLER)
perform_grouping("OTHER DEVICES",
lambda dev: dev.class_id not in (PciDevice.USB_CONTROLLER, PciDevice.VGA_CONTROLLER))
return printed_devs
def download_internal(url, target):
print 'Downloading %s as %s' % (url, target)
try:
with contextlib.closing(urllib.urlopen(url)) as page:
with open(target, 'wb') as f:
f.write(page.read())
except IOError:
raise IOError("Can't download the file %s as %s" % (url, target))
def download_wget(url, target):
print 'Downloading %s as %s' % (url, target)
try:
call_cmd(['wget', url, '-O', target], need_output=False)
except subprocess.CalledProcessError:
raise IOError("Can't download the file %s as %s" % (url, target))
def print_title(msg):
with PrintEscControl(BOLD):
print '%s\n%s\n' % (msg, '=' * len(msg.strip()))
def prompt_yesno(msg, default_answer=True):
prompt = '%s? [%s] ' % (msg, 'Y/n' if default_answer else 'y/N')
with PrintEscControl(BOLD):
while True:
ans = raw_input(prompt).strip().lower()
if not ans:
return default_answer
if ans in ('y', 'yes'):
return True
if ans in ('n', 'no'):
return False
class IncorrectInputException(Exception):
def __init__(self, msg):
self.msg = msg
def prompt_predicate(msg, predicate):
with PrintEscControl(BOLD):
while True:
try:
result = predicate(raw_input(msg))
except IncorrectInputException as err:
with PrintEscControl(RED_COLOR):
print err.msg
continue
return result
def prompt_comma_list(msg, min_val, max_val):
def parse_comma_list(ans):
try:
ans = ans.strip().split(',')
result = [int(e.strip()) for e in ans if e.strip()]
for e in result:
if e < min_val or e > max_val:
raise ValueError()
except ValueError:
raise IncorrectInputException(
'Incorrect input: enter comma-separated list of values from %s to %s\n' % (min_val, max_val))
return result
return prompt_predicate(msg, parse_comma_list)
def prompt_int(msg, min_val, max_val):
def parse_int(ans):
try:
result = int(ans.strip())
if result < min_val or result > max_val:
raise ValueError()
except ValueError:
raise IncorrectInputException(
'Incorrect input: enter integer value from %s to %s\n' % (min_val, max_val))
return result
return prompt_predicate(msg, parse_int)
def get_module_depends(modname):
try:
modinfo = subprocess.check_output(['modinfo', modname], stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as err:
if ('%s not found' % modname) in err.output:
return []
sys.stderr.write(err.output)
raise
for line in modinfo.splitlines():
if line.split(':')[0] == 'depends':
depends = line.split(':', 1)[1].strip()
return depends.split(',') if depends else []
return []
def inject_geexmox_overrides():
sbin_command = '/usr/local/sbin/geexmox'
if os.path.abspath(__file__) != sbin_command:
print '\nInstalling "%s" command...' % sbin_command
if os.path.exists(sbin_command) or os.path.lexists(sbin_command):
os.unlink(sbin_command)
os.symlink(os.path.abspath(__file__), sbin_command)
stats = os.stat(os.path.abspath(__file__))
os.chmod(os.path.abspath(__file__), stats.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
with open(__file__, 'rb') as myself:
content = myself.read()
if '\r' in content:
with open(__file__, 'wb') as myself:
myself.write(content.replace('\r', ''))
print '\nMaking sure apt has https transport...'
call_cmd(['apt-get', 'install', '-y', 'apt-transport-https'], need_output=False)
for url, target in APT_CONFIGS:
download(url, target)
def disable_pve_enterprise(verbose=True):
logos = [
('bootsplash_dg.jpg', '/usr/share/qemu-server/bootsplash.jpg'),
('logo-128_dg.png', '/usr/share/pve-manager/images/logo-128.png'),
('proxmox_logo_dg.png', '/usr/share/pve-manager/images/proxmox_logo.png'),
]
logo_start_url = 'https://dendygeeks.github.io/geexmox-pve-overrides/logos/'
for fname in glob.glob('/etc/apt/sources.list.d/*'):
with open(fname) as conf:
contents = conf.read().splitlines()
remove = False
for idx, line in enumerate(contents):
nocomment = line.split('#')[0].strip()
if 'https://enterprise.proxmox.com/debian/pve' in nocomment and 'pve-enterprise' in nocomment:
remove = True
contents[idx] = '# removed by %s: # %s' % (os.path.basename(sys.argv[0]), line)
if remove:
if verbose:
print 'Removing PVE Enterprise apt repo at %s...' % fname
with open(fname, 'w') as conf:
conf.write('\n'.join(contents))
libjs = r'/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js'
if os.path.exists(libjs):
if verbose:
print 'Patching %s to remove nag...' % libjs
with open(libjs) as jsfile:
text = jsfile.read().splitlines()
for idx, line in enumerate(text):
if line.strip() == "if (data.status !== 'Active') {":
text[idx] = line.replace("data.status !== 'Active'", "false")
patched = True
break
else:
patched = False
if verbose:
with PrintEscControl(YELLOW_COLOR):
print 'Cannot find the nag, maybe already patched'
if patched:
with open(libjs, 'w') as jsfile:
jsfile.write('\n'.join(text))
if verbose:
print 'Patched out the nag'
for logo_name, logo_target in logos:
if os.path.exists(logo_target):
download(logo_start_url + logo_name, logo_target)
ADDRESS_RE = re.compile(r'\s*inet\s+(\d+\.\d+\.\d+\.\d+).*scope\s+global.*')
def install_proxmox():
# installing ProxMox by following official guide:
# https://pve.proxmox.com/wiki/Install_Proxmox_VE_on_Debian_Stretch
hostname = subprocess.check_output(['hostname']).strip()
hostname_ip = subprocess.check_output(['hostname', '--ip-address']).strip()
ip_config = subprocess.check_output(['ip', 'address']).strip()
real_ip = None
patch_hosts = False
for line in ip_config.splitlines():
match = ADDRESS_RE.match(line)
if match:
if match.group(1) == hostname_ip:
break
if real_ip is None:
real_ip = match.group(1)
else:
with PrintEscControl(YELLOW_COLOR + BOLD):
print '"hostname --ip-address" is not assigned to any valid network interface'
if real_ip is not None:
if prompt_yesno('Assign "%s" as "%s" address instead' % (real_ip, hostname)):
hostname_ip = real_ip
patch_hosts = True
if not patch_hosts:
with open('/etc/hosts') as hosts:
for line in hosts:
line = line.split('#')[0].strip()
if not line:
continue
if line.split()[0] == hostname_ip:
print 'Current host %(b)s%(h)s%(r)s ip address %(b)s%(ip)s%(r)s is present in /etc/hosts' % \
{'b': BOLD, 'r': RESET_ALL, 'h': hostname, 'ip': hostname_ip}
break
else:
patch_hosts = True
if patch_hosts:
print 'Current host %(b)s%(h)s%(r)s ip address %(b)s%(ip)s%(r)s not present in /etc/hosts' % \
{'b': BOLD, 'r': RESET_ALL, 'h': hostname, 'ip': hostname_ip}
print 'It should be there for ProxMox installation to succeed.'
if prompt_yesno('Add %s entry to /etc/hosts' % hostname):
with open('/etc/hosts') as hosts:
lines = hosts.readlines()
with open('/etc/hosts', 'w') as hosts:
for line in lines:
no_comment = line.split('#')[0].strip()
if not no_comment:
continue
if re.search(r'\s+%s(\s+|$)' % re.escape(hostname), no_comment):
hosts.write('#%(line)s # automagically commented by %(prog)s\n' %
{'line': line.rstrip(), 'prog': os.path.basename(sys.argv[0])})
else:
hosts.write(line)
hosts.write('\n%(ip)s\t%(host)s\t\t# automagically added by %(prog)s\n' %
{'ip': hostname_ip, 'host': hostname, 'prog': os.path.basename(sys.argv[0])})
print 'Adding ProxMox repo and key...'
with open('/etc/apt/sources.list.d/pve-install-repo.list', 'w') as pve:
pve.write('deb [arch=amd64] http://download.proxmox.com/debian/pve stretch pve-no-subscription\n')
download('http://download.proxmox.com/debian/proxmox-ve-release-5.x.gpg',
'/etc/apt/trusted.gpg.d/proxmox-ve-release-5.x.gpg')
no_enterprise = prompt_yesno('Remove PVE Enterprise configs and nag warnings', default_answer=False)
if no_enterprise:
disable_pve_enterprise()
print '\nUpdating apt db...'
call_cmd(['apt-get', 'update'], need_output=False)
print
if prompt_yesno('ProxMox recommends dist-upgrade, perform now'):
print 'Upgrading distribution...'
call_cmd(['apt-get', 'dist-upgrade', '-y', '--allow-unauthenticated', '--allow-downgrades'], need_output=False)
print '\nInstalling ProxMox...'
call_cmd(['apt-get', 'install', '-y', '--allow-unauthenticated', '--allow-downgrades', 'proxmox-ve', 'open-iscsi'], need_output=False)
print
if prompt_yesno('ProxMox recommends installing postfix, install', default_answer=False):
call_cmd(['apt-get', 'install', '-y', 'postfix'], need_output=False)
print
if no_enterprise:
disable_pve_enterprise(verbose=False)
def ensure_vfio(devices):
need_update_initramfs = False
print 'Ensuring VFIO drivers are enabled'
vfio_drivers = {key: False for key in 'vfio vfio_iommu_type1 vfio_pci vfio_virqfd'.split()}
with open('/etc/modules') as modules:
for line in modules:
line = line.split('#')[0].strip()
if not line:
continue
if line in vfio_drivers:
vfio_drivers[line] = True
if not all(vfio_drivers.values()):
with open('/etc/modules', 'a+') as modules:
modules.write('# automagically added by %s\n' % os.path.basename(sys.argv[0]))
for driver, is_present in vfio_drivers.items():
if not is_present:
need_update_initramfs = True
modules.write('%s\n' % driver)
modprobe_cfg = [
('options vfio_iommu_type1', 'allow_unsafe_interrupts=1'),
('options kvm', 'ignore_msrs=Y'),
]
device_modules = set()
device_ids = []
vfio_modules = set([PciDevice.VFIO_DRIVER] + get_module_depends(PciDevice.VFIO_DRIVER))
modules_to_walk = set()
for dev in devices:
for module in dev.module.split():
if module not in vfio_modules:
modules_to_walk.add(module)
device_ids.append('%s:%s' % (dev.vendor_id, dev.device_id))
while modules_to_walk:
device_modules |= modules_to_walk
next_modules = set()
for module in modules_to_walk:
next_modules |= set(get_module_depends(module))
modules_to_walk = next_modules - device_modules
device_modules = [module.strip() for module in sorted(device_modules)]
modprobe_cfg.append(('softdep vfio-pci', ' '.join(['post:'] + device_modules)))
for module in device_modules:
modprobe_cfg.append(('softdep %s' % module, 'pre: vfio-pci'))
modprobe_cfg.append(('options vfio-pci', 'ids=%s' % ','.join(sorted(set(device_ids)))))
not_found = []
for starter, value in modprobe_cfg:
found_starter = False
for fname in glob.glob('/etc/modprobe.d/*.conf'):
content, do_patch = [], False
with open(fname) as f:
for line in f:
no_comment = line.split('#')[0].strip()
if no_comment.startswith(starter + ' '):
if no_comment[len(starter):].strip() != value:
with PrintEscControl(YELLOW_COLOR):
print 'Commenting out "%s" in %s' % (line.strip(), fname)
do_patch = True
need_update_initramfs = True
content.append('# %s #-- commented by %s' % (line.rstrip(), os.path.basename(sys.argv[0])))
continue
print 'Required "%s %s" present in %s' % (starter, value, fname)
found_starter = True
content.append(line.rstrip())
if do_patch:
with open(fname, 'w') as f:
f.write('\n'.join(content) + '\n')
if not found_starter:
not_found.append((starter, value))
if not_found:
with open('/etc/modprobe.d/geexmox.conf', 'a+') as f:
for starter, value in not_found:
print 'Writing "%s %s" to geexmox.conf' % (starter, value)
f.write('%s %s\n' % (starter, value))
need_update_initramfs = True
if need_update_initramfs:
print '\nUpdating initramfs to apply vfio configuration...'
call_cmd(['update-initramfs', '-u', '-k', 'all'], need_output=False)
IOMMU_ENABLING = {
CpuVendor.INTEL: ['intel_iommu=on', 'video=efifb:off'],
}
GRUB_CMDLINE_RE = re.compile(r'(\s*GRUB_CMDLINE_LINUX_DEFAULT\s*=\s*")([^"]*)("\s*)')
def ensure_kernel_params_no_reboot(kernel_params):
grub_text = []
update_grub_config = False
with open('/etc/default/grub') as grub_conf:
for line in grub_conf:
no_comment = line.split('#')[0].strip()
match = GRUB_CMDLINE_RE.match(no_comment)
if match:
args = shlex.split(match.group(2))
for extra_arg in kernel_params:
if extra_arg not in args:
update_grub_config = True
args.append(extra_arg)
line = GRUB_CMDLINE_RE.sub(r'\1%s\3' % subprocess.list2cmdline(args), line)
grub_text.append(line)
if update_grub_config:
print 'Updating grub config...'
with open('/etc/default/grub', 'w') as grub_conf:
grub_conf.write(''.join(grub_text))
call_cmd(['update-grub'], need_output=False)
return not update_grub_config
def enable_iommu(devices):
try:
kernel_params = IOMMU_ENABLING[CpuVendor.os_collect()]
except KeyError:
with PrintEscControl(RED_COLOR):
sys.stderr.write('%s does not know how to enable IOMMU on your CPU yet.\n' % os.path.basename(sys.argv[0]))
return
ensure_kernel_params_no_reboot(kernel_params)
def stage1():
if CpuVendor.os_collect() != CpuVendor.INTEL:
with PrintEscControl(YELLOW_COLOR):
sys.stderr.write('Non-Intel CPUs are not fully supported by GeexMox. Pull requests are welcome! :)\n')
inject_geexmox_overrides()
install_proxmox()
print_title('PCI devices present:')
devices = print_devices(lambda dev: dev.can_passthru())
while True:
passthru = prompt_comma_list('Input comma-separated list of devices to enable passthrough for: ', 1, len(devices))
if passthru:
with PrintEscControl(BOLD):
print '\nDevices selected for passing through:'
for idx in passthru:
print devices[idx - 1]
else:
with PrintEscControl(BOLD):
print '\nNo devices selected for passing through'
print
if prompt_yesno('Is it correct'):
break
if passthru:
pass_devices = [devices[idx - 1] for idx in passthru]
for parent_device in list(pass_devices):
pass_devices.extend(PciDeviceList.get_functions(parent_device))
ensure_vfio(pass_devices)
enable_iommu(pass_devices)
with PrintEscControl(BOLD):
print '\nTo continue with configuring VMs please reboot and re-run %s' % os.path.basename(sys.argv[0])
def check_iommu_groups(devices):
iommu = {}
for device_path in glob.glob('/sys/kernel/iommu_groups/*/devices/*'):
group = device_path.split('/')[4]
device_addr = device_path.split('/')[-1]
if not device_addr.startswith('0000:'):
with PrintEscControl(RED_COLOR):
sys.stderr.write('Unsupported PCI configuration, more than one bus found')
iommu[device_addr[5:]] = group
group_devs = {}
for dev in devices:
group = iommu[dev.full_slot]
group_devs.setdefault(group, []).append(dev)
group_devs = {key: val for (key, val) in group_devs.items() if len(val) > 1}
if group_devs:
for group, devices in group_devs.items():
with PrintEscControl(BOLD):
print 'IOMMU group %s:' % group
for dev in devices:
print dev
if prompt_yesno('Do you want to pass through devices from same group to different VMs'):
if not ensure_kernel_params_no_reboot(['pcie_acs_override=downstream,multifunction']):
with PrintEscControl(BOLD + YELLOW_COLOR):
print 'To continue with configuring VMs please reboot and re-run %s' % os.path.basename(sys.argv[0])
sys.exit(0)
def list_and_validate_vms():
vms = list(VmNodeList.os_collect())
print '\nValidating created VMs configurations...'
for vm in vms:
vm.parse_config()
have_to_stop = False
for vm in vms:
issues = vm.config.validate()
if not issues:
continue
with PrintEscControl(YELLOW_COLOR + BOLD):
print '\nWARNING: VM "%s" has configuration %s:' % (vm.name, 'issues' if len(issues) > 1 else 'issue')
for issue in issues:
with PrintEscControl(YELLOW_COLOR):
print issue.problem + '.'
with PrintEscControl(GREEN_COLOR):
with PrintEscControl(BOLD):
print 'SOLUTION: ',
print '%s.' % issue.solution
have_to_stop = have_to_stop or issue.have_to_stop
if have_to_stop:
with PrintEscControl(BOLD):
print '\nPlease fix issues above and refresh VM list to continue configuring'
return []
return vms
def show_passthrough_devs(vm):
if vm.config.get('hostpci'):