source: ogClient-Git/src/live/ogOperations.py @ f5be6f3

Last change on this file since f5be6f3 was 782f46a, checked in by Jose M. Guisado <jguisado@…>, 2 years ago

live: rewrite setup operation

Rewrites the setup operation using python-libfdisk module instead of an
external bash script. Consolidating the operation into Python's code,
limiting external subprocesses to well known programs and small
concrete tasks that are difficult to fully integrate into Python.

Use parttypes.py to fetch partition types from python-libfdisk module.
Use fs.py to create any specified supported filesystem.

OpenGnsys cache partitions are created labelling the partition as
"CACHE". Stops setting non-standard MBR hexcode (0xca) to the cache
partition in addition to the filesystem label.

Any partition specified as type EMPTY will be ignored.

  • Property mode set to 100644
File size: 18.8 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
9import hashlib
10import logging
11import os
12import subprocess
13import shlex
14import shutil
15
16from subprocess import Popen, PIPE
17
18import fdisk
19
20from src.ogClient import ogClient
21from src.ogRest import ThreadState
22from src.live.partcodes import GUID_MAP
23from src.live.parttypes import get_parttype
24
25from src.utils.legacy import *
26from src.utils.net import ethtool
27from src.utils.menu import generate_menu
28from src.utils.fs import *
29from src.utils.probe import os_probe, cache_probe
30from src.utils.disk import *
31from src.utils.cache import generate_cache_txt, umount_cache, init_cache
32from src.utils.tiptorrent import *
33
34
35OG_SHELL = '/bin/bash'
36
37class OgLiveOperations:
38    def __init__(self, config):
39        self._url = config['opengnsys']['url']
40        self._url_log = config['opengnsys']['url_log']
41        self._smb_user = config['samba']['user']
42        self._smb_pass = config['samba']['pass']
43
44    def _restartBrowser(self, url):
45        try:
46            proc = subprocess.call(["pkill", "-9", "browser"])
47            proc = subprocess.Popen(["browser", "-qws", url])
48        except:
49            logging.error('Cannot restart browser')
50            raise ValueError('Error: cannot restart browser')
51
52    def _refresh_payload_disk(self, cxt, part_setup, num_disk):
53        part_setup['disk'] = str(num_disk)
54        part_setup['disk_type'] = 'DISK'
55        part_setup['partition'] = '0'
56        part_setup['filesystem'] = ''
57        part_setup['os'] = ''
58        part_setup['size'] = str(cxt.nsectors * cxt.sector_size // 1024)
59        part_setup['used_size'] = '0'
60        if not cxt.label:
61            part_setup['code'] = '0'
62        else:
63            part_setup['code'] = '2' if cxt.label.name == 'gpt' else '1'
64
65    def _refresh_payload_partition(self, cxt, pa, part_setup, disk):
66        parttype = cxt.partition_to_string(pa, fdisk.FDISK_FIELD_TYPEID)
67        fstype = cxt.partition_to_string(pa, fdisk.FDISK_FIELD_FSTYPE)
68        padev = cxt.partition_to_string(pa, fdisk.FDISK_FIELD_DEVICE)
69        size = cxt.partition_to_string(pa, fdisk.FDISK_FIELD_SIZE)
70        partnum = pa.partno + 1
71        source = padev
72        target = padev.replace('dev', 'mnt')
73
74        if cxt.label.name == 'gpt':
75            code = GUID_MAP.get(parttype, 0x0)
76        else:
77            code = int(parttype, base=16)
78
79        if mount_mkdir(source, target):
80            probe_result = os_probe(target)
81            part_setup['os'] = probe_result
82            part_setup['used_size'] = get_usedperc(target)
83            umount(target)
84        else:
85            part_setup['os'] = ''
86            part_setup['used_size'] = '0'
87
88
89        part_setup['disk_type'] = ''
90        part_setup['partition'] = str(partnum)
91        part_setup['filesystem'] = fstype.upper() if fstype else 'EMPTY'
92        # part_setup['code'] = hex(code).removeprefix('0x')
93        part_setup['code'] = hex(code)[2:]
94        part_setup['size'] = str(int(size) // 1024)
95
96        if (part_setup['filesystem'] == 'VFAT'):
97            part_setup['filesystem'] = 'FAT32'
98
99    def _refresh_part_setup_cache(self, cxt, pa, part_setup, cache):
100        padev = cxt.partition_to_string(pa, fdisk.FDISK_FIELD_DEVICE)
101        if padev == cache:
102            part_setup['filesystem'] = 'CACHE'
103            part_setup['code'] = 'ca'
104
105    def _compute_md5(self, path, bs=2**20):
106        m = hashlib.md5()
107        with open(path, 'rb') as f:
108            while True:
109                buf = f.read(bs)
110                if not buf:
111                    break
112                m.update(buf)
113        return m.hexdigest()
114
115    def _write_md5_file(self, path):
116        if not os.path.exists(path):
117            logging.error('Invalid path in _write_md5_file')
118            raise ValueError('Invalid image path when computing md5 checksum')
119        filename = path + ".full.sum"
120        dig = self._compute_md5(path)
121        with open(filename, 'w') as f:
122            f.write(dig)
123
124    def _copy_image_to_cache(self, image_name):
125        """
126        Copies /opt/opengnsys/image/{image_name} into
127        /opt/opengnsys/cache/opt/opengnsys/images/
128
129        Implies a unicast transfer. Does not use tiptorrent.
130        """
131        src = f'/opt/opengnsys/images/{image_name}.img'
132        dst = f'/opt/opengnsys/cache/opt/opengnsys/images/{image_name}.img'
133        try:
134            r = shutil.copy(src, dst)
135            tip_write_csum(image_name)
136        except:
137            logging.error('Error copying image to cache', repo)
138            raise ValueError(f'Error: Cannot copy image {image_name} to cache')
139
140    def _restore_image_unicast(self, repo, name, devpath, cache=False):
141        if ogChangeRepo(repo).returncode != 0:
142            self._restartBrowser(self._url)
143            logging.error('ogChangeRepo could not change repository to %s', repo)
144            raise ValueError(f'Error: Cannot change repository to {repo}')
145        logging.debug(f'restore_image_unicast: name => {name}')
146        if cache:
147            image_path = f'/opt/opengnsys/cache/opt/opengnsys/images/{name}.img'
148            if (not os.path.exists(image_path) or
149                not tip_check_csum(repo, name)):
150                self._copy_image_to_cache(name)
151        else:
152            image_path = f'/opt/opengnsys/images/{name}.img'
153        self._restore_image(image_path, devpath)
154
155    def _restore_image_tiptorrent(self, repo, name, devpath):
156        image_path = f'/opt/opengnsys/cache/opt/opengnsys/images/{name}.img'
157        try:
158            if (not os.path.exists(image_path) or
159                not tip_check_csum(repo, name)):
160                tip_client_get(repo, name)
161        except:
162            self._restartBrowser(self._url)
163            raise ValueError('Error before restoring image')
164        self._restore_image(image_path, devpath)
165
166    def _restore_image(self, image_path, devpath):
167        logging.debug(f'Restoring image at {image_path} into {devpath}')
168        cmd_lzop = shlex.split(f'lzop -dc {image_path}')
169        cmd_pc = shlex.split(f'partclone.restore -d0 -C -I -o {devpath}')
170        cmd_mbuffer = shlex.split('mbuffer -q -m 40M') if shutil.which('mbuffer') else None
171
172        if not os.path.exists(image_path):
173            logging.error('f{image_path} does not exist, exiting.')
174            raise ValueError(f'Error: Image not found at {image_path}')
175
176        with open('/tmp/command.log', 'wb', 0) as logfile:
177            proc_lzop = subprocess.Popen(cmd_lzop,
178                                         stdout=subprocess.PIPE)
179            proc_pc = subprocess.Popen(cmd_pc,
180                                       stdin=proc_lzop.stdout,
181                                       stderr=logfile)
182            proc_lzop.stdout.close()
183            proc_pc.communicate()
184
185    def _ogbrowser_clear_logs(self):
186        logfiles = ['/tmp/command.log', '/tmp/session.log']
187        for logfile in logfiles:
188            with open(logfile, 'wb', 0) as f:
189                f.truncate(0)
190
191    def poweroff(self):
192        logging.info('Powering off client')
193        if os.path.exists('/scripts/oginit'):
194            cmd = f'source {ogClient.OG_PATH}etc/preinit/loadenviron.sh; ' \
195                  f'{ogClient.OG_PATH}scripts/poweroff'
196            subprocess.call([cmd], shell=True, executable=OG_SHELL)
197        else:
198            subprocess.call(['/sbin/poweroff'])
199
200    def reboot(self):
201        logging.info('Rebooting client')
202        if os.path.exists('/scripts/oginit'):
203            cmd = f'source {ogClient.OG_PATH}etc/preinit/loadenviron.sh; ' \
204                  f'{ogClient.OG_PATH}scripts/reboot'
205            subprocess.call([cmd], shell=True, executable=OG_SHELL)
206        else:
207            subprocess.call(['/sbin/reboot'])
208
209    def shellrun(self, request, ogRest):
210        cmd = request.getrun()
211        cmds = cmd.split(";|\n\r")
212
213        self._restartBrowser(self._url_log)
214
215        try:
216            ogRest.proc = subprocess.Popen(cmds,
217                               stdout=subprocess.PIPE,
218                               shell=True,
219                               executable=OG_SHELL)
220            (output, error) = ogRest.proc.communicate()
221        except:
222            logging.error('Exception when running "shell run" subprocess')
223            raise ValueError('Error: Incorrect command value')
224
225        if ogRest.proc.returncode != 0:
226            logging.warn('Non zero exit code when running: %s', ' '.join(cmds))
227        else:
228            logging.info('Shell run command OK')
229
230        self.refresh(ogRest)
231
232        return output.decode('utf-8')
233
234    def session(self, request, ogRest):
235        disk = request.getDisk()
236        partition = request.getPartition()
237        cmd = f'{ogClient.OG_PATH}interfaceAdm/IniciarSesion {disk} {partition}'
238
239        try:
240            ogRest.proc = subprocess.Popen([cmd],
241                               stdout=subprocess.PIPE,
242                               shell=True,
243                               executable=OG_SHELL)
244            (output, error) = ogRest.proc.communicate()
245        except:
246            logging.error('Exception when running session subprocess')
247            raise ValueError('Error: Incorrect command value')
248
249        logging.info('Starting OS at disk %s partition %s', disk, partition)
250        return output.decode('utf-8')
251
252    def software(self, request, path, ogRest):
253        disk = request.getDisk()
254        partition = request.getPartition()
255
256        self._restartBrowser(self._url_log)
257
258        try:
259            cmd = f'{ogClient.OG_PATH}interfaceAdm/InventarioSoftware {disk} ' \
260                  f'{partition} {path}'
261
262            ogRest.proc = subprocess.Popen([cmd],
263                               stdout=subprocess.PIPE,
264                               shell=True,
265                               executable=OG_SHELL)
266            (output, error) = ogRest.proc.communicate()
267        except:
268            logging.error('Exception when running software inventory subprocess')
269            raise ValueError('Error: Incorrect command value')
270
271        self._restartBrowser(self._url)
272
273        software = ''
274        with open(path, 'r') as f:
275            software = f.read()
276
277        logging.info('Software inventory command OK')
278        return software
279
280    def hardware(self, path, ogRest):
281        self._restartBrowser(self._url_log)
282
283        try:
284            cmd = f'{ogClient.OG_PATH}interfaceAdm/InventarioHardware {path}'
285            ogRest.proc = subprocess.Popen([cmd],
286                               stdout=subprocess.PIPE,
287                               shell=True,
288                               executable=OG_SHELL)
289            (output, error) = ogRest.proc.communicate()
290        except:
291            logging.error('Exception when running hardware inventory subprocess')
292            raise ValueError('Error: Incorrect command value')
293
294        self._restartBrowser(self._url)
295
296        logging.info('Hardware inventory command OK')
297        return output.decode('utf-8')
298
299    def setup(self, request, ogRest):
300        table_type = request.getType()
301        disk = request.getDisk()
302        cache = request.getCache()
303        cache_size = request.getCacheSize()
304        partlist = request.getPartitionSetup()
305
306        self._ogbrowser_clear_logs()
307        self._restartBrowser(self._url_log)
308
309        diskname = get_disks()[int(disk)-1]
310        cxt = fdisk.Context(f'/dev/{diskname}',
311                            details=True)
312
313        if table_type == 'MSDOS':
314            cxt.create_disklabel('dos')
315        elif table_type == 'GPT':
316            cxt.create_disklabel('gpt')
317
318        for part in partlist:
319            logging.debug(f'Adding partition: {part}')
320            if part["code"] == 'EMPTY':
321                continue
322            if ogRest.terminated:
323                break
324            if part["code"] == 'CACHE':
325                umount_cache()
326
327            pa = fdisk.Partition(start_follow_default=True,
328                                 end_follow_default=False,
329                                 partno_follow_default=False)
330            parttype = get_parttype(cxt, part["code"])
331            size = int(part["size"])
332            pa.size = (size * (1 << 10)) // cxt.sector_size
333            pa.partno = int(part["partition"]) - 1
334            pa.type = parttype
335            cxt.add_partition(pa)
336
337        cxt.write_disklabel()
338        subprocess.run('partprobe')
339
340        for part in partlist:
341            if part["filesystem"] == 'EMPTY':
342                continue
343            partition = int(part["partition"])
344            fs = part["filesystem"].lower()
345            if fs == 'cache':
346                mkfs('ext4', int(disk), partition, label='CACHE')
347                init_cache()
348            else:
349                mkfs(fs, int(disk), partition)
350
351        logging.info('Setup command OK')
352        result = self.refresh(ogRest)
353
354        self._restartBrowser(self._url)
355
356        return result
357
358    def image_restore(self, request, ogRest):
359        disk = request.getDisk()
360        partition = request.getPartition()
361        name = request.getName()
362        repo = request.getRepo()
363        ctype = request.getType()
364        profile = request.getProfile()
365        cid = request.getId()
366        partdev = get_partition_device(int(disk), int(partition))
367
368        self._ogbrowser_clear_logs()
369        self._restartBrowser(self._url_log)
370
371        logging.debug('Image restore params:')
372        logging.debug(f'\tname: {name}')
373        logging.debug(f'\trepo: {repo}')
374        logging.debug(f'\tprofile: {profile}')
375        logging.debug(f'\tctype: {ctype}')
376
377        if shutil.which('restoreImageCustom'):
378            restoreImageCustom(repo, name, disk, partition, ctype)
379        elif 'UNICAST' in ctype:
380            cache = 'DIRECT' not in ctype
381            self._restore_image_unicast(repo, name, partdev, cache)
382        elif ctype == 'TIPTORRENT':
383            self._restore_image_tiptorrent(repo, name, partdev)
384
385        output = configureOs(disk, partition)
386
387        self.refresh(ogRest)
388
389        logging.info('Image restore command OK')
390        return output
391
392    def image_create(self, path, request, ogRest):
393        disk = int(request.getDisk())
394        partition = int(request.getPartition())
395        name = request.getName()
396        repo = request.getRepo()
397        cmd_software = f'{ogClient.OG_PATH}interfaceAdm/InventarioSoftware {disk} ' \
398                   f'{partition} {path}'
399        image_path = f'/opt/opengnsys/images/{name}.img'
400
401        self._ogbrowser_clear_logs()
402        self._restartBrowser(self._url_log)
403
404        if ogChangeRepo(repo).returncode != 0:
405            self._restartBrowser(self._url)
406            logging.error('ogChangeRepo could not change repository to %s', repo)
407            raise ValueError(f'Error: Cannot change repository to {repo}')
408
409        try:
410            ogRest.proc = subprocess.Popen([cmd_software],
411                               stdout=subprocess.PIPE,
412                               shell=True,
413                               executable=OG_SHELL)
414            (output, error) = ogRest.proc.communicate()
415        except:
416            self._restartBrowser(self._url)
417            logging.error('Exception when running software inventory subprocess')
418            raise ValueError('Error: Incorrect command value')
419
420        if ogRest.terminated:
421            return
422
423        try:
424            diskname = get_disks()[disk-1]
425            cxt = fdisk.Context(f'/dev/{diskname}', details=True)
426            pa = None
427
428            for i, p in enumerate(cxt.partitions):
429                if (p.partno + 1) == partition:
430                    pa = cxt.partitions[i]
431
432            if pa is None:
433                self._restartBrowser(self._url)
434                logging.error('Target partition not found')
435                raise ValueError('Target partition number not found')
436
437            padev = cxt.partition_to_string(pa, fdisk.FDISK_FIELD_DEVICE)
438            fstype = cxt.partition_to_string(pa, fdisk.FDISK_FIELD_FSTYPE)
439            if not fstype:
440                    logging.error('No filesystem detected. Aborting image creation.')
441                    raise ValueError('Target partition has no filesystem present')
442
443            cambiar_acceso(user=self._smb_user, pwd=self._smb_pass)
444            ogCopyEfiBootLoader(disk, partition)
445            ogReduceFs(disk, partition)
446
447            cmd1 = shlex.split(f'partclone.{fstype} -I -C --clone -s {padev} -O -')
448            cmd2 = shlex.split(f'lzop -1 -fo {image_path}')
449
450            logfile = open('/tmp/command.log', 'wb', 0)
451
452            p1 = Popen(cmd1, stdout=PIPE, stderr=logfile)
453            p2 = Popen(cmd2, stdin=p1.stdout)
454            p1.stdout.close()
455
456            try:
457                    retdata = p2.communicate()
458            except OSError as e:
459                    logging.error('Unexpected error when running partclone and lzop commands')
460            finally:
461                    logfile.close()
462                    p2.terminate()
463                    p1.poll()
464
465            logging.info(f'partclone process exited with code {p1.returncode}')
466            logging.info(f'lzop process exited with code {p2.returncode}')
467
468            ogExtendFs(disk, partition)
469
470            image_info = ogGetImageInfo(image_path)
471        except:
472            self._restartBrowser(self._url)
473            logging.error('Exception when running "image create" subprocess')
474            raise ValueError('Error: Incorrect command value')
475
476        self._write_md5_file(f'/opt/opengnsys/images/{name}.img')
477
478        self._restartBrowser(self._url)
479
480        logging.info('Image creation command OK')
481        return image_info
482
483    def refresh(self, ogRest):
484        self._restartBrowser(self._url_log)
485
486        cache = cache_probe()
487        disks = get_disks()
488        interface = os.getenv('DEVICE')
489        link = ethtool(interface)
490        parsed = { 'serial_number': '',
491                'disk_setup': [],
492                'partition_setup': [],
493                'link': link
494        }
495
496        for num_disk, disk in enumerate(get_disks(), start=1):
497            logging.debug('refresh: processing %s', disk)
498            part_setup = {}
499            try:
500                cxt = fdisk.Context(device=f'/dev/{disk}', details=True)
501            except:
502                continue
503
504            self._refresh_payload_disk(cxt, part_setup, num_disk)
505            parsed['disk_setup'].append(part_setup)
506
507            for pa in cxt.partitions:
508                part_setup = part_setup.copy()
509                self._refresh_payload_partition(cxt, pa, part_setup, disk)
510                self._refresh_part_setup_cache(cxt, pa, part_setup, cache)
511                parsed['partition_setup'].append(part_setup)
512
513        generate_menu(parsed['partition_setup'])
514        generate_cache_txt()
515        self._restartBrowser(self._url)
516
517        logging.info('Sending response to refresh request')
518        return parsed
519
520    def probe(self, ogRest):
521
522        interface = os.getenv('DEVICE')
523        speed = ethtool(interface)
524
525        return {'status': 'OPG' if ogRest.state != ThreadState.BUSY else 'BSY',
526                'speed': speed}
Note: See TracBrowser for help on using the repository browser.