source: ogClient-Git/src/virtual/ogOperations.py @ 082079a

Last change on this file since 082079a was cb9edc8, checked in by OpenGnSys Support Team <soporte-og@…>, 4 years ago

ogClient is AGPLv3+

Update license header in files.

  • Property mode set to 100644
File size: 21.9 KB
Line 
1#
2# Copyright (C) 2020-2021 Soleta Networks <info@soleta.eu>
3#
4# This program is free software: you can redistribute it and/or modify it under
5# the terms of the GNU Affero General Public License as published by the
6# Free Software Foundation; either version 3 of the License, or
7# (at your option) any later version.
8
9from src.ogRest import ThreadState
10import socket
11import errno
12import select
13import json
14import subprocess
15import shutil
16import os
17import guestfs
18import hivex
19import pathlib
20import re
21import math
22import sys
23import enum
24import time
25
26class OgVM:
27    DEFAULT_CPU = 'host'
28    DEFAULT_VGA = 'virtio-vga'
29    DEFAULT_QMP_IP = 'localhost'
30    DEFAULT_QMP_PORT = 4444
31
32    class State(enum.Enum):
33        STOPPED = 0
34        RUNNING = 1
35
36    def __init__(self,
37                 partition_path,
38                 memory=None,
39                 cpu=DEFAULT_CPU,
40                 vga=DEFAULT_VGA,
41                 qmp_ip=DEFAULT_QMP_IP,
42                 qmp_port=DEFAULT_QMP_PORT,
43                 vnc_params=None):
44        self.partition_path = partition_path
45        self.cpu = cpu
46        self.vga = vga
47        self.qmp_ip = qmp_ip
48        self.qmp_port = qmp_port
49        self.proc = None
50        self.vnc_params = vnc_params
51
52        if memory:
53            self.mem = memory
54        else:
55            available_ram = os.sysconf('SC_PAGE_SIZE') * os.sysconf('SC_PHYS_PAGES')
56            available_ram = available_ram / 2 ** 20
57            # Calculate the lower power of 2 amout of RAM memory for the VM.
58            self.mem = 2 ** math.floor(math.log(available_ram) / math.log(2))
59
60
61    def run_vm(self):
62        if self.vnc_params:
63            vnc_str = f'-vnc 0.0.0.0:0,password'
64        else:
65            vnc_str = ''
66
67        cmd = (f'qemu-system-x86_64 -accel kvm -cpu {self.cpu} -smp 4 '
68               f'-drive file={self.partition_path},if=virtio '
69               f'-qmp tcp:localhost:4444,server,nowait '
70               f'-device {self.vga} -display gtk '
71               f'-m {self.mem}M -boot c -full-screen {vnc_str}')
72        self.proc = subprocess.Popen([cmd], shell=True)
73
74        if self.vnc_params:
75            # Wait for QMP to be available.
76            time.sleep(20)
77            cmd = { "execute": "change",
78                    "arguments": { "device": "vnc",
79                                   "target": "password",
80                                   "arg": str(self.vnc_params['pass']) } }
81            with OgQMP(self.qmp_ip, self.qmp_port) as qmp:
82                qmp.talk(str(cmd))
83
84class OgQMP:
85    QMP_TIMEOUT = 5
86    QMP_POWEROFF_TIMEOUT = 300
87
88    def __init__(self, ip, port):
89        self.ip = ip
90        self.port = port
91
92    def connect(self):
93        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
94        self.sock.setblocking(0)
95        try:
96            self.sock.connect((self.ip, self.port))
97        except socket.error as err:
98            if err.errno == errno.ECONNREFUSED:
99                raise Exception('cannot connect to qemu')
100            elif err.errno == errno.EINPROGRESS:
101                pass
102
103        readset = [ self.sock ]
104        readable, writable, exception = select.select(readset,
105                                                      [],
106                                                      [],
107                                                      OgQMP.QMP_TIMEOUT)
108
109        if self.sock in readable:
110            try:
111                out = self.recv()
112            except:
113                pass
114
115        if 'QMP' not in out:
116            raise Exception('cannot handshake qemu')
117
118        out = self.talk(str({"execute": "qmp_capabilities"}))
119        if 'return' not in out:
120            raise Exception('cannot handshake qemu')
121
122    def disconnect(self):
123        try:
124            self.sock.close()
125        except:
126            pass
127
128    def __enter__(self):
129        self.connect()
130        return self
131
132    def __exit__(self, exc_type, exc_val, exc_tb):
133        self.disconnect()
134
135    def talk(self, data, timeout=QMP_TIMEOUT):
136        writeset = [ self.sock ]
137        readable, writable, exception = select.select([],
138                                                      writeset,
139                                                      [],
140                                                      timeout)
141        if self.sock in writable:
142            try:
143                self.sock.send(bytes(data, 'utf-8'))
144            except:
145                raise Exception('cannot talk to qemu')
146        else:
147            raise Exception('timeout when talking to qemu')
148
149        return self.recv(timeout=timeout)
150
151    def recv(self, timeout=QMP_TIMEOUT):
152        readset = [self.sock]
153        readable, _, _ = select.select(readset, [], [], timeout)
154
155        if self.sock in readable:
156            try:
157                out = self.sock.recv(4096).decode('utf-8')
158                out = json.loads(out)
159            except socket.error as err:
160                raise Exception('cannot talk to qemu')
161        else:
162            raise Exception('timeout when talking to qemu')
163        return out
164
165class OgVirtualOperations:
166    def __init__(self):
167        self.IP = '127.0.0.1'
168        self.VIRTUAL_PORT = 4444
169        self.USABLE_DISK = 0.75
170        self.OG_PATH = os.path.dirname(os.path.realpath(sys.argv[0]))
171        self.OG_IMAGES_PATH = f'{self.OG_PATH}/images'
172        self.OG_PARTITIONS_PATH = f'{self.OG_PATH}/partitions'
173        self.OG_PARTITIONS_CFG_PATH = f'{self.OG_PATH}/partitions.json'
174
175        if not os.path.exists(self.OG_IMAGES_PATH):
176            os.mkdir(self.OG_IMAGES_PATH, mode=0o755)
177        if not os.path.exists(self.OG_PARTITIONS_PATH):
178            os.mkdir(self.OG_PARTITIONS_PATH, mode=0o755)
179
180    def poweroff_guest(self):
181        try:
182            with OgQMP(self.IP, self.VIRTUAL_PORT) as qmp:
183                qmp.talk(str({"execute": "system_powerdown"}))
184                out = qmp.recv()
185                assert(out['event'] == 'POWERDOWN')
186                out = qmp.recv(timeout=OgQMP.QMP_POWEROFF_TIMEOUT)
187                assert(out['event'] == 'SHUTDOWN')
188        except:
189            return
190
191    def poweroff_host(self):
192        subprocess.run(['/sbin/poweroff'])
193
194    def poweroff(self):
195        self.poweroff_guest()
196        self.poweroff_host()
197
198    def reboot(self):
199        try:
200            with OgQMP(self.IP, self.VIRTUAL_PORT) as qmp:
201                qmp.talk(str({"execute": "system_reset"}))
202        except:
203            pass
204
205    def check_vm_state(self):
206        try:
207            with OgQMP(self.IP, self.VIRTUAL_PORT) as qmp:
208                pass
209            return OgVM.State.RUNNING
210        except:
211            return OgVM.State.STOPPED
212
213    def get_installed_os(self):
214        installed_os = {}
215        try:
216            with open(self.OG_PARTITIONS_CFG_PATH, 'r') as f:
217                cfg = json.loads(f.read())
218            for part in cfg['partition_setup']:
219                if len(part['os']) > 0:
220                    installed_os[part['os']] = (part['disk'], part['partition'])
221        except:
222            pass
223        return installed_os
224
225    def check_vm_state_loop(self, ogRest):
226        POLLING_WAIT_TIME = 12
227        while True:
228            time.sleep(POLLING_WAIT_TIME)
229            state = self.check_vm_state()
230            installed_os = self.get_installed_os()
231            if state == OgVM.State.STOPPED and \
232               ogRest.state == ThreadState.IDLE and \
233               len(installed_os) > 0:
234                self.poweroff_host()
235
236    def shellrun(self, request, ogRest):
237        return
238
239    def session(self, request, ogRest):
240        disk = request.getDisk()
241        partition = request.getPartition()
242
243        part_path = f'{self.OG_PARTITIONS_PATH}/disk{disk}_part{partition}.qcow2'
244        if ogRest.CONFIG['vnc']['activate']:
245            qemu = OgVM(part_path, vnc_params=ogRest.CONFIG['vnc'])
246        else:
247            qemu = OgVM(part_path)
248        qemu.run_vm()
249
250    def partitions_cfg_to_json(self, data):
251        for part in data['partition_setup']:
252            part.pop('virt-drive')
253            for k, v in part.items():
254                part[k] = str(v)
255        for disk in data['disk_setup']:
256            for k, v in disk.items():
257                disk[k] = str(v)
258        return data
259
260    def refresh(self, ogRest):
261        try:
262            # Return last partitions setup in case VM is running.
263            with OgQMP(self.IP, self.VIRTUAL_PORT) as qmp:
264                pass
265            with open(self.OG_PARTITIONS_CFG_PATH, 'r') as f:
266                data = json.loads(f.read())
267            data = self.partitions_cfg_to_json(data)
268            return data
269        except:
270            pass
271
272        try:
273            with open(self.OG_PARTITIONS_CFG_PATH, 'r+') as f:
274                data = json.loads(f.read())
275                for part in data['partition_setup']:
276                    if len(part['virt-drive']) > 0:
277                        if not os.path.exists(part['virt-drive']):
278                            part['code'] = '',
279                            part['filesystem'] = 'EMPTY'
280                            part['os'] = ''
281                            part['size'] = 0
282                            part['used_size'] = 0
283                            part['virt-drive'] = ''
284                            continue
285                        g = guestfs.GuestFS(python_return_dict=True)
286                        g.add_drive_opts(part['virt-drive'],
287                                         format="qcow2",
288                                         readonly=1)
289                        g.launch()
290                        devices = g.list_devices()
291                        assert(len(devices) == 1)
292                        partitions = g.list_partitions()
293                        assert(len(partitions) == 1)
294                        filesystems_dict = g.list_filesystems()
295                        assert(len(filesystems_dict) == 1)
296                        g.mount(partitions[0], '/')
297                        used_disk = g.du('/')
298                        g.umount_all()
299                        total_size = g.disk_virtual_size(part['virt-drive']) / 1024
300
301                        part['used_size'] = int(100 * used_disk / total_size)
302                        part['size'] = total_size
303                        root = g.inspect_os()
304                        if len(root) == 1:
305                            part['os'] = f'{g.inspect_get_distro(root[0])} ' \
306                                         f'{g.inspect_get_product_name(root[0])}'
307                        else:
308                            part['os'] = ''
309                        filesystem = [fs for fs in filesystems_dict.values()][0]
310                        part['filesystem'] = filesystem.upper()
311                        if filesystem == 'ext4':
312                            part['code'] = '0083'
313                        elif filesystem == 'ntfs':
314                            part['code'] = '0007'
315                        g.close()
316                f.seek(0)
317                f.write(json.dumps(data, indent=4))
318                f.truncate()
319        except FileNotFoundError:
320            total_disk, used_disk, free_disk = shutil.disk_usage("/")
321            free_disk = int(free_disk * self.USABLE_DISK)
322            data = {'serial_number': '',
323                    'disk_setup': [{'disk': 1,
324                                   'partition': 0,
325                                   'code': '0',
326                                   'filesystem': '',
327                                   'os': '',
328                                   'size': int(free_disk / 1024),
329                                   'used_size': int(100 * used_disk / total_disk)}],
330                    'partition_setup': []}
331            for i in range(4):
332                part_json = {'disk': 1,
333                             'partition': i + 1,
334                             'code': '',
335                             'filesystem': 'EMPTY',
336                             'os': '',
337                             'size': 0,
338                             'used_size': 0,
339                             'virt-drive': ''}
340                data['partition_setup'].append(part_json)
341            with open(self.OG_PARTITIONS_CFG_PATH, 'w+') as f:
342                f.write(json.dumps(data, indent=4))
343        except:
344            with open(self.OG_PARTITIONS_CFG_PATH, 'r') as f:
345                data = json.load(f)
346
347        data = self.partitions_cfg_to_json(data)
348
349        return data
350
351    def setup(self, request, ogRest):
352        self.poweroff_guest()
353        self.refresh(ogRest)
354
355        part_setup = request.getPartitionSetup()
356        disk = request.getDisk()
357
358        for i, part in enumerate(part_setup):
359            if int(part['format']) == 0:
360                continue
361
362            drive_path = f'{self.OG_PARTITIONS_PATH}/disk{disk}_part{part["partition"]}.qcow2'
363            g = guestfs.GuestFS(python_return_dict=True)
364            g.disk_create(drive_path, "qcow2", int(part['size']) * 1024)
365            g.add_drive_opts(drive_path, format="qcow2", readonly=0)
366            g.launch()
367            devices = g.list_devices()
368            assert(len(devices) == 1)
369            g.part_disk(devices[0], "gpt")
370            partitions = g.list_partitions()
371            assert(len(partitions) == 1)
372            g.mkfs(part["filesystem"].lower(), partitions[0])
373            g.close()
374
375            with open(self.OG_PARTITIONS_CFG_PATH, 'r+') as f:
376                data = json.loads(f.read())
377                if part['code'] == 'LINUX':
378                    data['partition_setup'][i]['code'] = '0083'
379                elif part['code'] == 'NTFS':
380                    data['partition_setup'][i]['code'] = '0007'
381                elif part['code'] == 'DATA':
382                    data['partition_setup'][i]['code'] = '00da'
383                data['partition_setup'][i]['filesystem'] = part['filesystem']
384                data['partition_setup'][i]['size'] = int(part['size'])
385                data['partition_setup'][i]['virt-drive'] = drive_path
386                f.seek(0)
387                f.write(json.dumps(data, indent=4))
388                f.truncate()
389
390        return self.refresh(ogRest)
391
392    def image_create(self, path, request, ogRest):
393        disk = request.getDisk()
394        partition = request.getPartition()
395        name = request.getName()
396        repo = request.getRepo()
397        samba_config = ogRest.samba_config
398
399        self.poweroff_guest()
400
401        self.refresh(ogRest)
402
403        drive_path = f'{self.OG_PARTITIONS_PATH}/disk{disk}_part{partition}.qcow2'
404
405        cmd = f'mount -t cifs //{repo}/ogimages {self.OG_IMAGES_PATH} -o ' \
406              f'rw,nolock,serverino,acl,' \
407              f'username={samba_config["user"]},' \
408              f'password={samba_config["pass"]}'
409        subprocess.run([cmd], shell=True)
410
411        try:
412            shutil.copy(drive_path, f'{self.OG_IMAGES_PATH}/{name}')
413        except:
414            return None
415
416        subprocess.run([f'umount {self.OG_IMAGES_PATH}'], shell=True)
417
418        return True
419
420    def image_restore(self, request, ogRest):
421        disk = request.getDisk()
422        partition = request.getPartition()
423        name = request.getName()
424        repo = request.getRepo()
425        # TODO Multicast? Unicast? Solo copia con samba.
426        ctype = request.getType()
427        profile = request.getProfile()
428        cid = request.getId()
429        samba_config = ogRest.samba_config
430
431        self.poweroff_guest()
432        self.refresh(ogRest)
433
434        drive_path = f'{self.OG_PARTITIONS_PATH}/disk{disk}_part{partition}.qcow2'
435
436        if os.path.exists(drive_path):
437            os.remove(drive_path)
438
439        cmd = f'mount -t cifs //{repo}/ogimages {self.OG_IMAGES_PATH} -o ' \
440              f'ro,nolock,serverino,acl,' \
441              f'username={samba_config["user"]},' \
442              f'password={samba_config["pass"]}'
443        subprocess.run([cmd], shell=True)
444
445        try:
446            shutil.copy(f'{self.OG_IMAGES_PATH}/{name}', drive_path)
447        except:
448            return None
449
450        subprocess.run([f'umount {self.OG_IMAGES_PATH}'], shell=True)
451        self.refresh(ogRest)
452
453        return True
454
455    def software(self, request, path, ogRest):
456        DPKG_PATH = '/var/lib/dpkg/status'
457
458        disk = request.getDisk()
459        partition = request.getPartition()
460        drive_path = f'{self.OG_PARTITIONS_PATH}/disk{disk}_part{partition}.qcow2'
461        g = guestfs.GuestFS(python_return_dict=True)
462        g.add_drive_opts(drive_path, readonly=1)
463        g.launch()
464        root = g.inspect_os()[0]
465
466        os_type = g.inspect_get_type(root)
467        os_major_version = g.inspect_get_major_version(root)
468        os_minor_version = g.inspect_get_minor_version(root)
469        os_distro = g.inspect_get_distro(root)
470
471        software = []
472
473        if 'linux' in os_type:
474            g.mount_ro(g.list_partitions()[0], '/')
475            try:
476                g.download('/' + DPKG_PATH, 'dpkg_list')
477            except:
478                pass
479            g.umount_all()
480            if os.path.isfile('dpkg_list'):
481                pkg_pattern = re.compile('Package: (.+)')
482                version_pattern = re.compile('Version: (.+)')
483                with open('dpkg_list', 'r') as f:
484                    for line in f:
485                        pkg_match = pkg_pattern.match(line)
486                        version_match = version_pattern.match(line)
487                        if pkg_match:
488                            pkg = pkg_match.group(1)
489                        elif version_match:
490                            version = version_match.group(1)
491                        elif line == '\n':
492                            software.append(pkg + ' ' + version)
493                        else:
494                            continue
495                os.remove('dpkg_list')
496        elif 'windows' in os_type:
497            g.mount_ro(g.list_partitions()[0], '/')
498            hive_file_path = g.inspect_get_windows_software_hive(root)
499            g.download('/' + hive_file_path, 'win_reg')
500            g.umount_all()
501            h = hivex.Hivex('win_reg')
502            key = h.root()
503            key = h.node_get_child (key, 'Microsoft')
504            key = h.node_get_child (key, 'Windows')
505            key = h.node_get_child (key, 'CurrentVersion')
506            key = h.node_get_child (key, 'Uninstall')
507            software += [h.node_name(x) for x in h.node_children(key)]
508            # Just for 64 bit Windows versions, check for 32 bit software.
509            if (os_major_version == 5 and os_minor_version >= 2) or \
510               (os_major_version >= 6):
511                key = h.root()
512                key = h.node_get_child (key, 'Wow6432Node')
513                key = h.node_get_child (key, 'Microsoft')
514                key = h.node_get_child (key, 'Windows')
515                key = h.node_get_child (key, 'CurrentVersion')
516                key = h.node_get_child (key, 'Uninstall')
517                software += [h.node_name(x) for x in h.node_children(key)]
518            os.remove('win_reg')
519
520        return '\n'.join(software)
521
522    def parse_pci(self, path='/usr/share/misc/pci.ids'):
523        data = {}
524        with open(path, 'r') as f:
525            for line in f:
526                if line[0] == '#':
527                    continue
528                elif len(line.strip()) == 0:
529                    continue
530                else:
531                    if line[:2] == '\t\t':
532                        fields = line.strip().split(maxsplit=2)
533                        data[last_vendor][last_device][(fields[0], fields[1])] = fields[2]
534                    elif line[:1] == '\t':
535                        fields = line.strip().split(maxsplit=1)
536                        last_device = fields[0]
537                        data[last_vendor][fields[0]] = {'name': fields[1]}
538                    else:
539                        fields = line.strip().split(maxsplit=1)
540                        if fields[0] == 'ffff':
541                            break
542                        last_vendor = fields[0]
543                        data[fields[0]] = {'name': fields[1]}
544        return data
545
546    def hardware(self, path, ogRest):
547        try:
548            with OgQMP(self.IP, self.VIRTUAL_PORT) as qmp:
549                pci_data = qmp.talk(str({"execute": "query-pci"}))
550                mem_data = qmp.talk(str({"execute": "query-memory-size-summary"}))
551                cpu_data = qmp.talk(str({"execute": "query-cpus-fast"}))
552        except:
553            return
554
555        pci_data = pci_data['return'][0]['devices']
556        pci_list = self.parse_pci()
557        device_names = {}
558        for device in pci_data:
559            vendor_id = hex(device['id']['vendor'])[2:]
560            device_id = hex(device['id']['device'])[2:]
561            subvendor_id = hex(device['id']['subsystem-vendor'])[2:]
562            subdevice_id = hex(device['id']['subsystem'])[2:]
563            description = device['class_info']['desc'].lower()
564            name = ''
565            try:
566                name = pci_list[vendor_id]['name']
567                name += ' ' + pci_list[vendor_id][device_id]['name']
568                name += ' ' + pci_list[vendor_id][device_id][(subvendor_id, subdevice_id)]
569            except KeyError:
570                if vendor_id == '1234':
571                    name = 'VGA Cirrus Logic GD 5446'
572                else:
573                    pass
574
575            if 'usb' in description:
576                device_names['usb'] = name
577            elif 'ide' in description:
578                device_names['ide'] = name
579            elif 'ethernet' in description:
580                device_names['net'] = name
581            elif 'vga' in description:
582                device_names['vga'] = name
583
584            elif 'audio' in description or 'sound' in description:
585                device_names['aud'] = name
586            elif 'dvd' in description:
587                device_names['cdr'] = name
588
589        ram_size = int(mem_data['return']['base-memory']) * 2 ** -20
590        device_names['mem'] = f'QEMU {int(ram_size)}MiB'
591
592        cpu_arch = cpu_data['return'][0]['arch']
593        cpu_target = cpu_data['return'][0]['target']
594        cpu_cores = len(cpu_data['return'])
595        device_names['cpu'] = f'CPU arch:{cpu_arch} target:{cpu_target} ' \
596                              f'cores:{cpu_cores}'
597
598        with open(path, 'w+') as f:
599            f.seek(0)
600            for k, v in device_names.items():
601                f.write(f'{k}={v}\n')
602            f.truncate()
603
604    def probe(self, ogRest):
605        return {'status': 'VDI' if ogRest.state != ThreadState.BUSY else 'BSY'}
Note: See TracBrowser for help on using the repository browser.