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

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

live: improve error paths

Fix error paths in live operations which do not
reset the "browser" to the main page (one with the menu).

Add error logging messages when:

  • _restartBrowser fails.
  • ogChangeRepo fails.

Improve checksum fetch error handling. For example, when an invalid
repository IP is specified.

  • Property mode set to 100644
File size: 17.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
23
24from src.utils.legacy import *
25from src.utils.net import ethtool
26from src.utils.menu import generate_menu
27from src.utils.fs import *
28from src.utils.probe import os_probe, cache_probe
29from src.utils.disk import *
30from src.utils.cache import generate_cache_txt
31from src.utils.tiptorrent import *
32
33
34OG_SHELL = '/bin/bash'
35
36class OgLiveOperations:
37    def __init__(self, config):
38        self._url = config['opengnsys']['url']
39        self._url_log = config['opengnsys']['url_log']
40        self._smb_user = config['samba']['user']
41        self._smb_pass = config['samba']['pass']
42
43    def _restartBrowser(self, url):
44        try:
45            proc = subprocess.call(["pkill", "-9", "browser"])
46            proc = subprocess.Popen(["browser", "-qws", url])
47        except:
48            logging.error('Cannot restart browser')
49            raise ValueError('Error: cannot restart browser')
50
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'
59        if not cxt.label:
60            part_setup['code'] = '0'
61        else:
62            part_setup['code'] = '2' if cxt.label.name == 'gpt' else '1'
63
64    def _refresh_payload_partition(self, cxt, pa, part_setup, disk):
65        parttype = cxt.partition_to_string(pa, fdisk.FDISK_FIELD_TYPEID)
66        fstype = cxt.partition_to_string(pa, fdisk.FDISK_FIELD_FSTYPE)
67        padev = cxt.partition_to_string(pa, fdisk.FDISK_FIELD_DEVICE)
68        size = cxt.partition_to_string(pa, fdisk.FDISK_FIELD_SIZE)
69        partnum = pa.partno + 1
70        source = padev
71        target = padev.replace('dev', 'mnt')
72
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'] = ''
89        part_setup['partition'] = str(partnum)
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
95        if (part_setup['filesystem'] == 'VFAT'):
96            part_setup['filesystem'] = 'FAT32'
97
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
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
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):
140        if ogChangeRepo(repo).returncode != 0:
141            self._restartBrowser(self._url)
142            logging.error('ogChangeRepo could not change repository to %s', repo)
143            raise ValueError(f'Error: Cannot change repository to {repo}')
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'
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'
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')
163        self._restore_image(image_path, devpath)
164
165    def _restore_image(self, image_path, devpath):
166        logging.debug(f'Restoring image at {image_path} into {devpath}')
167        cmd_lzop = shlex.split(f'lzop -dc {image_path}')
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
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
175        with open('/tmp/command.log', 'wb', 0) as logfile:
176            proc_lzop = subprocess.Popen(cmd_lzop,
177                                         stdout=subprocess.PIPE)
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
184    def poweroff(self):
185        logging.info('Powering off client')
186        if os.path.exists('/scripts/oginit'):
187            cmd = f'source {ogClient.OG_PATH}etc/preinit/loadenviron.sh; ' \
188                  f'{ogClient.OG_PATH}scripts/poweroff'
189            subprocess.call([cmd], shell=True, executable=OG_SHELL)
190        else:
191            subprocess.call(['/sbin/poweroff'])
192
193    def reboot(self):
194        logging.info('Rebooting client')
195        if os.path.exists('/scripts/oginit'):
196            cmd = f'source {ogClient.OG_PATH}etc/preinit/loadenviron.sh; ' \
197                  f'{ogClient.OG_PATH}scripts/reboot'
198            subprocess.call([cmd], shell=True, executable=OG_SHELL)
199        else:
200            subprocess.call(['/sbin/reboot'])
201
202    def shellrun(self, request, ogRest):
203        cmd = request.getrun()
204        cmds = cmd.split(";|\n\r")
205
206        self._restartBrowser(self._url_log)
207
208        try:
209            ogRest.proc = subprocess.Popen(cmds,
210                               stdout=subprocess.PIPE,
211                               shell=True,
212                               executable=OG_SHELL)
213            (output, error) = ogRest.proc.communicate()
214        except:
215            logging.error('Exception when running "shell run" subprocess')
216            raise ValueError('Error: Incorrect command value')
217
218        if ogRest.proc.returncode != 0:
219            logging.warn('Non zero exit code when running: %s', ' '.join(cmds))
220        else:
221            logging.info('Shell run command OK')
222
223        self.refresh(ogRest)
224
225        return output.decode('utf-8')
226
227    def session(self, request, ogRest):
228        disk = request.getDisk()
229        partition = request.getPartition()
230        cmd = f'{ogClient.OG_PATH}interfaceAdm/IniciarSesion {disk} {partition}'
231
232        try:
233            ogRest.proc = subprocess.Popen([cmd],
234                               stdout=subprocess.PIPE,
235                               shell=True,
236                               executable=OG_SHELL)
237            (output, error) = ogRest.proc.communicate()
238        except:
239            logging.error('Exception when running session subprocess')
240            raise ValueError('Error: Incorrect command value')
241
242        logging.info('Starting OS at disk %s partition %s', disk, partition)
243        return output.decode('utf-8')
244
245    def software(self, request, path, ogRest):
246        disk = request.getDisk()
247        partition = request.getPartition()
248
249        self._restartBrowser(self._url_log)
250
251        try:
252            cmd = f'{ogClient.OG_PATH}interfaceAdm/InventarioSoftware {disk} ' \
253                  f'{partition} {path}'
254
255            ogRest.proc = subprocess.Popen([cmd],
256                               stdout=subprocess.PIPE,
257                               shell=True,
258                               executable=OG_SHELL)
259            (output, error) = ogRest.proc.communicate()
260        except:
261            logging.error('Exception when running software inventory subprocess')
262            raise ValueError('Error: Incorrect command value')
263
264        self._restartBrowser(self._url)
265
266        software = ''
267        with open(path, 'r') as f:
268            software = f.read()
269
270        logging.info('Software inventory command OK')
271        return software
272
273    def hardware(self, path, ogRest):
274        self._restartBrowser(self._url_log)
275
276        try:
277            cmd = f'{ogClient.OG_PATH}interfaceAdm/InventarioHardware {path}'
278            ogRest.proc = subprocess.Popen([cmd],
279                               stdout=subprocess.PIPE,
280                               shell=True,
281                               executable=OG_SHELL)
282            (output, error) = ogRest.proc.communicate()
283        except:
284            logging.error('Exception when running hardware inventory subprocess')
285            raise ValueError('Error: Incorrect command value')
286
287        self._restartBrowser(self._url)
288
289        logging.info('Hardware inventory command OK')
290        return output.decode('utf-8')
291
292    def setup(self, request, ogRest):
293        table_type = request.getType()
294        disk = request.getDisk()
295        cache = request.getCache()
296        cache_size = request.getCacheSize()
297        partlist = request.getPartitionSetup()
298        cfg = f'dis={disk}*che={cache}*tch={cache_size}!'
299
300        for part in partlist:
301            cfg += f'par={part["partition"]}*cpt={part["code"]}*' \
302                   f'sfi={part["filesystem"]}*tam={part["size"]}*' \
303                   f'ope={part["format"]}%'
304
305            if ogRest.terminated:
306                break
307
308        cmd = f'{ogClient.OG_PATH}interfaceAdm/Configurar {table_type} {cfg}'
309        try:
310            ogRest.proc = subprocess.Popen([cmd],
311                               stdout=subprocess.PIPE,
312                               shell=True,
313                               executable=OG_SHELL)
314            (output, error) = ogRest.proc.communicate()
315        except:
316            logging.error('Exception when running setup subprocess')
317            raise ValueError('Error: Incorrect command value')
318
319        logging.info('Setup command OK')
320        result = self.refresh(ogRest)
321
322        return result
323
324    def image_restore(self, request, ogRest):
325        disk = request.getDisk()
326        partition = request.getPartition()
327        name = request.getName()
328        repo = request.getRepo()
329        ctype = request.getType()
330        profile = request.getProfile()
331        cid = request.getId()
332
333        partdev = get_partition_device(int(disk), int(partition))
334
335        self._restartBrowser(self._url_log)
336
337        logging.debug('Image restore params:')
338        logging.debug(f'\tname: {name}')
339        logging.debug(f'\trepo: {repo}')
340        logging.debug(f'\tprofile: {profile}')
341        logging.debug(f'\tctype: {ctype}')
342
343        if shutil.which('restoreImageCustom'):
344            restoreImageCustom(repo, name, disk, partition, ctype)
345        elif 'UNICAST' in ctype:
346            cache = 'DIRECT' not in ctype
347            self._restore_image_unicast(repo, name, partdev, cache)
348        elif ctype == 'TIPTORRENT':
349            self._restore_image_tiptorrent(repo, name, partdev)
350
351        output = configureOs(disk, partition)
352
353        self.refresh(ogRest)
354
355        logging.info('Image restore command OK')
356        return output
357
358    def image_create(self, path, request, ogRest):
359        disk = int(request.getDisk())
360        partition = int(request.getPartition())
361        name = request.getName()
362        repo = request.getRepo()
363        cmd_software = f'{ogClient.OG_PATH}interfaceAdm/InventarioSoftware {disk} ' \
364                   f'{partition} {path}'
365        image_path = f'/opt/opengnsys/images/{name}.img'
366
367        self._restartBrowser(self._url_log)
368
369        if ogChangeRepo(repo).returncode != 0:
370            self._restartBrowser(self._url)
371            logging.error('ogChangeRepo could not change repository to %s', repo)
372            raise ValueError(f'Error: Cannot change repository to {repo}')
373
374        try:
375            ogRest.proc = subprocess.Popen([cmd_software],
376                               stdout=subprocess.PIPE,
377                               shell=True,
378                               executable=OG_SHELL)
379            (output, error) = ogRest.proc.communicate()
380        except:
381            self._restartBrowser(self._url)
382            logging.error('Exception when running software inventory subprocess')
383            raise ValueError('Error: Incorrect command value')
384
385        if ogRest.terminated:
386            return
387
388        try:
389            diskname = get_disks()[disk-1]
390            cxt = fdisk.Context(f'/dev/{diskname}', details=True)
391            pa = None
392
393            for i, p in enumerate(cxt.partitions):
394                if (p.partno + 1) == partition:
395                    pa = cxt.partitions[i]
396
397            if pa is None:
398                self._restartBrowser(self._url)
399                logging.error('Target partition not found')
400                raise ValueError('Target partition number not found')
401
402            padev = cxt.partition_to_string(pa, fdisk.FDISK_FIELD_DEVICE)
403            fstype = cxt.partition_to_string(pa, fdisk.FDISK_FIELD_FSTYPE)
404            if not fstype:
405                    logging.error('No filesystem detected. Aborting image creation.')
406                    raise ValueError('Target partition has no filesystem present')
407
408            cambiar_acceso(user=self._smb_user, pwd=self._smb_pass)
409            ogCopyEfiBootLoader(disk, partition)
410            ogReduceFs(disk, partition)
411
412            cmd1 = shlex.split(f'partclone.{fstype} -I -C --clone -s {padev} -O -')
413            cmd2 = shlex.split(f'lzop -1 -fo {image_path}')
414
415            logfile = open('/tmp/command.log', 'wb', 0)
416
417            p1 = Popen(cmd1, stdout=PIPE, stderr=logfile)
418            p2 = Popen(cmd2, stdin=p1.stdout)
419            p1.stdout.close()
420
421            try:
422                    retdata = p2.communicate()
423            except OSError as e:
424                    logging.error('Unexpected error when running partclone and lzop commands')
425            finally:
426                    logfile.close()
427                    p2.terminate()
428                    p1.poll()
429
430            logging.info(f'partclone process exited with code {p1.returncode}')
431            logging.info(f'lzop process exited with code {p2.returncode}')
432
433            ogExtendFs(disk, partition)
434
435            image_info = ogGetImageInfo(image_path)
436        except:
437            self._restartBrowser(self._url)
438            logging.error('Exception when running "image create" subprocess')
439            raise ValueError('Error: Incorrect command value')
440
441        self._write_md5_file(f'/opt/opengnsys/images/{name}.img')
442
443        self._restartBrowser(self._url)
444
445        logging.info('Image creation command OK')
446        return image_info
447
448    def refresh(self, ogRest):
449        self._restartBrowser(self._url_log)
450
451        cache = cache_probe()
452        disks = get_disks()
453        interface = os.getenv('DEVICE')
454        link = ethtool(interface)
455        parsed = { 'serial_number': '',
456                'disk_setup': [],
457                'partition_setup': [],
458                'link': link
459        }
460
461        for num_disk, disk in enumerate(get_disks(), start=1):
462            logging.debug('refresh: processing %s', disk)
463            part_setup = {}
464            try:
465                cxt = fdisk.Context(device=f'/dev/{disk}', details=True)
466            except:
467                continue
468
469            self._refresh_payload_disk(cxt, part_setup, num_disk)
470            parsed['disk_setup'].append(part_setup)
471
472            for pa in cxt.partitions:
473                part_setup = part_setup.copy()
474                self._refresh_payload_partition(cxt, pa, part_setup, disk)
475                self._refresh_part_setup_cache(cxt, pa, part_setup, cache)
476                parsed['partition_setup'].append(part_setup)
477
478        generate_menu(parsed['partition_setup'])
479        generate_cache_txt()
480        self._restartBrowser(self._url)
481
482        logging.info('Sending response to refresh request')
483        return parsed
484
485    def probe(self, ogRest):
486
487        interface = os.getenv('DEVICE')
488        speed = ethtool(interface)
489
490        return {'status': 'OPG' if ogRest.state != ThreadState.BUSY else 'BSY',
491                'speed': speed}
Note: See TracBrowser for help on using the repository browser.