#!/usr/bin/python3

import subprocess
import re
import json
import os.path
import shutil

import ogGlobals
import SystemLib
import ImageLib
import FileSystemLib
import StringLib
import NetLib
import DiskLib
import FileLib
import CacheLib

#/**
#@file    ProtocolLib.py
#@brief   Librería o clase Protocol
#@class   Protocol
#@brief   Funciones para transmisión de datos
#@warning License: GNU GPLv3+
#*/


##################### FUNCIONES UNICAST ################

#/**
#         ogUcastSyntax
#@brief   Función para generar la instrucción de transferencia de datos unicast
#@param 1 Tipo de operación [ SENDPARTITION RECEIVERPARTITION SENDFILE RECEIVERFILE ]
#@param 2 Sesion Unicast
#@param 3 Dispositivo (opción PARTITION) o fichero(opción FILE) que será enviado.
#@param 4 Tools de clonación (opcion PARTITION)
#@param 5 Tools de compresion (opcion PARTITION)
#@return  instrucción para ser ejecutada.
#@exception OG_ERR_FORMAT     formato incorrecto.
#@exception OG_ERR_UCASTSYNTAXT     formato de la sesion unicast incorrecta.
#@note    Requisitos: mbuffer
#@todo: controlar que mbuffer esta disponible para los clientes.
#*/ ##

#ogUcastSyntax SENDPARTITION     8000:172.17.36.11:172.17.36.12 device tool level
#ogUcastSyntax RECEIVERPARTITION 8000:172.17.36.249             device tool level

#ogUcastSyntax SENDFILE          8000:172.17.36.11:172.17.36.12 file
#ogUcastSyntax RECEIVERFILE      8000:172.17.36.249             file

def ogUcastSyntax (op, sess, file=None, device=None, tool=None, level=None):
    if 'SENDPARTITION' == op or 'RECEIVERPARTITION' == op:
        if device is None:
            raise TypeError ('missing required argument: "device"')
        if tool is None:
            raise TypeError ('missing required argument: "tool"')
        if tool.lower() not in ['partclone', 'partimage', 'ntfsclone']:
            raise TypeError (f'argument "tool" has unsupported value "{tool}"')
        if level is None:
            raise TypeError ('missing required argument: "level"')
        if level.lower() not in ['lzop', 'gzip', 'zstd']:
            raise TypeError (f'argument "level" has unsupported value "{level}"')
    elif 'SENDFILE' == op or 'RECEIVERFILE' == op:
        if file is None:
            raise TypeError ('missing required argument: "file"')
    else:
        raise TypeError ('first parameter should match (SEND|RECEIVER)(PARTITION|FILE), eg. "SENDFILE"')

    if 'SEND' in op: mode = 'server'
    else:            mode = 'client'

    session = sess.split (':')

    portbase = int (session[0])
    if portbase not in range (8000, 8006):
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_FORMAT, f'McastSession portbase {portbase}') ## || PERROR=3
        return

    if 'server' == mode:
        address = ''
        for i in range (1, len (session)):
            address += f' -O {session[i]}:{portbase}'
    else:
        address = f'{session[1]}:{portbase}'

    if 'SENDPARTITION' == op:
        syn = ImageLib.ogCreateImageSyntax (device, ' ', tool, level)
        ## REQUIRES package mbuffer to be installed!!
        ## otherwise, param2 in ImageLib.ogCreateImageSyntax() is not '| mbuffer' but empty
        ## and then parts[2] is out of range
        parts = syn.split ('|')
        prog1 = f'{parts[0]}|{parts[2]}'.strip()
        prog1 = prog1.replace ('>', '').strip()
        return f'{prog1} | mbuffer {address}'
    elif 'RECEIVERPARTITION' == op:
        syn = ImageLib.ogRestoreImageSyntax (' ', device, tool, level)
        parts = syn.split ('|')
        compressor = parts[0].strip()
        tools      = parts[-1].strip()
        return f'mbuffer -I {address} | {compressor} | {tools}'
    elif 'SENDFILE' == op:
        return f'mbuffer {address} -i {file}'
    elif 'RECEIVERFILE' == op:
        return f'mbuffer -I {address} -i {file}'
    else:
        pass   ## shouldn't happen


#/**
#         ogUcastSendPartition
#@brief   Función para enviar el contenido de una partición a multiples particiones remotas usando UNICAST.
#@param 1  disk
#@param 2  partition
#@param 3  sesionUcast
#@param 4  tool image
#@param 5  tool compresor
#@return
#@exception $OG_ERR_FORMAT
#@exception $OG_ERR_UCASTSENDPARTITION
#@note
#@todo: ogIsLocked siempre devuelve 1
#*/ ##
#ogUcastSendPartition 1 2 8001:192.168.1.113 partclone lzop
def ogUcastSendPartition (disk, par, sess, tool, level):
    PART = DiskLib.ogDiskToDev (disk, par)
    if not PART: return None

    FileSystemLib.ogUnmount (disk, par)

    cmd = ogUcastSyntax ('SENDPARTITION', sess, device=PART, tool=tool, level=level)
    if not cmd: return None
    rc = subprocess.run (cmd, shell=True).returncode
    if rc:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_UCASTSENDPARTITION, f'pipeline "{cmd}" returned code "{rc}"')
        return None
    return True



#/**
#         ogUcastReceiverPartition
#@brief   Función para recibir directamente en la partición el contenido de un fichero imagen remoto enviado por UNICAST.
#@param 1   disk
#@param 2   partition
#@param 3   session unicast
#@param 4   tool image
#@param 5   tool compresor
#@return
#@exception OG_ERR_FORMAT
#@exception OG_ERR_UCASTRECEIVERPARTITION
#@note
#@todo:
#*/ ##
#ogUcastReceiverPartition 1 1 8001:192.168.1.111 partclone lzop
def ogUcastReceiverPartition (disk, par, sess, tool, level):
    PART = DiskLib.ogDiskToDev (disk, par)
    if not PART: return None

    FileSystemLib.ogUnmount (disk, par)

    cmd = ogUcastSyntax ('RECEIVERPARTITION', sess, device=PART, tool=tool, level=level)
    if not cmd: return None
    rc = subprocess.run (cmd, shell=True).returncode
    if rc:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_UCASTRECEIVERPARTITION, f'pipeline "{cmd}" returned code "{rc}"')
        return None
    return True



#/**
#         ogUcastSendFile [ str_repo | int_ndisk int_npart ] /Relative_path_file  sessionMulticast
#@brief   Envía un fichero por unicast   ORIGEN(fichero) DESTINO(sessionmulticast)
#@param (2 parámetros)  $1 path_aboluto_fichero  $2 sesionMcast
#@param (3 parámetros)  $1 Contenedor REPO|CACHE $2 path_absoluto_fichero $3 sesionMulticast
#@param (4 parámetros)  $1 disk $2 particion $3 path_absoluto_fichero $4 sesionMulticast
#@return
#@exception OG_ERR_FORMAT     formato incorrecto.
#@exception $OG_ERR_NOTFOUND
#@exception OG_ERR_UCASTSENDFILE
#@note    Requisitos:
#*/ ##
#

## TODO esta función es idéntica a ogMcastSendFile pero con s/Ucast/Mcast/;

#ogUcastSendFile ([str_REPOSITORY] [int_ndisk int_npart] /Relative_path_file sesionMcast(puerto:ip:ip:ip)" \
#ogUcastSendFile (disk=1, par=1,     file='/aula1/winxp.img',                      sess='8000:172.17.36.11:172.17.36.12')
#ogUcastSendFile (container='REPO',  file='/aula1/ubuntu.iso',                     sess='sesionUcast')
#ogUcastSendFile (container='CACHE', file='/aula1/winxp.img',                      sess='sesionUcast')
#ogUcastSendFile (                   file='/opt/opengnsys/images/aula1/hd500.vmx', sess='sesionUcast')

#ogUcastSendFile 1 2 /boot/vmlinuz-6.8.0-51-generic 8000:192.168.1.113
#ogUcastSendFile 1 4 /swapfile.sys 8000:192.168.1.111
#ogUcastSendFile REPO /ubu24.img 8000:192.168.1.111
def ogUcastSendFile (disk=None, par=None, container=None, file=None, sess=None):
    if file is None:
        raise TypeError ('missing required argument: "file"')
    if sess is None:
        raise TypeError ('missing required argument: "sess"')

    if container is not None:
        if disk is None and par is None:
            ## we were given container=
            source = FileLib.ogGetPath (src=container, file=file)
            dev_err = f'{container} {file}'
        else:
            raise TypeError ('argument "container" can be specified along neither "disk" nor "par"')

    else:
        if disk is not None and par is not None:
            ## we were given disk= par=
            source = FileLib.ogGetPath (src=f'{disk} {par}', file=file)
            dev_err = f'{disk} {par} {file}'
        elif disk is None and par is None:
            ## we were given nothing
            source = FileLib.ogGetPath (file=file)
            dev_err = file
        else:
            raise TypeError ('if one of "disk" and "par" are specified, then both must be')

    if not source:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTFOUND, f'device or file {dev_err} not found')
        return

    path2 = FileLib.ogGetPath (file=source)
    if not path2:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTFOUND, f'device or file {dev_err} not found')
        return

    cmd = ogUcastSyntax ('SENDFILE', sess, file=source)
    if not cmd: return None
    rc = subprocess.run (cmd, shell=True).returncode
    if rc:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_UCASTSENDFILE, f'pipeline "{cmd}" returned code "{rc}"')
        return None
    return True

def _clientip():
    ipas = subprocess.run (['ip', '-json', 'address', 'show', 'up'], capture_output=True, text=True).stdout
    ipasj = json.loads (ipas)
    addresses = []
    for e in ipasj:
        if not isinstance(e, dict): continue
        if 'ifname' not in e: continue
        if 'lo' == e['ifname']: continue
        if 'addr_info' not in e: continue
        addrs = e['addr_info']
        for a in addrs:
            if 'inet' != a['family']: continue
            addresses.append ({ 'local': a['local'], 'prefixlen': a['prefixlen'] })
    return addresses

def _binary_ip (ip):
    for l in subprocess.run (['ipcalc', '--nocolor', ip ], capture_output=True, text=True).stdout.splitlines():
        if 'Address' not in l: continue
        match = re.search (r'^(Address:)\s+(\S+)\s+(.*$)', l).group(3).replace (' ', '').replace ('.', '')
        break
    return match

#/**
#         ogMcastSyntax
#@brief   Función para generar la instrucción de ejucción la transferencia de datos multicast
#@param 1 Tipo de operación [ SENDPARTITION RECEIVERPARTITION SENDFILE RECEIVERFILE ]
#@param 2 Sesión Mulicast
#@param 3 Dispositivo (opción PARTITION) o fichero(opción FILE) que será enviado.
#@param 4 Tools de clonación (opcion PARTITION)
#@param 5 Tools de compresion (opcion PARTITION)
#@return  instrucción para ser ejecutada.
#@exception OG_ERR_FORMAT     formato incorrecto.
#@exception OG_ERR_NOTEXEC
#@exception OG_ERR_MCASTSYNTAXT
#@note    Requisitos: upd-cast 2009 o superior
#@todo    localvar  check versionudp
#*/ ##
#

#ogMcastSyntax SENDPARTITION      9000:full-duplex|half-duplex|broadcast:239.194.17.36:80M:50:60 device tools level
#ogMcastSyntax RECEIVERPARTITION  9000                                                           device tools level
#ogMcastSyntax RECEIVERPARTITION  9000:172.17.88.161:40:120                                      device tools level

#ogMcastSyntax SENDFILE           9000:full-duplex|half-duplex|broadcast:239.194.17.36:80M:50:60 file
#ogMcastSyntax RECEIVERFILE       9000                                                           file
#ogMcastSyntax RECEIVERFILE       9000:172.17.88.161:40:120                                      file
def ogMcastSyntax (op, sess, file=None, device=None, tool=None, level=None):
    if 'SENDPARTITION' == op or 'RECEIVERPARTITION' == op:
        if device is None:
            raise TypeError ('missing required argument: "device"')
        if tool is None:
            raise TypeError ('missing required argument: "tool"')
        if tool.lower() not in ['partclone', 'partimage', 'ntfsclone']:
            raise TypeError (f'argument "tool" has unsupported value "{tool}"')
        if level is None:
            raise TypeError ('missing required argument: "level"')
        if level.lower() not in ['lzop', 'gzip', 'zstd']:
            raise TypeError (f'argument "level" has unsupported value "{level}"')
    elif 'SENDFILE' == op or 'RECEIVERFILE' == op:
        if file is None:
            raise TypeError ('missing required argument: "file"')
    else:
        raise TypeError ('first parameter should match (SEND|RECEIVER)(PARTITION|FILE), eg. "SENDFILE"')

    if 'SEND' in op: mode = 'server'
    else:            mode = 'client'

    try:
        isudpcast = subprocess.run (['udp-receiver', '--help'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True).stdout
    except subprocess.CalledProcessError:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTEXEC, 'upd-cast no existe')
        return

    session = sess.split (':')

    PERROR = 0
    if 'server' == mode:
        if 6 != len (session):
            SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_FORMAT, 'parametros session de servidor multicast no completa')
            PERROR = 2
    elif 'client' == mode:
        if 4 < len (session):
            SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_FORMAT, 'parametros session de cliente multicast no completa')
            PERROR = 2

    mbuffer = " --pipe 'mbuffer -q -m 20M' "
    portbase = int (session[0])
    if portbase not in range (9000, 9100, 2):
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_FORMAT, f'McastSession portbase {portbase}')
        PERROR = 3

    if 'server' == mode:
        method, address, bitrate, nclients, maxtime = session[1:]

        if method.lower() not in ['full-duplex', 'half-duplex', 'broadcast']:
            SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_FORMAT, f'McastSession method {method}')
            PERROR = 4

        if not StringLib.ogCheckIpAddress (address):
            SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_FORMAT, f'McastSession address {address}')
            PERROR = 5

        ## the original regex has a backslash: ^[0-9]{1,3}\M$
        ## not sure why
        if not re.search (r'^[0-9]{1,3}M$', bitrate):
            SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_FORMAT, f'McastSession bitrate {bitrate}')
            PERROR = 6

        if not re.search (r'^[0-9]{1,10}$', nclients):
            SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_FORMAT, f'McastSession nclients {nclients}')
            PERROR = 7

        if not re.search (r'^[0-9]{1,10}$', maxtime):
            SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_FORMAT, f'McastSession maxtime {maxtime}')
            PERROR = 8

        cerror  = '8x8/128'
        syntaxserver = f'udp-sender {mbuffer} --nokbd --portbase {portbase} --{method} --mcast-data-address {address} --fec {cerror} --max-bitrate {bitrate} --ttl 16 --min-clients {nclients} --max-wait {maxtime} --autostart {maxtime} --log /tmp/mcast.log'

    if PERROR:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_MCASTSYNTAXT, f' {PERROR}')
        return


    if 'client' == mode:
        other = session[1:]
        serveraddress   = other[0] if len (other) > 0 else ''
        starttimeout    = other[1] if len (other) > 1 else ''
        receivertimeout = other[2] if len (other) > 2 else ''

        ## serveraddres
        if StringLib.ogCheckIpAddress (serveraddress):
            serveraddress = f' --mcast-rdv-address {serveraddress}'
        else:
            repoip = NetLib.ogGetRepoIp()
            clientip = _clientip()
            if 1 != len (clientip):
                raise Exception ('more than one local IP address found')
            c = clientip[0]
            clientip = c['local']
            mascara  = c['prefixlen']

            ripbt = _binary_ip (repoip)
            ipbt  = _binary_ip (clientip)
            reposubred   = ripbt[0:mascara]
            clientsubred =  ipbt[0:mascara]
            if reposubred == clientsubred: serveraddress = ' '
            else:                          serveraddress = f' --mcast-rdv-address {repoip}'

        ## starttimeout
        if re.search (r'^[0-9]{1,10}$', starttimeout):
            if 0 == starttimeout: starttimeout = ' '
            else:                 starttimeout = f' --start-timeout {starttimeout}'
        else:
            starttimeout = f' --start-timeout {ogGlobals.MCASTERRORSESSION}'
        if 'start-timeout' not in isudpcast: starttimeout = ' '

        ## receivertimeout
        if re.search (r'^[0-9]{1,10}$', receivertimeout):
            if 0 == receivertimeout: receivertimeout = ' '
            else:                    receivertimeout = f' --receive-timeout {receivertimeout}'
        else:
            receivertimeout = f' --receive-timeout {ogGlobals.MCASTWAIT}'
        if 'receive-timeout' not in isudpcast: receivertimeout = ' '

        syntaxclient = f'udp-receiver {mbuffer} --portbase {portbase} {serveraddress} {starttimeout} {receivertimeout} --log /tmp/mcast.log'

    if 'SENDPARTITION' == op:
        syn = ImageLib.ogCreateImageSyntax (device, ' ', tool, level)
        ## REQUIRES package mbuffer to be installed!!
        ## otherwise, param2 in ImageLib.ogCreateImageSyntax() is not '| mbuffer' but empty
        ## and then parts[2] is out of range
        parts = syn.split ('|')
        prog1 = f'{parts[0]}|{parts[2]}'.strip()
        prog1 = prog1.replace ('>', '').strip()
        return f'{prog1} | {syntaxserver}'

    elif 'RECEIVERPARTITION' == op:
        syn = ImageLib.ogRestoreImageSyntax (' ', device, tool, level)
        parts = syn.split ('|')
        compressor = parts[0].strip()
        tools      = parts[-1].strip()
        return f'{syntaxclient} | {compressor} | {tools} '

    elif 'SENDFILE' == op:
        return f'{syntaxserver} --file {file}'

    elif 'RECEIVERFILE' == op:
        return f'{syntaxclient} --file {file}'

    else:
        raise Exception (f'unknown op ({op})--this should not happen')



#/**
#         ogMcastSendFile [ str_repo | int_ndisk int_npart ] /Relative_path_file  sessionMulticast
#@brief   Envía un fichero por multicast   ORIGEN(fichero) DESTINO(sessionmulticast)
#@param (2 parámetros)  $1 path_aboluto_fichero  $2 sesionMcast
#@param (3 parámetros)  $1 Contenedor REPO|CACHE $2 path_absoluto_fichero $3 sesionMulticast
#@param (4 parámetros)  $1 disk $2 particion $3 path_absoluto_fichero $4 sesionMulticast
#@return
#@exception OG_ERR_FORMAT     formato incorrecto.
#@exception $OG_ERR_NOTFOUND
#@exception OG_ERR_MCASTSENDFILE
#*/ ##
#

#ogMcastSendFile [str_REPOSITORY] [int_ndisk int_npart] /Relative_path_file sesionMcast" \
#ogMcastSendFile (disk=1, par=1,     file='/aula1/winxp.img',                      sess='sesionMcast')
#ogMcastSendFile (container='REPO',  file='/aula1/ubuntu.iso',                     sess='sesionMcast')
#ogMcastSendFile (container='CACHE', file='/aula1/winxp.img',                      sess='sesionMcast')
#ogMcastSendFile (                   file='/opt/opengnsys/images/aula1/hd500.vmx', sess='sesionMcast')

#ogMcastSendFile 1 2  /boot/vmlinuz-6.8.0-51-generic     9000:full-duplex:239.194.37.31:50M:20:10
#ogMcastSendFile REPO /ubu24.img                         9000:full-duplex:239.194.37.31:50M:20:10
#ogMcastSendFile      /usr/lib64/ld-linux-x86-64.so.2    9000:full-duplex:239.194.37.31:50M:20:10
def ogMcastSendFile (disk=None, par=None, container=None, file=None, sess=None):
    if file is None:
        raise TypeError ('missing required argument: "file"')
    if sess is None:
        raise TypeError ('missing required argument: "sess"')

    if container is not None:
        if disk is None and par is None:
            ## we were given container=
            source = FileLib.ogGetPath (src=container, file=file)
            dev_err = f'{container} {file}'
        else:
            raise TypeError ('argument "container" can be specified along neither "disk" nor "par"')

    else:
        if disk is not None and par is not None:
            ## we were given disk= par=
            source = FileLib.ogGetPath (src=f'{disk} {par}', file=file)
            dev_err = f'{disk} {par} {file}'
        elif disk is None and par is None:
            ## we were given nothing
            source = FileLib.ogGetPath (file=file)
            dev_err = file
        else:
            raise TypeError ('if one of "disk" and "par" are specified, then both must be')

    if not source:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTFOUND, f'device or file {dev_err} not found')
        return

    path2 = FileLib.ogGetPath (file=source)
    if not path2:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTFOUND, f'device or file {dev_err} not found')
        return

    cmd = ogMcastSyntax ('SENDFILE', sess, file=source)
    if not cmd: return None
    rc = subprocess.run (cmd, shell=True).returncode
    if rc:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_MCASTSENDFILE, f'pipeline "{cmd}" returned code "{rc}"')
        return None
    return True



#/**
#         ogMcastReceiverFile  sesion Multicast [ str_repo | int_ndisk int_npart ] /Relative_path_file
#@brief   Recibe un fichero multicast   ORIGEN(sesionmulticast) DESTINO(fichero)
#@param (2 parámetros)  $1 sesionMcastCLIENT $2 path_aboluto_fichero_destino
#@param (3 parámetros)  $1 sesionMcastCLIENT $2 Contenedor REPO|CACHE $3 path_absoluto_fichero_destino
#@param (4 parámetros)  $1 sesionMcastCLIENT $2 disk $3 particion $4 path_absoluto_fichero_destino
#@return
#@exception OG_ERR_FORMAT     formato incorrecto.
#@exception $OG_ERR_MCASTRECEIVERFILE
#@note    Requisitos:
#*/ ##
#

#ogMcastReceiverFile ([ str_portMcast] [ [Relative_path_file] | [str_REPOSITORY path_file] |  [int_ndisk int_npart path_file ]  ]" \
#ogMcastReceiverFile (                   file='/PS1_PH1.img',       sess='9000')
#ogMcastReceiverFile (container='CACHE', file='/aula1/PS2_PH4.img', sess='9000')
#ogMcastReceiverFile (disk=1, par=1,     file='/isos/linux.iso',    sess='9000')

#ogMcastReceiverFile 9000:full-duplex:239.194.37.31 1 4 /hola
def ogMcastReceiverFile (disk=None, par=None, container=None, file=None, sess=None):
    if file is None:
        raise TypeError ('missing required argument: "file"')
    if sess is None:
        raise TypeError ('missing required argument: "sess"')

    if container is not None:
        if disk is None and par is None:
            ## we were given container=
            targetdir = FileLib.ogGetParentPath (src=container, file=file)
            dev_err = f'{container} {file}'
        else:
            raise TypeError ('argument "container" can be specified along neither "disk" nor "par"')

    else:
        if disk is not None and par is not None:
            ## we were given disk= par=
            targetdir = FileLib.ogGetParentPath (src=f'{disk} {par}', file=file)
            dev_err = f'{disk} {par} {file}'
        elif disk is None and par is None:
            ## we were given nothing
            targetdir = FileLib.ogGetParentPath (file=file)
            dev_err = file
        else:
            raise TypeError ('if one of "disk" and "par" are specified, then both must be')

    if not targetdir:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTFOUND, f'target directory {targetdir} not found')
        return

    targetfile = os.path.basename (file)

    cmd = ogMcastSyntax ('RECEIVERFILE', sess, file=os.path.join (targetdir, targetfile))
    if not cmd: return None
    rc = subprocess.run (cmd, shell=True).returncode
    if rc:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_MCASTRECEIVERFILE, f'pipeline "{cmd}" returned code "{rc}"')
        return None
    return True


#/**
#         ogMcastSendPartition
#@brief   Función para enviar el contenido de una partición a multiples particiones remotas.
#@param 1   disk
#@param 2  partition
#@param 3  session multicast
#@param 4  tool clone
#@param 5  tool compressor
#@return
#@exception OG_ERR_FORMAT
#@exception OG_ERR_MCASTSENDPARTITION
#@note
#@todo: ogIsLocked siempre devuelve 1. crear ticket
#*/ ##

#ogMcastSendPartition (disk, par, SessionMulticastSERVER, tools, compresor)
#ogMcastSendPartition (1, 1, '9000:full-duplex:239.194.37.31:50M:20:2', 'partclone', 'lzop')
#ogMcastSendPartition 1 2 9000:full-duplex:239.194.37.31:50M:20:10 partclone lzop
def ogMcastSendPartition (disk, par, sess, tool, compressor):
    PART = DiskLib.ogDiskToDev (disk, par)
    if not PART: return

    FileSystemLib.ogUnmount (disk, par)
    cmd = ogMcastSyntax ('SENDPARTITION', sess, device=PART, tool=tool, level=compressor)
    if not cmd: return None
    rc = subprocess.run (cmd, shell=True).returncode
    if rc:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_MCASTSENDPARTITION, f'pipeline "{cmd}" returned code "{rc}"')
        return None
    return True

#/**
#         ogMcastReceiverPartition
#@brief   Función para recibir directamente en la partición el contenido de un fichero imagen remoto enviado por multicast.
#@param 1   disk
#@param 2  partition
#@param 3  session multicast
#@param 4  tool clone
#@param 5  tool compressor
#@return
#@exception $OG_ERR_FORMAT
#*/ ##
#ogMcastReceiverPartition 1 1 9000:full-duplex:239.194.37.31 partclone lzop
def ogMcastReceiverPartition (disk, par, sess, tool, compressor):
    PART = DiskLib.ogDiskToDev (disk, par)
    if not PART: return

    FileSystemLib.ogUnmount (disk, par)
    cmd = ogMcastSyntax ('RECEIVERPARTITION', sess, device=PART, tool=tool, level=compressor)
    if not cmd: return None
    rc = subprocess.run (cmd, shell=True).returncode
    if rc:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_MCASTRECEIVERPARTITION, f'pipeline "{cmd}" returned code "{rc}"')    ## original code has OG_ERR_MCASTSENDPARTITION
        return None
    return True


##########################################
############## funciones torrent
#/**
#         ogTorrentStart  [ str_repo | int_ndisk int_npart ] Relative_path_file.torrent | SessionProtocol
#@brief   Función iniciar P2P - requiere un tracker para todos los modos, y un seeder para los modos peer y leecher y los ficheros .torrent.
#@param   str_pathDirectory  str_Relative_path_file
#@param  int_disk    int_partition   str_Relative_path_file
#@param  str_REPOSITORY(CACHE - LOCAL)  str_Relative_path_file
#@param (2 parámetros)  $1 path_aboluto_fichero_torrent  $2 Parametros_Session_Torrent
#@param (3 parámetros)  $1 Contenedor CACHE $2 path_absoluto_fichero_Torrent $3 Parametros_Session_Torrent
#@param (4 parámetros)  $1 disk $2 particion $3 path_absoluto_fichero_Torrent 4$ Parametros_Session_Torrent
#@return
#@note    protocoloTORRENT=mode:time   mode=seeder  -> Dejar el equipo seedeando hasta que transcurra el tiempo indicado o un kill desde consola, mode=peer    -> seedear mientras descarga mode=leecher  -> NO seedear mientras descarga time tiempo que una vez descargada la imagen queremos dejar al cliente como seeder.
#*/ ##
#ogTorrentStart (                   torrentfile='/opt/opengnsys/cache/linux.iso', torrentsess='peer:60')
#ogTorrentStart (container='CACHE', torrentfile='/PS1_PH1.img.torrent',           torrentsess='seeder:10000')
#ogTorrentStart (disk=1, par=1,     torrentfile='/linux.iso.torrent',             torrentsess='leecher:60')
#ogTorrentStart /opt/opengnsys/cache/opt/opengnsys/images/ubu24.EXTFS.torrent seeder:10000
#ogTorrentStart 1 4 /opt/opengnsys/images/ubu24.EXTFS.torrent seeder:10000
#ogTorrentStart CACHE /ubu24.EXTFS.torrent seeder:10000
def ogTorrentStart (disk=None, par=None, container=None, torrentfile=None, torrentsess=None):
    if torrentfile is None:
        raise TypeError ('missing required argument: "torrentfile"')
    if torrentsess is None:
        raise TypeError ('missing required argument: "torrentsess"')

    if container is not None:
        if disk is None and par is None:
            ## we were given container=
            if 'CACHE' != container.upper():
                SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_FORMAT, 'La descarga torrent solo se hace desde local, copia el torrent a la cache y realiza la operación desde esa ubicación')
                return None
            source = FileLib.ogGetPath (src=container, file=torrentfile)
            dev_err = f'{container} {torrentfile}'
        else:
            raise TypeError ('argument "container" can be specified along neither "disk" nor "par"')

    else:
        if disk is not None and par is not None:
            ## we were given disk= par=
            source = FileLib.ogGetPath (src=f'{disk} {par}', file=torrentfile)
            dev_err = f'{disk} {par} {torrentfile}'
        elif disk is None and par is None:
            ## we were given nothing
            if torrentfile.startswith ('/opt/opengnsys/images'):
                SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_FORMAT, 'La descarga torrent solo se hace desde local, copia el torrent a la cache y realiza la operación desde esa ubicación')
                return None
            source = FileLib.ogGetPath (file=torrentfile)
            dev_err = torrentfile
        else:
            raise TypeError ('if one of "disk" and "par" are specified, then both must be')

    if not source:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTFOUND, f'device or torrentfile {dev_err} not found')
        return

    if subprocess.run (['aria2c', '-S', source]).returncode:      ## if True, then aria2c failed
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTFOUND, '')
        return None

    target = re.sub (r'\.torrent$', '', source)
    dirsource = FileLib.ogGetParentPath (file=source)

    ERROR = None
    sess = torrentsess.split (':')
    if 2 != len (sess):
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_FORMAT, 'parametros session Torrent no completa:  modo:tiempo')
        return None

    mode = sess[0].lower()
    if mode not in ['seeder', 'peer', 'leecher']:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_FORMAT, f'valor modo Torrent no valido {sess[0]}')
        return None

    time = sess[1]
    if not re.search (r'^[0-9]{1,10}$', time):
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_FORMAT, f'valor tiempo no valido {sess[1]}')
        return None
    time = int (time)

    OPTION = None
    cwd = os.getcwd()
    # si No fichero .aria2, y Si fichero destino imagen ya descargada y su chequeo fue comprobado en su descarga inicial.
    
    if not os.path.exists (f'{target}.aria2') and os.path.exists (target):
        print ('imagen ya descargada')
        if 'seeder' != mode: return 'success'    ## return any true value
        print ('MODE seeder aria2c')
        os.chdir (dirsource)
        aria2_cmd = [
            "aria2c", 
            "--check-integrity=true", 
            "--bt-seed-unverified=true", 
            "--disable-ipv6",
            "--seed-ratio=0.0", 
            f"--seed-time={time}", 
            "--enable-dht=false", 
            "--enable-peer-exchange=false", 
            f"--dir={dirsource}", 
            f"{source}"
        ]
        subprocess.run (aria2_cmd)
        os.chdir (cwd)
        return 'success'

    #Si no existe bf ni fichero destino descarga inicial.
    if not os.path.exists (f'{target}.aria2') and not os.path.exists (target):
        print ('descarga inicial')
        OPTION = 'DOWNLOAD'

    # Si fichero bf           descarga anterior no completada -.
    if os.path.exists (f'{target}.aria2') and os.path.exists (target):
        print ('Continuar con Descargar inicial no terminada.')
        OPTION = 'DOWNLOAD'

    if 'DOWNLOAD' != OPTION: return 'success'

    os.chdir (dirsource)
    if 'peer' == mode:
        print ('Donwloading Torrent as peer')
        SystemLib.ogEcho(["session","log"],"warning",f'Starting PeerTorrent download in PEER mode')
        aria2_cmd = [
            "aria2c", 
            "--enable-dht=false", 
            "--enable-peer-exchange=false", 
            "--disable-ipv6" , 
            f"--seed-time={time}", 
            f"--dir={dirsource}", 
            f"{source}"
        ]

    elif 'leecher' == mode:
        print ('Donwloading Torrent as leecher')
        SystemLib.ogEcho(["session","log"],"warning",f'Starting PeerTorrent download in LEECHER mode')
        aria2_cmd = [
            "aria2c", 
            "--enable-dht=false", 
            "--enable-peer-exchange=false", 
            "--disable-ipv6" , 
            "--seed-time=0", 
            f"--dir={dirsource}", 
            f"{source}"
        ]

    elif 'seeder' == mode:
        print ('MODE seeder aria2c')
        SystemLib.ogEcho(["session","log"],"warning",f'Starting PeerTorrent download in SEEDER mode')
        aria2_cmd = [
            "aria2c", 
            "--check-integrity=true", 
            "--bt-seed-unverified=true", 
            "--disable-ipv6" , 
            "--seed-ratio=0.0", 
            f"--seed-time={time}", 
            "--enable-dht=false", 
            "--enable-peer-exchange=false", 
            f"--dir={dirsource}", f"{source}"
        ]

    else:
        print ('this should not happen')
        return None

    SystemLib.ogEcho(["session","log"],"warning",f'Launching torrent command: {aria2_cmd}')
    subprocess.run (aria2_cmd)
    os.chdir (cwd)


#/**
#         ogCreateTorrent  [ str_repo | int_ndisk int_npart ] Relative_path_file
#@brief   Función para crear el fichero torrent.
#@param   str_pathDirectory  str_Relative_path_file
#@param  int_disk    int_partition   str_Relative_path_file
#@param  str_REPOSITORY(CACHE - LOCAL)  str_Relative_path_file
#@return
#@exception OG_ERR_FORMAT    Formato incorrecto.
#@exception OG_ERR_NOTFOUND  Disco o particion no corresponden con un dispositivo.
#@exception OG_ERR_PARTITION Tipo de partición desconocido o no se puede montar.
#@exception OG_ERR_NOTOS     La partición no tiene instalado un sistema operativo.
#*/ ##

#ogCreateTorrent ([str_REPOSITORY] [int_ndisk int_npart] Relative_path_file IpBttrack)
#ogCreateTorrent (disk=1, par=1,     file='/aula1/winxp', ip_bttrack='10.1.15.23')
#ogCreateTorrent (container='REPO',  file='/aula1/winxp', ip_bttrack='10.1.15.45')
#ogCreateTorrent (container='CACHE', file='/aula1/winxp', ip_bttrack='10.1.15.45')
def ogCreateTorrent (disk=None, par=None, container=None, file=None, ip_bttrack=None):
    if file is None:
        raise TypeError ('missing required argument: "file"')
    if ip_bttrack is None:
        raise TypeError ('missing required argument: "ip_bttrack"')

    from_cache = False

    if container is not None:
        if disk is None and par is None:
            ## we were given container=
            if 'CACHE' == container: from_cache = True
            ext = ImageLib.ogGetImageType (container, file)
            if ext is None:
                SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTFOUND, f'{container} {file}')
                return
            f = f'{file}.{ext}'
            source = FileLib.ogGetPath (src=container, file=f)
        else:
            raise TypeError ('argument "container" can be specified along neither "disk" nor "par"')

    else:
        if disk is not None and par is not None:
            ## we were given disk= par=
            f = f'{file}.img'
            source = FileLib.ogGetPath (src=f'{disk} {par}', file=f)
        elif disk is None and par is None:
            ## we were given nothing
            f = f'{file}.img'
            source = FileLib.ogGetPath (file=f)
        else:
            raise TypeError ('if one of "disk" and "par" are specified, then both must be')

    if not source:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTFOUND, '')
        return

    if from_cache:
        if not CacheLib.ogFindCache():
            SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTFOUND, "CACHE")
            #return None

    if os.path.exists (f'{source}.torrent'):
        os.rename (f'{source}.torrent', f'{source}.torrent.ant')
        print ('Esperamos que se refresque el servidor')
        time.sleep (20)

    cwd = os.getcwd()
    os.chdir (os.path.dirname (source))
    print (f'mktorrent -a http://{ip_bttrack}:6969/announce -o {source}.torrent {source}')
    subprocess.run (['mktorrent' , '-a', f'http://{ip_bttrack}:6969/announce', '-o', f'{source}.torrent', source])
    os.chdir (cwd)




#/**
#         ogUpdateCacheIsNecesary  [ str_repo ] Relative_path_file_OGIMG_with_/
#@brief   Comprueba que el fichero que se desea almacenar en la cache del cliente, no esta.
#@param   1 str_REPO
#@param   2 str_Relative_path_file_OGIMG_with_/
#@param   3 md5 to check: use full to check download image torrent
#@return    True cache sin imagen, SI es necesario actualizar el fichero.
#@return    False imagen en la cache, NO es necesario actualizar el fichero
#@return    None error de sintaxis (TODO)
#@note
#@todo:      Proceso en el caso de que el fichero tenga el mismo nombre, pero su contenido sea distinto.
#@todo:      Se dejan mensajes mientras se confirma su funcionamiento.
#*/ ##
#ogUpdateCacheIsNecesary ('REPO', '/PS1_PH1.img', 'UNICAST')
#ogUpdateCacheIsNecesary ('REPO', '/ogclient.sqfs', 'FULL')
#ogUpdateCacheIsNecesary ('REPO', '/ogclient.sqfs', 'TORRENT')
def ogUpdateCacheIsNecesary (repo, file, proto):
    if not CacheLib.ogFindCache():
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTCACHE, '')
        return None

    if repo.lower() != 'repo':
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTFOUND, f' {repo} {file}')
        return None

    filesource = FileLib.ogGetPath (src=repo, file=file)
    if not filesource:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTFOUND, f' {repo} {file}')
        return None

    # paso 1. si no existe la imagen, confirmar que es necesario actualizar la cache.
    filetarget = FileLib.ogGetPath (src='CACHE', file=file)
    if not filetarget:
    # borramos el fichero bf del torrent, en el caso de que se hubiese quedado de algun proceso fallido
        if FileLib.ogGetPath (src='CACHE', file=f'/{file}.torrent.bf'): FileLib.ogDeleteFile (container='CACHE', file=f'{file}.torrent.bf')
        if FileLib.ogGetPath (src='CACHE', file=f'/{file}.sum'):        FileLib.ogDeleteFile (container='CACHE', file=f'{file}.sum')
        if FileLib.ogGetPath (src='CACHE', file=f'/{file}.full.sum'):   FileLib.ogDeleteFile (container='CACHE', file=f'{file}.full.sum')
        print ('TRUE(0), es necesario actualizar. Paso 1, la cache no contiene esa imagen ')
        return True

    # Paso 2. Comprobamos que la imagen no estuviese en un proceso previo torrent
    if FileLib.ogGetPath (file=f'{filetarget}.torrent.bf'):
    #TODO: comprobar los md5 del fichero .torrent para asegurarnos que la imagen a descarga es la misma.
        print ('TRUE(0), es necesario actualizar. Paso 2, la imagen esta en un estado de descarga torrent interrumpido')
        return True

    ## En este punto la imagen en el repo y en la cache se llaman igual,
    # paso 4. Obtener los md5 del fichero imagen en la cacha segun PROTOCOLO  $3
    if proto.lower() in ['full', 'torrent']:
        #Buscamos MD5 en el REPO SOURCE
        if os.path.exists (f'{filesource}.full.sum'):
            with open (f'{filesource}.full.sum', 'r') as fd:
                md5source = fd.read().strip()
        else:
            md5source = FileLib.ogCalculateFullChecksum (file=filesource)

        # Generamos el MD5 (full) en la CACHE
        if not os.path.exists (f'{filetarget}.full.sum'):
            fullck = FileLib.ogCalculateFullChecksum (file=filetarget)
            with open (f'{filetarget}.full.sum', 'w') as fd:
                fd.write (fullck + '\n')
        with open (f'{filetarget}.full.sum', 'r') as fd:
            md5target = fd.read().strip()
        # Generamos el MD5 (little) en la CACHE para posteriores usos del protocolo MULTICAST
        if not os.path.exists (f'{filetarget}.sum'):
            ck = FileLib.ogCalculateChecksum (file=filetarget)
            with open (f'{filetarget}.sum', 'w') as fd:
                fd.write (ck + '\n')
    else:
        #Buscamos MD5 en el REPO SOURCE
        if os.path.exists (f'{filesource}.sum'):
            with open (f'{filesource}.sum', 'r') as fd:
                md5source = fd.read().strip()
        else:
            md5source = FileLib.ogCalculateChecksum (file=filesource)

        # Generamos el MD5 (little) en la CACHE
        if not os.path.exists (f'{filetarget}.sum'):
            ck = FileLib.ogCalculateChecksum (file=filetarget)
            with open (f'{filetarget}.sum', 'w') as fd:
                fd.write (ck + '\n')
        with open (f'{filetarget}.sum', 'r') as fd:
            md5target = fd.read().strip()
        #Generamos o copiamos MD5 (full) en la CACHE para posteriores usos con Torrent
        # Si no existe el full.sum y si existe el .sum es porque el upateCACHE multicast o unicast ha sido correcto.
        if not os.path.exists (f'{filetarget}.full.sum') and os.path.exists (f'{filetarget}.sum'):
            if os.path.exists (f'{filesource}.full.sum'):
                #Existe el .full.sum en REPO realizamos COPIA
                shutil.copy2 (f'{filesource}.full.sum', f'{filetarget}.full.sum')
            else:
                #No existe .full.sum no en REPO LO GENERAMOS en la cache: situacion dificil que ocurra
                fullck = FileLib.ogCalculateFullChecksum (file=filetarget)
                with open (f'{filetarget}.full.sum', 'w') as fd:
                    fd.write (fullck + '\n')

    # Paso 5. comparar los md5
    if md5source == md5target:
        print ('FALSE (1), No es neceario actualizar. Paso5.A la imagen esta en cache')
        return False
    else:
        print ('imagen en cache distinta,  borramos la imagen anterior')
        for f in [f'{filetarget}', f'{filetarget}.sum', f'{filetarget}.torrent', f'{filetarget}.full.sum']:
            if os.path.exists (f):
                os.unlink (f)
        print ('TRUE (0), Si es necesario actualizar.')
        return True
