#!/usr/bin/python

'''
 The aim of this script is to update the controller with new images on the other bank (rootfs, kernel and dtb)

 The memory layout must contains all UBI volume (similar to logical disk partitions) doubled to support dual bank: rootfs-0, rootfs-1, kernel-0, kernel-1, dtb-0, dtb-1

 It :
   - sets leds in update mode (amber blinking)
   - does some verifications (checksum, UBI volume available for update)
   - updates rootfs, kernel and dtb volumes
   - duplicate appfs volume. This way if the appfs is updated by the new rootfs, it is still possible to retrieve the old version in case of rollback
   - correct the rootfs volume to mount the right appfs at startup
   - update all connected RLs
   - update the bootable_partition (indicate the bank to boot) for next reboot
   - set leds on update finished mode
   - reboot

 In case of error detected, the leds are set in error mode for 15 seconds and then we reboot.
'''

import sys
import os
import logging
import logging.handlers
from logging.handlers import SysLogHandler
import subprocess
from subprocess import CalledProcessError
import socket
import ctypes
import time
import glob
import lib_wifi

# Below libraries must be located in the same folder where this script is executed from, and correctly imported
# update_user.py can't rely on the libraries located in "/home/root/SeaCloudScripts" as they can be outdated, or even missing

try:
    import lib_logger
    import lib_uboot
    import lib_UBI
    import lib_system
    import lib_RL78
    import lib_localUI
    import_succeeded = True
except ImportError as e:
    import_succeeded = False

logger = logging.getLogger()
logger.info("import_succeeded={}".format(import_succeeded))

update_base_folder_name = os.getcwd()
update_folder_name = "update"
update_type_script_name = "whichupdateami.sh"
update_script_rl78 = "RL78Updater.py"

# Around 15K per update - here we can keep 2*100K -> ~18 updates
FILE_LOG_MAX_SIZE   = 100*1024
FILE_LOG_COUNT      = 1
FILE_LOG_NAME       = "last_update.log"

KERNEL_BASE_VOLUME_NAME = "kernel"
DTB_BASE_VOLUME_NAME = "dtb"
ROOTFS_BASE_VOLUME_NAME = "rootfs"
APPFS_BASE_VOLUME_NAME = "appfs"

# Folder that contains all artifacts
ARTIFACT_FOLDER = "colibri_vf"

ROOTFS_MOUNT_FOLDER = "/tmp/rootfs"
APPFS_MOUNT_FOLDER = "/tmp/appfs"
ROOTFS_BACKUP_FOLDER = "backup"
CURRENT_APPFS_FOLDER = "/mnt/fcc"
CURRENT_APPFS_ENCRYPTED_FOLDER = "/mnt/fcc/encrypted"
CONNMAN_CONFIG_FOLDER = "/var/lib/connman"

# Magic paths for rootfs/appfs customization
FORCEAMQP_PATH = "patch/amqp.json"
VPNOFF_PATH = "patch/vpnoff"
FORCE_NEBULA_PATH = "patch/nebula.properties"

# Enum not used to avoid new dependency
UI_MODE_OFF = 0
UI_MODE_UPDATE = 1
UI_MODE_REBOOT = 2
UI_MODE_ERROR = 3

localUI_wrapper = lib_localUI.init_local_UI()
if localUI_wrapper is None:
    error_in_import()

######################
# Controller actions #
######################
def set_hold_mode( hold_enable ):
    '''
    Set the sensors in hold
    @hold_enable: is True to enable hold
    '''
    logger.warning("TODO: set_hold_mode to link to LocalUIwrapper")


####################
# Error management #
####################
def exit_and_reboot_on_error(message, error_code=1):
    '''
    Exit current application with an error code, message and UI update
    @message: message to be output
    @error_code: error_code return to caller
    '''
    lib_localUI.set_UI_mode(lib_localUI.UI_MODE_ERROR)
    logger.error(message)

    # Wait with error UI before going back to working mode
    time.sleep(15)

    lib_localUI.set_UI_mode(lib_localUI.UI_MODE_REBOOT)

    # Wait the local UI to be updated before rebooting
    time.sleep(1)
    logger.error("Application exited")

    update_type = lib_system.get_update_type()
    if update_type == "mcb":
        logger.info("sync 1")
        lib_system.sync()
        logger.info("sync 2")
        lib_system.sync()
        logger.info("/bin/systemctl reboot --force")
        logging.shutdown()
        os.system("/bin/systemctl reboot --force")
    else:
        logging.shutdown()
        os.system("reboot")

    # should not be called
    sys.exit(error_code)


##############
# UBI chroot #
##############
class UbiVolumeChroot:
    """ A class that manage a chroot of an UBI volume """

    def __init__(self, rootfs_ubi_volume_dev_file, chroot_location):
        '''
        @rootfs_ubi_volume_dev_file: device file of the ubi volume that contain rootfs (e.g.:/dev/ubi0_6)
        @chroot_location: location where the chroot will be created
        '''
        self.ubi_volume_dev_file = rootfs_ubi_volume_dev_file
        self.location = chroot_location

    def create(self):
        '''
        Create the chroot
        '''
        lib_system.call_shell(["/bin/sh","-c", "mkdir -p " + self.location + " && mount -t ubifs " + self.ubi_volume_dev_file + " " + self.location])
        lib_system.call_shell(["/bin/sh","-c", "mount --bind /dev " + self.location + "/dev"])
        lib_system.call_shell(["/bin/sh","-c", "mount --bind /sys " + self.location + "/sys"])
        lib_system.call_shell(["/bin/sh","-c", "mount --bind /proc " + self.location + "/proc"])
        self.execute("ldconfig")

    def destroy(self, try_to=False):
        '''
        Destroy the chroot
        @try_to: If True, an error does not generated exception
        '''
        command_postfix = ""
        if try_to:
            # Disable outputs and error value
            command_postfix = " 2>&1 >/dev/null ; true"

        lib_system.call_shell(["/bin/sh","-c", "umount " + self.location + "/dev" + command_postfix])
        lib_system.call_shell(["/bin/sh","-c", "umount " + self.location + "/sys" + command_postfix])
        lib_system.call_shell(["/bin/sh","-c", "umount " + self.location + "/proc" + command_postfix])
        lib_system.call_shell(["/bin/sh","-c", "umount " + self.location + command_postfix])
        lib_system.call_shell(["/bin/sh","-c", "rm -rf " + self.location + command_postfix])

    def execute(self, command, check_error_code=True):
        '''
        Execute a given command in the chroot
        @command: shell command to execute
        '''
        return lib_system.call_shell(["/bin/sh","-c", "chroot " + self.location + " " + command], check_error_code)

    def copy_to(self, source, destination):
        '''
        Copy file into chroot
        @source: path of the file to copy
        @destination: path of the destination folder
        '''
        lib_system.call_shell(["/bin/sh","-c", "mkdir -p " + self.location + "/" + destination])
        lib_system.call_shell(["/bin/sh","-c", "cp -a " + source + " " + self.location + "/" + destination + "/" ])

#################
# Rootfs chroot #
#################
def prepare_rootfs_chroot(chroot):
    '''
    Prepare a chroot for of the new rootfs image
    @chroot: chroot object
    '''
    try:
        chroot.create()
        logger.info("rootfs chroot created")
    except CalledProcessError as e:
        exit_and_reboot_on_error("Unable to create the rootfs chroot.")

def update_appfs_volume_in_rootfs(chroot, appfs_index):
    '''
    Update the appfs volume used by rootfs
    @appfs_index: index of the appfs to use as an int (e.g.:0, 1)
    '''
    try:
        chroot.execute("sed -i \"s/appfs-0/appfs-" + str(appfs_index) + "/g\" /etc/fstab")
        logger.info("rootfs update to use new appfs")
    except CalledProcessError as e:
        exit_and_reboot_on_error("Unable to update rootfs to use new appfs.")

def update_hostname_in_rootfs(chroot, hostname):
    '''
    Update the hostname used by rootfs
    @hostname: hostname to set
    '''
    try:
        chroot.execute("/bin/sh -c \"echo " + hostname + " > /etc/hostname\"")
        chroot.execute("/bin/sh -c \"echo '127.0.0.1       localhost' > /etc/hosts\"")
        chroot.execute("/bin/sh -c \"echo '127.0.0.1       " + hostname + "' >> /etc/hosts\"")
        logger.info("hostname of rootfs updated")
    except CalledProcessError as e:
        exit_and_reboot_on_error("Unable to update hostname in new rootfs.")

def destroy_rootfs_volume(chroot, try_to=False):
    '''
    Umount the rootfs volume from /tmp/rootfs folder
    @try_to: If True, an error does not generated exception
    '''
    try:
        chroot.destroy(try_to)
        logger.info("Removal of rootfs chroot done")
    except CalledProcessError as e:
        if not try_to:
            exit_and_reboot_on_error("Unable to remove the rootfs chroot.")


############
# Main app #
############
def display_versions():
    '''
    Display the versions of the package to install
    '''
    logger.info("Will install the following package:")
    VERSION_FILE='Versions.txt'
    if os.path.exists(VERSION_FILE):
        file = open(VERSION_FILE, "r")
        for line in file:
            line = line.rstrip()
            if line:
                logger.info(line)
    else:
        logger.info("No version found")

def check_UBI_volumes_exists(bank_index):
    '''
    Check if needed UBI volumes are present on the system for a given bank
    @bank_index: index of the bank to test append to expected volume names (0 or 1)
    return True on success, False otherwise
    '''
    succeed = True
    base_volume_names = [KERNEL_BASE_VOLUME_NAME, DTB_BASE_VOLUME_NAME, ROOTFS_BASE_VOLUME_NAME, APPFS_BASE_VOLUME_NAME]
    for base_volume_name in base_volume_names:
        if lib_UBI.ubi_volume_exists("{}-{}".format(base_volume_name,bank_index)):
            logger.info("UBI volume {} found on UBI partition {}".format(base_volume_name, bank_index))
        else:
            logger.error("ubi{}:{} volume missing".format(partition_index,base_volume_name))
            succeed = False
    return succeed


def is_encrypted(folder):
    '''
    Check if a folder is encrypted (type ecryptfs)
    @folder: name of the folder to check
    return True if the folder exists and it is encrypted, False otherwise
    '''
    (unencrypted, output_) = lib_system.call_shell(["/bin/sh","-c", "mount | grep '" + folder + " type ecryptfs'"], False)
    return not unencrypted


def duplicated_appfs_volume(volume_name):
    '''
    Duplicate the current appfs into the new one to be able to revert to previous appfs version in case of update rollback
    Mount the appfs volume in a temporary location, clean it and copy in it the current /mnt/fcc folder.
    If the current /mnt/fcc/encrypted folder is already encrypted, encrypt before the corresponding /encrypted folder of the temporary location where the appfs volume is mounted
    @volume_name: name of the UBI volume to update
    '''
    try:
        lib_system.call_shell(["/bin/sh","-c", "mkdir -p " + APPFS_MOUNT_FOLDER + " && mount -t ubifs ubi0:" + volume_name + " " + APPFS_MOUNT_FOLDER])
        if is_encrypted(CURRENT_APPFS_ENCRYPTED_FOLDER):
            lib_system.call_shell(["/bin/sh","-c", "cd " + APPFS_MOUNT_FOLDER + " && rm -rf * && mkdir -p " + APPFS_MOUNT_FOLDER + "/encrypted && /etc/ecryptfs-mount.sh " + APPFS_MOUNT_FOLDER + "/encrypted"])
            lib_system.call_shell(["/bin/sh","-c", "cd " + APPFS_MOUNT_FOLDER + " && cp -a " + CURRENT_APPFS_FOLDER + "/* . && cd - && umount " + APPFS_MOUNT_FOLDER + "/encrypted && umount " + APPFS_MOUNT_FOLDER + " && rm -rf " + APPFS_MOUNT_FOLDER])
        else:
            lib_system.call_shell(["/bin/sh","-c", "cd " + APPFS_MOUNT_FOLDER + " && rm -rf * && cp -a " + CURRENT_APPFS_FOLDER + "/* . && cd - && umount " + APPFS_MOUNT_FOLDER + " && rm -rf " + APPFS_MOUNT_FOLDER])
        return True
    except CalledProcessError as e:
        return False

def synchronize_updatelogs_of_appfs_volume(volume_name):
    '''
    Synchronize the update logs of current appfs into the new one
    Mount the appfs volume in a temporary location and copy in it the current logs of /mnt/fcc folder
    @volume_name: name of the UBI volume to update
    '''
    try:
        lib_system.call_shell(["/bin/sh","-c", "mkdir -p " + APPFS_MOUNT_FOLDER + " && mount -t ubifs ubi0:" + volume_name + " " + APPFS_MOUNT_FOLDER])
        lib_system.call_shell(["/bin/sh","-c", "cd " + APPFS_MOUNT_FOLDER + " && rm -rf " + FILE_LOG_NAME + "* && cp -a " + CURRENT_APPFS_FOLDER + "/" + FILE_LOG_NAME + "* . && cd - && umount " + APPFS_MOUNT_FOLDER + " && rm -rf " + APPFS_MOUNT_FOLDER])
        return True
    except CalledProcessError as e:
        return False

def umount_appfs():
    '''
    Umount previous appfs folder if mounted
    '''
    lib_system.call_shell(["/bin/sh","-c", "umount " + APPFS_MOUNT_FOLDER + " > /dev/null"], False)

def copy_to_new_rootfs(folder, files):
    '''
    Copy files from current rootfs to new rootfs
    @folder where files are located in current rootfs and where they will be stored in new rootfs
    @files to be copied (shell wildcard supported)
    '''
    lib_system.call_shell(["/bin/sh","-c", "mkdir -p " + ROOTFS_MOUNT_FOLDER + folder])
    lib_system.call_shell(["/bin/sh","-c", "cp -a " + os.path.join(folder, files) + " " + ROOTFS_MOUNT_FOLDER + folder])


def do_update(reboot_after_update):
    '''
    Do the update of the group of volume not currently used
    '''
    lib_logger.log_header("Controller update started")
    lib_localUI.set_UI_mode(lib_localUI.UI_MODE_UPDATE)

    display_versions()

    try:
        bootable_partition_string = lib_uboot.get_uboot_value("bootable_partition")
    except CalledProcessError as e:
        # Variable not found
        exit_and_reboot_on_error("bootable_partition not found in u-boot environment variables")

    current_bootable_partition_index = int (bootable_partition_string)
    new_bootable_partition_index  = (current_bootable_partition_index + 1) % 2
    logger.info("Current bootable partition index is {}".format(current_bootable_partition_index))
    logger.info("New bootable partition index is {}".format(new_bootable_partition_index))

    lib_logger.log_header("Check that all needed UBI volumes exist ...")
    if check_UBI_volumes_exists (new_bootable_partition_index):
        logger.info("UBI volumes OK")
    else:
        exit_and_reboot_on_error("One or more UBI volumes are missing. Check the UBI layout.")

    lib_logger.log_header("Remove rootfs chroot if any from a previous failed install ...")
    rootfs_device = "/dev/" + lib_UBI.get_device_node_from_volume_name("{}-{}".format(ROOTFS_BASE_VOLUME_NAME,new_bootable_partition_index))
    chroot = UbiVolumeChroot(rootfs_device, ROOTFS_MOUNT_FOLDER)
    destroy_rootfs_volume(chroot, try_to=True)

    lib_logger.log_header("Remove appfs mount if any from a previous failed install ...")
    umount_appfs()

    fdt_filename = lib_uboot.get_fdt_filename()
    volumes_to_update = [
    {'name' : "rootfs", 'base_volume_name' : ROOTFS_BASE_VOLUME_NAME, 'filename' : "ubifs.img"},
    {'name' : "kernel", 'base_volume_name' : KERNEL_BASE_VOLUME_NAME, 'filename' : "zImage"},
    {'name' : "device-tree", 'base_volume_name' : DTB_BASE_VOLUME_NAME, 'filename' : fdt_filename}
    ]

    # If appfs is not in UBIFS, the volume is updated with appfs.img
    lib_logger.log_header("Update appfs with appfs.img if it is not in UBIFS ...")
    if not lib_UBI.is_volume_using_ubifs( "{}-{}".format(APPFS_BASE_VOLUME_NAME,new_bootable_partition_index) ):
        logger.info("Force appfs volume update")
        volumes_to_update.append ( {'name' : "appfs", 'base_volume_name' : APPFS_BASE_VOLUME_NAME, 'filename' : "appfs.img"} )

    for volume in volumes_to_update:
        lib_logger.log_header("Update {} ...".format(volume['name']))
        device = lib_UBI.get_device_node_from_volume_name("{}-{}".format(volume['base_volume_name'],new_bootable_partition_index))
        if lib_UBI.update_ubi_volume(device, "{}/{}".format(ARTIFACT_FOLDER, volume['filename'])):
            logger.info("{} volume updated".format(volume['name']))
        else:
            exit_and_reboot_on_error("Fail to update {} volume .".format(volume['name']))

    new_appfs_volume_name="{}-{}".format(APPFS_BASE_VOLUME_NAME,new_bootable_partition_index)

    lib_logger.log_header("Duplicate current appfs ...")
    if duplicated_appfs_volume ( new_appfs_volume_name ):
        logger.info("appfs duplicated OK")
    else:
        exit_and_reboot_on_error("Unable to duplicate appfs.")

    lib_logger.log_header("Prepare rootfs chroot ...")
    prepare_rootfs_chroot(chroot)

    # If fec1 Ethernet configuration file exists, copy it to the new rootfs
    lib_logger.log_header("Ethernet configuration")
    if os.path.exists("/etc/systemd/network/fec1.network"):
        copy_to_new_rootfs("/etc/systemd/network", "fec1.network")

    # If the current rootfs is still using a real "/var/lib/connman" instead of a symlink, we need to copy all connman configuration files to the new rootfs, and
    # the script appfs_update.sh will move those settings to the new connman configuration folder (/mnt/fcc/connman) on next reboot.
    if not os.path.islink(CONNMAN_CONFIG_FOLDER):
        # If fec0 Ethernet configuration file exists, copy it to the new rootfs
        if os.path.exists(CONNMAN_CONFIG_FOLDER + "/fec0.config"):
            copy_to_new_rootfs(CONNMAN_CONFIG_FOLDER, "fec0.config")

        # If Internet Sharing option, contained in connmnan settings file exists, copy it to the new rootfs
        lib_logger.log_header("Internet Sharing configuration")
        if os.path.exists(CONNMAN_CONFIG_FOLDER + "/settings"):
            copy_to_new_rootfs(CONNMAN_CONFIG_FOLDER, "settings")

        # If WiFi configuration files exist, copy them to the new rootfs
        lib_logger.log_header("WiFi configuration")
        if glob.glob("/var/lib/connman/wifi_*/"):
            copy_to_new_rootfs(CONNMAN_CONFIG_FOLDER, "wifi_*")
    else:
        # Copy the symlink to the new rootfs
        copy_to_new_rootfs("/var/lib", "connman")

    #######
    # VPN
    #######
    # disable the VPN if it was disabled or if vpnoff file is present on USB stick root, otherwise enable the VPN
    lib_logger.log_header("VPN")
    disable_vpn=False

    # On old system, VPN is enabled by openvpn.service
    # On new system, VPN is enabled by openvpn.path and openvpn.service is always disabled
    ret, output = lib_system.call_shell(["/bin/sh","-c", "systemctl is-enabled openvpn.path 2>/dev/null || systemctl is-enabled openvpn.service"], False)
    if ret==1:
        logger.info("VPN was disabled on the device")
        disable_vpn=True

    if os.path.exists(VPNOFF_PATH):
        logger.info("vpnoff file found on USB stick root")
        disable_vpn=True

    if disable_vpn:
        logger.info("Disable VPN on new rootfs")
        chroot.execute("systemctl disable openvpn.path")
    else:
        logger.info("Enable VPN on new rootfs")
        chroot.execute("systemctl enable openvpn.path")

    appfs_customization_needed = os.path.exists(FORCEAMQP_PATH) or os.path.exists(FORCE_NEBULA_PATH)
    if appfs_customization_needed:
        lib_logger.log_header("Appfs customization needed")
        try:
            # mount new appfs
            lib_system.call_shell(["/bin/sh","-c", "mkdir -p " + APPFS_MOUNT_FOLDER + " && mount -t ubifs ubi0:" + new_appfs_volume_name + " " + APPFS_MOUNT_FOLDER])
            # encrypt new appfs if currernt appfs is already encrypted
            if is_encrypted(CURRENT_APPFS_ENCRYPTED_FOLDER):
                lib_system.call_shell(["/bin/sh","-c", "/etc/ecryptfs-mount.sh " + APPFS_MOUNT_FOLDER + "/encrypted"])

            if os.path.exists(FORCE_NEBULA_PATH):
                lib_logger.log_header("Nebula properties")
                # Update the nebula.properties link to point on the content of the file FORCE_NEBULA_PATH
                lib_system.call_shell(["/bin/sh","-c", "cp -a " + APPFS_MOUNT_FOLDER + "/set/nebula.properties " + APPFS_MOUNT_FOLDER + "/set/nebula.properties.bak && ln -sfn $(cat " + FORCE_NEBULA_PATH + ") " + APPFS_MOUNT_FOLDER + "/set/nebula.properties"])

                logger.info("Clean files generated by NebulaClient (amqp.json and VPN files)")
                lib_system.call_shell(["/bin/sh","-c", "rm -rf " + APPFS_MOUNT_FOLDER + "/{set/amqp.json,etc/keys/openvpn.cfg,etc/keys/*.crt,etc/keys/*.key}"])

            if os.path.exists(FORCEAMQP_PATH):
                lib_logger.log_header("AMQP")
                # Copy the amqp.json file (if present) on the USB stick in the new appfs
                lib_system.call_shell(["/bin/sh","-c", "[ -e " + APPFS_MOUNT_FOLDER + "/set/amqp.json ] && cp -a " + APPFS_MOUNT_FOLDER + "/set/amqp.json " + APPFS_MOUNT_FOLDER + "/set/amqp.json.bak ; cp " + FORCEAMQP_PATH + " " + APPFS_MOUNT_FOLDER + "/set"])

        except CalledProcessError as e:
            logger.error("Error in call_shell")
        finally:
            # umount new appfs
            if is_encrypted(CURRENT_APPFS_ENCRYPTED_FOLDER):
                lib_system.call_shell(["/bin/sh","-c", "umount " + APPFS_MOUNT_FOLDER + "/encrypted && umount " + APPFS_MOUNT_FOLDER + " && rm -rf " + APPFS_MOUNT_FOLDER + ""])
            else:
                lib_system.call_shell(["/bin/sh","-c", "umount " + APPFS_MOUNT_FOLDER + " && rm -rf " + APPFS_MOUNT_FOLDER + ""])

    lib_logger.log_header("Update rootfs to point on new appfs ...")
    update_appfs_volume_in_rootfs(chroot, new_bootable_partition_index )

    lib_logger.log_header("Update hostname of rootfs ...")

    update_type = lib_system.get_update_type()
    if update_type == "mcb":
        # set MCB style hostname to socket with serial number previously read
        ret,serno = lib_system.call_shell(["/bin/sh","-c", "cat /mnt/fcc/set/ControllerSerial.txt"])
        logger.info("ret = {}, read serial number {} from /mnt/fcc/set/ControllerSerial.txt".format(ret,serno))
        serno = serno.rjust(12, '0')
        mcb_fid = "HL001_17389_" + serno
        logger.info("generated fid {} from serno {}".format(mcb_fid,serno))
        update_hostname_in_rootfs(chroot, mcb_fid)
    else:
        update_hostname_in_rootfs(chroot, socket.gethostname())

    ##############
    # RL78 update
    ##############
    lib_logger.log_header("Update the firmware of connected RL78 ...")

    (return_code, output) = lib_system.call_shell(["/bin/sh","-c","./RL78Updater.py"])
    if (return_code == 0):
        logger.info("RL78 update succesfull")
    else:
        exit_and_reboot_on_error("Impossible to update RL78s")


    ###########################
    # new partition selection
    ###########################
    lib_logger.log_header("Remove the rootfs chroot ...")
    destroy_rootfs_volume(chroot)

    lib_logger.log_header("Select new partition for new reboot ...")
    if lib_uboot.set_uboot_value("bootable_partition",str(new_bootable_partition_index)):
        logger.info("New partition selected OK")
    else:
        exit_and_reboot_on_error("Unable to change the boot partition.")

    # Update uboot variables to put it in the update process
    if lib_uboot.set_uboot_value("updateinprogress",'1') and lib_uboot.set_uboot_value("boottriesremaining",'4'):
        logger.info("Set uboot variable updateinprogress to 1 and boottriesremaining")
    else:
        exit_and_reboot_on_error("Unable to set uboot variables updateinprogress and/or boottriesremaining.")


    ##########
    # Reboot
    ##########

    # switch off the LEDs
    lib_localUI.set_UI_mode(lib_localUI.UI_MODE_REBOOT)

    if reboot_after_update:
        logger.info("Controller updated")
        lib_logger.log_header("Reboot ...")

        lib_logger.log_header("Ensure the update logs are up-to-date in the new appfs ...")
        if synchronize_updatelogs_of_appfs_volume ( new_appfs_volume_name ):
            logger.info("Update logs synchronized")
        else:
            exit_and_reboot_on_error("Unable to synchronized update logs in new appfs.")

        update_type = lib_system.get_update_type()
        if update_type == "mcb":
            logger.info("sync 1")
            lib_system.sync()
            logger.info("sync 2")
            lib_system.sync()
            logger.info("/bin/systemctl reboot --force")
            logging.shutdown()
            os.system("/bin/systemctl reboot --force")
        else:
            logging.shutdown()
            os.system("reboot")


def error_in_import():
    '''
    Manage error when importing libs
    '''

    try:
        # Configure the root logger
        # This function is used only if the library lib_logger can not be imported, so that the issue is reported in the logger anyway
        # Normally, the functions lib_logger.add_handler... are used
        logger = logging.getLogger()
        logger.setLevel(logging.DEBUG)

        # Aug 17 12:59:21 HL001_17386_000000000105 test.py: test logs ||| <module> in test.py:30
        formatter = logging.Formatter('%(asctime)s ' + socket.gethostname() + ' %(filename)s: %(message)s ||| %(funcName)s in %(filename)s:%(lineno)d','%b %d %H:%M:%S')
        # Use UTC time for logger
        formatter.converter = time.gmtime

        # Log to stdout
        console_handler = logging.StreamHandler(sys.stdout)
        console_handler.setFormatter(formatter)
        logger.addHandler(console_handler)

        # Log into rotating file in appfs
        file_handler = logging.handlers.RotatingFileHandler(filename=FILE_LOG_NAME, maxBytes=FILE_LOG_MAX_SIZE, backupCount=FILE_LOG_COUNT)
        file_handler.setFormatter(formatter)
        logger.addHandler(file_handler)

        # Log to syslog
        syslog_handler = logging.handlers.SysLogHandler(address='/dev/log')
        syslog_handler.setFormatter(formatter)
        logger.addHandler(syslog_handler)

        logger = logging.getLogger(__name__)
        exit_and_reboot_on_error("Error importing some of these libraries: lib_logger, lib_uboot, lib_UBI, lib_system. They must be located in: " + os.getcwd())
    except OSError as e:
        logger.error("Error importing some of these libraries: lib_logger, lib_uboot, lib_UBI, lib_system. They must be located in: " + os.getcwd())
        logging.shutdown()
        sys.exit(error_code)


if __name__ == '__main__':
    # log and exit if an error was produced during the import of the non-built in libraries
    if not import_succeeded:
        error_in_import()

    # read no_reboot option
    reboot_after_update = True
    if (len(sys.argv) > 1):
        reboot_after_update = sys.argv[1] != "no_reboot"

    tag_name = "update_user"
    lib_logger.add_handler_stdout(tag_name)
    lib_logger.add_handler_syslog(tag_name)
    lib_logger.add_handler_file(tag_name, CURRENT_APPFS_FOLDER + "/" + FILE_LOG_NAME,FILE_LOG_MAX_SIZE,FILE_LOG_COUNT)

    logger.info ("reboot_after_update is set to {}".format(reboot_after_update))

    # ensure executability for update type detection script and RL78 updater script
    sys.path.append("{0}".format(update_base_folder_name))
    logger.info("Setting executive rights for {0} and {1}".format(update_type_script_name,update_script_rl78))
    os.system("cd {0} && chmod +x {1}".format(update_base_folder_name, update_type_script_name))
    os.system("cd {0} && chmod +x {1}".format(update_base_folder_name, update_script_rl78))

    # Stop HW watchdog at the beginning of the script. This is a workaround to upgrade from previous versions older than 2019.2,
    # where watchdog timeout was set to 60s for ExecDaemonII. Can be removed when no more old version will exist on the field.
    lib_system.call_shell(["/bin/sh","-c", "/home/root/SeaCloudScripts/config_watchdog_hw.sh disable"], False)

    try:
        # Check if an update is already in progress. If it is the case, abort the update
        try:
            update_in_progress_string = lib_uboot.get_uboot_value("updateinprogress")
        except CalledProcessError as e:
            # Variable not found - temporary workaround to update devices with u-boot of TestFest 0.08.03 version that did not include the full recovery mechanism.
            logger.warning("updateinprogress not found in u-boot environment variables, automatically initialized to 0")
            if lib_uboot.set_uboot_value("updateinprogress",'0'):
                update_in_progress_string = 0
            else:
                exit_and_reboot_on_error("Unable to set uboot variables updateinprogress")

        if update_in_progress_string == "1":
            # Exit the update procedure
            exit_and_reboot_on_error("An update is already running")

        # Execute the appfs garbage collector to ensure enough free memory space during the update process, if it exists
        appfs_gc_service = "appfs_garbage_collector.service"
        appfs_gc_service_installed, _ = lib_system.isServiceInstalled(appfs_gc_service)
        if appfs_gc_service_installed:
            logger.info("Execute appfs garbage collector before update")
            lib_system.startService(appfs_gc_service)

        do_update(reboot_after_update)

    except Exception:
        logger.exception("Unexpected error during update.")
        exit_and_reboot_on_error("Unexpected error during update.")
