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

Last change on this file since 29c53e5 was c010c42, checked in by Jose M. Guisado <jguisado@…>, 2 years ago

live: clear ogbrowser log before image_create

Remove any previous unrelated log message shown in the "real time log"
html page before executing this command.

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