#!/usr/bin/python3
import argparse
import logging
import os
import json
import shutil
import subprocess
import time
import datetime
import signal
import sys

PACKAGE_VERSION = "0.1.5-3.vz7"
PREFIX = "C2VM_"

conffile="/etc/vz/c2v.conf"
tempdir="/vz/tmp/c2v"
cepfile="/vz/tmp/cep/output/preliminary/c2v.json"
cep=True
logdir=""
args=None
current_ct_config={}

CT=-1
CT_count=1
ST=-1
ST_count=11

class CTLogFactory():
    def __init__(self, original_factory):
        self.original_factory = original_factory

    def __call__(self, *args, **kwargs):
        record = self.original_factory(*args, **kwargs)
        if CT < 1:
            record.ct=""
        else:
            record.ct = " Container [%s/%s]" % (CT,CT_count)
        if ST < 1:
            record.st=""
        else:
            record.st = " Stage [%s/%s]" % (ST,ST_count)
        return record

logging.setLogRecordFactory(CTLogFactory(logging.getLogRecordFactory()))

logger = logging.getLogger("c2v-convert")
log_format="%(asctime)s %(levelname)s%(ct)s%(st)s: %(message)s"
log_formater=logging.Formatter(log_format)
ct_log_format="%(asctime)s %(levelname)s%(st)s: %(message)s"
ct_log_formater=logging.Formatter(ct_log_format)

def config_update(config, param_name, value):
    with open(config, 'r+') as f:
        data = json.load(f)
        data[param_name] = value
        f.seek(0)
        json.dump(data, f, indent=4)
        f.truncate() 

def config_get(config):
    with open(config, 'r') as f:
        data = json.load(f)
        return data
        
def prompt(text):
    failed=0
    while failed<3:
        try:
            answer=input(text)
            if answer.lower() in ["y","yes"]:
                return True
            elif answer.lower() in ["n","no"]:
                return False
            else:
                print("Bad entry. Please answer yes or no. Your answer is: %s" % answer)
                logger.warning("Bad entry. Please answer yes or no. Your answer is: %s" % answer)
        except:
            print("Bad entry. Please answer yes or no.")
            logger.warning("Bad entry. Please answer yes or no.")
        failed+=1
    print("Failed to receive answer. Cancelling.")
    logger.error("Failed to receive answer. Cancelling.")
    return False

def cep_prompt():
    failed=0
    text = "Would you like to share anonymous statistics about this host, " \
            "this and future conversions? It will help Virtuozzo to improve " \
            "our products. No personal data will be collected, details: " \
            "https://docs.virtuozzo.com/virtuozzo_hybrid_server_7_users_guide/advanced-tasks/participating-in-customer-experience-program.html" \
            ": Yes(y); Ask me later(n); No, never ask me again(N)"
    print(text)
    while failed<3:
        try:
            answer=input()
            if answer.lower() in ["y","yes"]:
                cep_start()  #Enable cep
                return True
            elif answer in ["n","no"]:
                return False #Continues without saving
            elif answer.lower() in ["n","no"]:
                config_update(conffile, "cep", "never")
                return False #Save answer
            else:
                print("Bad entry. Please answer y, n or N. Your answer is: %s" % answer)
                logger.warning("Bad entry. Please answer y, n or N. Your answer is: %s" % answer)
        except:
            print("Bad entry. Please answer y, n or N.")
            logger.warning("Bad entry. Please answer y, n or N.")
        failed+=1
    logger.error("Failed to receive answer. Cancelling.")
    return False

def cep_running():
    result=cmd_run(["systemctl", "status", "disp-helper"], check=False)
    return "active (running)" in result.stdout

def cep_start():
    cmd_run(["systemctl", "enable", "disp-helper"])
    cmd_run(["systemctl", "start", "disp-helper"])

def timeout_handler(signum, frame):
    logger.error("%s conversion timed out." % args.ct)
    raise subprocess.TimeoutExpired("Timeout exceeded %s" % args.timeout, args.timeout)

def cmd_run(command, check=True, messages={}, timeout=None, input=None, env=None):
    default={
        'start':'Command "%s" started.' % (" ".join(command)),
        'finish':'Command "%s" finished.' % (" ".join(command)),
        'debug':'Command "%s" output:\n' % (" ".join(command)),
        'timeout':'Command "%s" timed out. ' % (" ".join(command)),
        'except':'Command "%s" failed. ' % (" ".join(command)),
        'failed':'Command "%s" failed: ' % (" ".join(command)),
    }

    # for message in default:
    #     if message not in messages:
    #         messages[message]=default[message]
    messages=default
    checklist=[0]
    if type(check)==list:
        checklist=check
        check=True
    if messages['start']:
        logger.debug(messages['start'])
    try:
        result=subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,check=False, input=input, universal_newlines=True, timeout=timeout, env=env)
        logger.debug(messages['debug']+result.stdout)
        if not result.returncode or result.returncode in checklist:
            logger.debug(" %s returncode %s"%(" ".join(command),result.returncode))
        elif check:
            logger.error(messages['failed']+" %s returncode %s\n%s"%(" ".join(command),result.returncode,result.stderr))
            raise subprocess.CalledProcessError(result.returncode, command)
        else:
            logger.warning(messages['failed']+" %s returncode %s\n%s"%(" ".join(command),result.returncode,result.stderr))
        if messages['finish']:
            logger.debug(messages['finish'])
        return result
    except subprocess.TimeoutExpired:
        if messages['timeout']:
            logger.error(messages['timeout'])
        raise
    except subprocess.CalledProcessError as e:
        logger.error(messages['failed']+" %s returncode %s\n%s"%(" ".join(command),e.returncode,e.stderr))
        raise
    except UnicodeDecodeError as e:
        logger.warning("Unable to parse command output: %s" % (e.reason))
        pass
    except Exception as e:
        import traceback
        if messages['except']:
            logger.error(messages['except'])
        logger.error(''.join(traceback.TracebackException.from_exception(e).format()))
        raise


def start_ct(ct):
    logger.debug("CT %s sent start signal" % ct)
    try:
        result=cmd_run(["prlctl", "status", ct],False)
        if 'exist running' not in result.stdout:
            result=cmd_run(["prlctl", "start", ct, "--wait"], check=False, timeout=60)
            time.sleep(5)
        logger.debug(("CT start request: \n" + result.stdout).replace("\n","\n\t\t"))
    except subprocess.TimeoutExpired:
        cmd_run(["prlctl", "exec", ct, "touch", "/.vzfifo"])
        time.sleep(5)
        result=cmd_run(["prlctl", "status", ct],False)
        if 'exist running' not in result.stdout:
            logger.error("Failed to get %s to the started state" % ct)    
            raise ValueError("Failed to start %s ina timely manner" % ct)
    except:
        logger.error("Failed to get %s to the started state" % ct)
        raise ValueError("Failed to detect status of %s" % ct)
    logger.debug("CT %s started" % ct)

    


def get_ct_state(ct):
    import re
    logger.debug("Checking CT %s status" % ct)
    try:
        result=cmd_run(["prlctl", "status", ct])
        resout=result.stdout
        return re.search("exist ([a-z]{1,})",resout.strip())[0].split()[1]
    except ValueError:
        raise
    except:
        logger.error("Failed to get %s state" % ct)
        raise ValueError("Failed to detect status of %s" % ct)
    return False
    
    

def stop_venv(ct):
    logger.debug("VEnv %s sent stop signal" % ct)
    try:
        attempt=0
        while True:
            result=cmd_run(["prlctl", "status", ct])
            if 'exist stopped' in result.stdout:
                break
            else:
                attempt+=1
                if attempt>=10:
                    logger.error("Unable to get %s to the stopped state" % ct)
                    raise ValueError("Unable to get %s to the stopped state" % ct)
                cmd_run(["prlctl", "stop", ct])
                logger.debug("Waiting %s to achieve stopped state" % ct)
                time.sleep(15)
        
        logger.debug(("CT stop request: \n" + result.stdout).replace("\n","\n\t\t"))
    except:
        logger.warning("Failed to get %s to the stopped state gracefully" % ct)
        try:
            cmd_run(["prlctl", "exec", ct, "rm","-f","/.vzfifo"],check=False)
            cmd_run(["prlctl", "exec", ct, "shutdown","-h","0"],check=False)
            time.sleep(15)
            result=cmd_run(["prlctl", "status", ct],False)
            if 'exist stopped' not in result.stdout:
                logger.error("Failed to get %s to the stopped state" % ct)    
                raise
        except:
            logger.error("Failed to get %s to the stopped state" % ct)
            raise
    logger.debug("CT %s stopped" % ct)

def get_ct_distro(ct):
    VMFlavour={
        "almalinux-8": "almalinux8",
        "almalinux-9": "almalinux9",
        "centos-6": "centos",
        "centos-7": "centos7",
        "centos-8": "centos8",
        "centos-9": "centos",
        "rockylinux-8": "rockylinux8",
        "ubuntu-18": "ubuntu18.04",
        "ubuntu-20": "ubuntu20.04",
        "ubuntu-22": "ubuntu",
        "ubuntu-24": "ubuntu",
        "debian-10": "debian",
        "debian-11": "debian",
        "debian-12": "debian",
        "unknown": ""
    }
    ct_template=get_ct_config(ct)["Template"]
    for template in VMFlavour:
        if ct_template.startswith(template):
            break
    if template == "unknown":
        logger.error("Unknown container template in CT %s: %s" % (ct, ct_template))
        raise ValueError("Unknown container template")
    start_ct(ct)
    pretty=""
    try:
        result=cmd_run(["prlctl", "exec", ct,"cat","/etc/os-release"])
        for line in result.stdout.split("\n"):
            if "PRETTY_NAME" in line:
                pretty=line.replace("\n","")
    except:
        logger.error("Error reading the %s CT os-release" % ct)
        raise ValueError("Error reading the %s CT os-release" % ct)
    pretty=pretty.replace('PRETTY_NAME="','').replace('"','')
    logger.debug("%s OS inside is %s" % (ct,pretty))
    logger.debug("%s OS by vzpkg in %s" % (ct,template))
    (os_template,os_version)=template.split("-")
    os_template=os_template.replace("rockylinux","rocky")
    if os_template in pretty.lower() and (os_version+"." in pretty.lower() or " "+os_version+" " in pretty.lower()+" "):
        logger.debug("CT %s distro is %s" % (ct, VMFlavour[template]))
        return VMFlavour[template]
    else:
        logger.error("Inconsistent  container template in CT %s: template '%s' inequal to '%s'" % (ct,template,pretty,))
        raise ValueError("Inconsistent  container template")
    

def fix_ubuntu18_kernel_setup(ct):
    logger.warning("Removing X flag from /usr/share/initramfs-tools/hooks/fixrtc to ensure proper kernel installation")
    try:
        cmd_run(["prlctl", "exec", ct, "chmod", "644", "/usr/share/initramfs-tools/hooks/fixrtc"])
    except:
        logger.error("Failed to remove X flag from /usr/share/initramfs-tools/hooks/fixrtc")
        raise
    logger.debug("Removed X flag from /usr/share/initramfs-tools/hooks/fixrtc")

def fix_ubuntu20_kernel_setup(ct):
    logger.warning("Creating the /etc/kernel/postinst.d/apt-auto-removal script to ensure proper kernel installation")
    try:
        cmd_run(["prlctl", "exec", ct, "echo", "'#!/bin/bash'", ">","/etc/kernel/postinst.d/apt-auto-removal"])
        cmd_run(["prlctl", "exec", ct, "echo", "'echo \"Not needed to remove kernel\"'", ">>","/etc/kernel/postinst.d/apt-auto-removal"])
        cmd_run(["prlctl", "exec", ct, "chmod", "+x", "/etc/kernel/postinst.d/apt-auto-removal"])
    except:
        logger.error("Failed to create /etc/kernel/postinst.d/apt-auto-removal script")
        raise
    logger.debug("Created the /etc/kernel/postinst.d/apt-auto-removal script")

def install_kernel_and_grub(ct,vm_distro):
    command= ["vzpkg", "install", "-p", ct]
    if vm_distro=="debian":
        command.append("linux-image-amd64")
        command.append("linux-headers-amd64")
        command.append("grub2")
        command.append("getty-run")
        command.append("rungetty")
        command.append("ngetty")
        command.append("passwd")
    elif vm_distro.startswith("ubuntu"):
        command.append("linux-image-generic")
        command.append("linux-headers-generic")
        command.append("grub2")
        command.append("getty-run")
        command.append("rungetty")
        command.append("ngetty")
        command.append("passwd")
    else:
        command.append("kernel")
        command.append("grub2")
        command.append("grubby")
    try:
        start_ct(ct)
        if vm_distro == "ubuntu20.04":
            fix_ubuntu20_kernel_setup(ct)
        if vm_distro == "ubuntu18.04":
            fix_ubuntu18_kernel_setup(ct)
        cmd_run(command)
        logger.debug(f"Kernel and GRUB installed successfully on container '{ct}'.")
    except:
        logger.error(f"Failed to install kernel and GRUB on container '{ct}'.")
        raise

def remove_ctpreset(ct,vm_distro):
    command= ["vzpkg", "remove", "-p", ct]
    if vm_distro=="debian" or vm_distro.startswith("ubuntu"):
        command.append("ct-preset-deb")
    else:
        command.append("ct-preset-systemd")
        command.append("vz-dummy-systemd")
        command.append("ct-preset-common")
    try:
        start_ct(ct)
        cmd_run(command)
        logger.debug(f"Removed ct-presets from container '{ct}'.")
    except:
        logger.error(f"Failed to remove ct-presets from container '{ct}'.")
        raise


def vm_exists(vm):
    try:
        result = cmd_run(["prlctl", "list", "-a", "-o", "name", "--vmtype", "vm"])
        return vm in result.stdout
    except Exception as e:
        logger.error(e)
        return False

def create_vm(vm_name, vm_distro):
    if vm_exists(vm_name):
        logger.error(f"Virtual machine '{vm_name}' already exists.")
        raise ValueError(f"Virtual machine '{vm_name}' already exists.")
    if vm_distro == "almalinux8" or vm_distro == "rockylinux8" :
        os_template="rhel8"
    elif vm_distro.startswith("ubuntu"):
        os_template="ubuntu"
    else:
        os_template=vm_distro
    try:
        result=cmd_run(["prlctl", "create", vm_name, "-d", os_template])
        logger.debug(("\n"+result.stdout).replace("\n","\n\t\t"))
        if not result.returncode:
            logger.debug(f"Virtual machine '{vm_name}' created successfully.")
        else:
            logger.error(f"Failed to create virtual machine '{vm_name}': Returncode is {result.returncode}")
            raise ValueError(f"Failed to create virtual machine '{vm_name}': Returncode is {result.returncode}")
    except subprocess.CalledProcessError as e:
        logger.error(f"Failed to create virtual machine '{vm_name}': {e}")
        raise

    try:
        diskimage=get_ct_config(vm_name)["Hardware"]["hdd0"]["image"]
        cmd_run(["prlctl", "set", vm_name,"--device-del","net0"])
        cmd_run(["prlctl", "set", vm_name,"--device-del","hdd0"])
        cmd_run(["prlctl", "set", vm_name,"--device-del","cdrom0"])
        cmd_run(["rm", "-rf", diskimage])
    except subprocess.CalledProcessError as e:
        logger.error(f"Failed virtual machine '{vm_name}' preliminary configuration: {e}")
        raise

def run_prl2qcow(vmdir, disk, rootdisk=True, swapsize=2048) -> None:
    logger.debug("Starting prl2qcow for %s" % os.path.join(vmdir, disk))
    swap=str(1025+swapsize)+"M"
    if rootdisk:
        command = ["prl2qcow", "-s", swap, "--meta", os.path.join(os.path.join(vmdir, disk),"root.hds.meta"), os.path.join(os.path.join(vmdir, disk),"root.hds")]
    else:
        command = ["prl2qcow", "--meta", os.path.join(os.path.join(vmdir, disk),"root.hds.meta"), os.path.join(os.path.join(vmdir, disk),"root.hds")]
    logger.debug("Executing prl2cow with optionset %s" % command[1:])
    try:
        result=cmd_run(command)
        logger.debug("prl2qcow output: " + ("\n"+result.stdout).replace("\n","\n\t\t"))
        if not result.returncode:
            logger.debug("Converted %s to QCOW" % os.path.join(vmdir, disk))
            os.rename(os.path.join(vmdir, disk,"root.hds"),os.path.join(vmdir,f"{disk}.hdd"))
            os.rename(os.path.join(vmdir, disk,"root.hds.meta"),os.path.join(vmdir,f"{disk}.hds.meta"))
            os.rmdir(os.path.join(vmdir, disk))
            return os.path.join(os.path.join(vmdir,f"{disk}.hdd"))
        else:
            logger.error(f"Failed to convert disk '{os.path.join(vmdir, disk)}': Returncode is {result.returncode}")
            raise ValueError(f"Failed to convert disk '{os.path.join(vmdir, disk)}': Returncode is {result.returncode}")
    except subprocess.CalledProcessError as e:
        logger.error(f"Failed prl2qcow '{os.path.join(vmdir, disk)}': {e}")
        raise ValueError(f"Failed prl2qcow '{os.path.join(vmdir, disk)}': {e}")


def create_temp_dir(tempdir):
    msg={
        'start':None,
        'finish':None,
        'debug':None,
        'timeout':None,
        'except':"Failed to create temp dir %s" % tempdir,
        'failed':"Failed to create temp dir %s" % tempdir,
    }
    cmd_run(["mkdir", "-p", tempdir],True,msg)

def create_ct_disks_copy(disk,newdir):
    try:
        cmd_run(["mkdir", "-p", newdir]) 
    except subprocess.CalledProcessError as e:
        logger.error(f"Failed to create temp dir '{newdir}': {e}")
        raise
    disk_path=disk["image"]+"/root.hds"
    try:
        cmd_run(["cp", disk_path, newdir])
        logger.debug(f"Disk {disk_path} copied successfully.")
    except subprocess.CalledProcessError as e:
        logger.error(f"Failed to copy disk {disk} from {disk_path}: {e}")
        raise


def load_nbd_module() -> None:
    msg={
        'start':None,
        'finish':"NBD module loaded successfully.",
        'debug':None,
        'timeout':None,
        'except':"Failed to load NBD module: ",
        'failed':"Failed to load NBD module: "
    }
    cmd_run(["modprobe", "nbd", "max_part=8"],True,msg)


def connect_nbd(vm_disk) -> None:
    try:
        attempt=3
        while attempt:
            ret=cmd_run(["qemu-nbd", "-c", "/dev/nbd0", vm_disk], check=False)
            if ret.returncode==0:
                logger.debug(f"Connected {vm_disk} to /dev/nbd0 successfully.")
                break
            else:
                time.sleep(5)
                attempt=attempt-1
                if not attempt:
                    raise subprocess.CalledProcessError(ret.returncode,["qemu-nbd", "-c", "/dev/nbd0", vm_disk],ret.stdout,ret.stderr)
    except subprocess.CalledProcessError as e:
        logger.error(f"Failed to connect {vm_disk} to /dev/nbd0: {e}")
        raise

def disconnect_nbd() -> None:
    attempt=0
    while True:
        result = cmd_run(["lsblk"])
        if "osprober" not in result.stdout:
            break
        attempt+=1
        logger.warning("Waiting osprober to exit gracefully (Attempt #%s)" % attempt)
        if (attempt % 3) == 0:
            result = cmd_run(["dmsetup", "remove", "-f", "osprober-linux-nbd0p1"])
        time.sleep(5)
    try:
        result=cmd_run(["qemu-nbd", "-d", "/dev/nbd0"])
        logger.debug("Unmounted qemu-nbd: %s"% result.stdout)
        logger.debug("/dev/nbd0 disconnected successfully.")
    except subprocess.CalledProcessError as e:
        logger.error(f"Failed to disconnect /dev/nbd0: {e}")
        raise
    while True:
        result = cmd_run(["lsblk"])
        if "nbd0" not in result.stdout:
            break
        logger.warning("Waiting nbd0 to be dismounted")
        time.sleep(5)


def initialize_partition_table() -> None:
    try:
        result = cmd_run(["sgdisk", "-o", "/dev/nbd0"],check=[0,1,2])
        logger.debug("Calling sgdisk to restore master GPT after shift with output:")
        logger.debug(result.stdout.replace("\n", "\n\t\t"))
        logger.debug(result.stderr.replace("\n", "\n\t\t"))
        if result.returncode==0:
            logger.debug("Initialized partition table on /dev/nbd0.")
        else:
            result = cmd_run(["sgdisk", "-o", "/dev/nbd0"],check=True)
            logger.debug("Calling sgdisk to restore master GPT after shift with output:")
            logger.debug(result.stdout.replace("\n", "\n\t\t"))
            logger.debug(result.stderr.replace("\n", "\n\t\t"))
            if "The operation has completed successfully" in result.stdout:
                logger.debug("Initialized partition table on /dev/nbd0.")
            else:
                result.check_returncode()
    except subprocess.CalledProcessError as e:
        logger.error(f"Failed to initialize partition table on /dev/nbd0: {e}")
        raise


def create_partitions(rootdisk,swapsize=2048) -> None:
    logger.debug("Creating partitions on /dev/nbd0.")
    swap="3:0:+"+str(swapsize)+"M"
    if rootdisk:
        command=["sgdisk", "-g", "/dev/nbd0", "-n", "1:0:+1M", "-t", "1:ef02", "-n", "2:0:+1G", "-t", "2:8300", "-n",
             swap, "-t", "3:8200", "-N", "4", "-t", "4:8300"]
    else:
        command=["sgdisk", "-g", "/dev/nbd0", "-N", "1", "-t", "1:8300"]
    logger.debug("Creating partition table with following parameters: %s" % command[1:])
    try:
        result = cmd_run(command)
        logger.debug("Partitions created: " + ("\n"+result.stdout).replace("\n","\n\t\t"))
        logger.debug("Partitions created successfully on /dev/nbd0.")
    except subprocess.CalledProcessError as e:
        logger.error(f"Failed to create partitions on /dev/nbd0: {e}")
        raise


def create_filesystems(rootdisk) -> None:
    try:
        if rootdisk:
            cmd_run(["mkfs.vfat", "/dev/nbd0p1"], check=True)
            cmd_run(["mkfs.ext4", "/dev/nbd0p2"], check=True)
            cmd_run(["mkswap", "/dev/nbd0p3"], check=[0,1])
            cmd_run(["e2fsck", "-fy", "/dev/nbd0p4"], check=[0,1,2])
            cmd_run(["resize2fs", "/dev/nbd0p4"], check=True)
        else:
            cmd_run(["e2fsck", "-fy", "/dev/nbd0p1"], check=True)
            cmd_run(["resize2fs", "/dev/nbd0p1"], check=True)
        logger.debug("Filesystems created successfully.")
    except subprocess.CalledProcessError as e:
        logger.error(f"Failed to create filesystems: {e}")
        raise

def fix_rhel9_boot(root_boot_mntdir):
    if os.path.exists(os.path.join(root_boot_mntdir,"loader","entries")):
        for file in os.scandir(os.path.join(root_boot_mntdir,"loader","entries")):
            fout=[]
            with open(file, "r") as fin:
                for line in fin:
                    fout.append(line.replace('/boot/','/').replace("nbd0p","sda"))
            with open(file,"w") as fin:
                fin.writelines(fout)

        


def configure_grub(mntdir,vm_distro,extradisks) -> None:
    logger.debug("Use the %s as temporary mount dir" % mntdir)
    root_mntdir=os.path.join(mntdir,"root")
    root_boot_mntdir=os.path.join(mntdir,"root","boot")
    boot_mntdir=os.path.join(mntdir,"boot")
    try:
        cmd_run(["mkdir", "-p", root_mntdir], check=True)
        cmd_run(["mkdir", "-p", root_boot_mntdir], check=True)
        cmd_run(["mkdir", "-p", boot_mntdir], check=True)
        mount_dev("/dev/nbd0p2", boot_mntdir)
        allfiles = os.listdir(root_boot_mntdir)
        for f in allfiles:
            shutil.move(os.path.join(root_boot_mntdir, f), os.path.join(boot_mntdir, f))
        umount_dev(boot_mntdir)
        mount_dev("/dev/nbd0p2", root_boot_mntdir)

        create_fstab(root_mntdir,extradisks)

        cmd_run(["mkdir", "-p", os.path.join(root_mntdir,"sys")], check=True)
        cmd_run(["mkdir", "-p", os.path.join(root_mntdir,"dev")], check=True)
        cmd_run(["mkdir", "-p", os.path.join(root_mntdir,"proc")], check=True)

        cmd_run(["mount", "-o", "bind", "/sys", os.path.join(root_mntdir,"sys")], check=True)
        cmd_run(["mount", "-o", "bind", "/dev", os.path.join(root_mntdir,"dev")], check=True)
        cmd_run(["mount", "-t", "proc", "/proc", os.path.join(root_mntdir,"proc")], check=True)

        if vm_distro=="debian" or vm_distro=="ubuntu" or vm_distro=="ubuntu20.04" or vm_distro=="ubuntu18.04":
            cmd_run(["chroot", root_mntdir, "grub-mkconfig", "-o", "/boot/grub/grub.cfg"], True, env={"PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"})
            cmd_run(["chroot", root_mntdir, "grub-install", "--boot-directory=/boot", "/dev/nbd0"], True, env={"PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"})
            cmd_run(["chroot", root_mntdir, "sed", "-i", "s/nbd[0-9]p/sda/","/boot/grub/grub.cfg"], True, env={"PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"})
            cmd_run(["chroot", root_mntdir, "systemctl", "enable", "getty@tty1.service"], True, env={"PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"})
            cmd_run(["chroot", root_mntdir, "systemctl", "disable", "systemd-time-wait-sync"], True, env={"PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"})
            cmd_run(["chroot", root_mntdir, "systemctl", "disable", "systemd-networkd-wait-online"], True, env={"PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"})
            if os.path.exists(os.path.join(root_mntdir,"etc/init.d/modules_dep.sh")):
                cmd_run(["chroot", root_mntdir, "chmod", "-x", "/etc/init.d/modules_dep.sh"], True, env={"PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"})
        else:
            cmd_run(["chroot", root_mntdir, "grub2-mkconfig", "-o", "/boot/grub2/grub.cfg"], True, env={"PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"})
            cmd_run(["chroot", root_mntdir, "grub2-install", "--boot-directory=/boot", "--target=i386-pc","/dev/nbd0"], True, env={"PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"})
            if os.path.exists(os.path.join(root_boot_mntdir,"loader","entries")):
                cmd_run(["chroot", root_mntdir, "rm", "/etc/grub.d/30_os-prober"], False, env={"PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"})
                fix_rhel9_boot(root_boot_mntdir)
            if vm_distro=="centos7":
                cmd_run(["chroot", root_mntdir, "sed", "-i", "s/nbd[0-9]p/sda/","/boot/grub2/grub.cfg"], True,env={"PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"})
                cmd_run(["chroot", root_mntdir, "sed", "-i", "s/linuxefi/linux/","/boot/grub2/grub.cfg"], True, env={"PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"})
                cmd_run(["chroot", root_mntdir, "sed", "-i", "s/initrdefi/initrd/","/boot/grub2/grub.cfg"], True, env={"PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"})
            cmd_run(["chroot", root_mntdir, "systemctl", "enable", "getty@tty1"], True, env={"PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"})
            if os.path.exists(os.path.join(root_mntdir,"etc/init.d/modules_dep.sh")):
                cmd_run(["chroot", root_mntdir, "chmod", "-x", "/etc/init.d/modules_dep.sh"], True, env={"PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"})
            if os.path.exists(os.path.join(root_mntdir,"etc/selinux/config")):
                cmd_run(["chroot", root_mntdir, "sed", "-i", "s/^SELINUX=.*/SELINUX=disabled/","/etc/selinux/config"], True, env={"PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"})
        logger.debug("GRUB configured successfully.")
    except subprocess.CalledProcessError as e:
        logger.error(f"Failed to configure GRUB: {e}")
        raise
    except:
        logger.error("Failed to configure GRUB!")
        raise


def get_partition_uuid(partition: str) -> str:
    try:
        result = cmd_run(["blkid", "-o", "value", "-s", "UUID", partition])
        result.check_returncode()
        return result.stdout.strip()
    except subprocess.CalledProcessError as e:
        logger.error(f"Failed to get UUID for {partition}: {e}")
        raise


def create_fstab(root_mntdir, extra_mounts) -> None:
    try:
        boot_uuid = get_partition_uuid("/dev/nbd0p2")
        swap_uuid = get_partition_uuid("/dev/nbd0p3")
        root_uuid = get_partition_uuid("/dev/nbd0p4")
    except Exception as e:
        logger.error(f"Error getting UUIDs: {e}")
        return

    fstab_content = (
        f"UUID={boot_uuid}    /boot    ext4    defaults        1 2\n"
        f"UUID={swap_uuid}    swap     swap    defaults        0 0\n"
        f"UUID={root_uuid}    /        ext4    defaults        1 1\n"
        f"\n"
    )
    logger.debug("Adding extra mounts: %s" % extra_mounts)
    for mount in extra_mounts:
        fstab_content+=f"UUID={extra_mounts[mount]}    {mount}    ext4    defaults        1 1\n"
    fstab_content+=f"\n"

    logger.debug(("%s created: \n%s" % (os.path.join(root_mntdir,"etc", "fstab"),fstab_content)).replace("\n","\n\t\t"))
    try:
        with open(os.path.join(root_mntdir,"etc", "fstab"), "w") as fstab_file:
            fstab_file.write(fstab_content)
        logger.debug("/etc/fstab created successfully.")
    except IOError as e:
        logger.error(f"Failed to update /etc/fstab: {e}")
        raise

def mount_dev(device, mount):
    try:
        cmd_run(["mkdir", "-p" , mount])
        while True:
            result = cmd_run(["mount", device, mount])
            if result.returncode == 0:
                break
            logger.warning("Waiting nbd partition to be mounted")
            time.sleep(5)
    except Exception as e:
        cmd_run(["lsblk"])
        logger.error(f"Error mounting {device} to {mount}: {e}")

def umount_dev(mount):
    try:
        while True:
            result=cmd_run(["umount", "-R", mount], check=False)
            if result.returncode ==0:
                break
            logger.warning("Waiting nbd partition to be unmounted")
            time.sleep(5)
    except Exception as e:
        cmd_run(["lsblk"])
        logger.error(f"Error unmounting {mount}: {e}")


def clear_networking(ct, devices):
    for device in devices:
        if device.startswith('venet') or device.startswith('net'):
            cmd_run(["prlctl", "set", ct,"--device-del",device],True)

def add_networking_to_vm(vm_name, devices):
    commands={
        'mac_filter': '--macfilter',
        'ip_filter': '--ipfilter',
        'network': '--network',
        'nameservers':'--nameserver',
        'searchdomains':'--searchdomain',
        'ips':'--ipadd',
        'gw':'--gw',
        'gw6':'--gw6',
        'dhcp':'--dhcp',
        'dhcp6':'--dhcp6',
        'preventpromisc':'--preventpromisc',
    }
    try:
        for device in devices:
            if device.startswith('venet') or device.startswith('net'):
                logger.debug("Adding the %s with config: %s" % (device, devices[device]))
                command=["prlctl", "set", vm_name,"--device-add","net"]
                if "network" in devices[device]:
                    command.append("--network")
                    command.append(devices[device]["network"])
                else:
                    command.append("--type")
                    command.append("routed")
                for value in devices[device]:
                    if value == "ips":
                        for ip in devices[device][value].split(" "):
                            if "." in ip:
                                command.append("--ipadd")    
                                command.append(ip)
                    elif value in commands:
                        command.append(commands[value])
                        command.append(devices[device][value])
                logger.debug("Command keys: %s" % command[3:])
                cmd_run(command)
    except:
        raise

def enable_vnc_vm(vm_name,passwd=None):
    if passwd:
        cmd_run(["prlctl", "set", vm_name, "--vnc-mode", "auto", "--vnc-passwd", passwd])
    else:
        cmd_run(["prlctl", "set", vm_name, "--vnc-mode", "auto", "--vnc-nopasswd"])

def adjust_cpu_vm(vm_name, hw):
    cpu=hw["cpu"]
    command=["prlctl", "set", vm_name]
    if "sockets" in cpu:
        command.append("--cpu-sockets")
        command.append(str(cpu["sockets"]))
    if "cpus" in cpu:
        command.append("--cpus")
        if cpu["cpus"]=="unlimited":
            command.append("2")
        else:
            command.append(str(cpu["cpus"]))
    try:
        cmd_run(command)
        logger.debug("Succesfully set CPU configuration according to %s" % cpu)
    except:
        logger.error("Failed to set CPU configuration to %s" % cpu)

def adjust_mem_vm(vm_name, hw):
    command=["prlctl", "set", vm_name]
    if "memory_guarantee" in hw:
        command.append("--memguarantee")
        if "value" in hw["memory_guarantee"]:
            command.append(str(hw["memory_guarantee"]["value"]).replace("%",""))
        else:
            command.append("auto")
    if "memory" in hw:
        # Commented due to PSBM-158986
        # if "hotplug" in hw["memory"]:
        #     command.append("--mem-hotplug")
        #     if hw["memory"]["hotplug"]:
        #         command.append("on")
        #     else:
        #         command.append("off")
        if "size" in hw["memory"]:
            command.append("--memsize")
            if "-" == hw["memory"]["size"][0]:
                command.append("4096")
            elif "Mb" in hw["memory"]["size"]:
                size=hw["memory"]["size"].replace("Mb","")
                if int(size) < 1024:
                    command.append("1024")
                else:
                    command.append(size)
            else:
                logger.warning("Failed to parse size %s in Megabytes. Setting to 4Gb" % hw["memory"]["size"])
                command.append("4096")
    try:
        cmd_run(command)
        logger.debug("Succesfully set memory configuration: %s" % command[3:])
    except:
        logger.error("Failed to set memory configuration to %s" % command[3:])

def add_disks_to_vm(vm_name, hw):
    try:
        home_dir=get_ct_config(vm_name)["Home"]
        for device in hw:
            if "vm_source" in hw[device]:
                cmd_run(["prlctl", "set", vm_name,"--device-add","hdd","--image", hw[device]["vm_source"]])
                logger.debug("Added disk %s with image %s" % (device, hw[device]["vm_source"]))
    except:
        raise

def harmonize_disk_sizes(vm_name, ct_hw, swapsize=2048):
    try:
        vm_hw=get_ct_config(vm_name, True)["Hardware"]
        ct_disks={}
        for dev in ct_hw:
            if "image" in ct_hw[dev]:
                if "vm_source" in ct_hw[dev]:
                   projected_size=int(ct_hw[dev]["size"].replace("Mb",""))
                   if ct_hw[dev]["mnt"] == "/":
                       projected_size=projected_size+1025+swapsize
                   ct_disks[ct_hw[dev]["vm_source"]]=projected_size
                else:
                    logger.warning("CT disk %s have no vm_source. Have it been converted?" % dev)
        for dev in vm_hw:
            if "image" in vm_hw[dev]:
                disk_size=int(vm_hw[dev]["size"].replace("Mb",""))
                if ct_disks[vm_hw[dev]["image"]] < disk_size:
                    logger.warning("Disk image %s needs resizing to size %sMb"%(vm_hw[dev]["image"],ct_disks[vm_hw[dev]["image"]]))
                    cmd_run(["prlctl","set",vm_name,"--device-set",dev,"--size=%s"%ct_disks[vm_hw[dev]["image"]]])
    except:
        pass    

def getvm_uuid(vmname):
    try:
        return get_ct_config(vmname)["ID"]
    except:
        return "Unknown"

def install_tools(vm_name):
    msg={
        'start':"Starting installing guest tools to %s" % vm_name,
        'finish':"Finished installing guest tools to %s" % vm_name,
        'debug':"prlctl install tools putput: %s:\n" % vm_name,
        'timeout':"Failed to install guest tools to VM %s in set amount of time" % vm_name,
        'except':"Failed to install guest tools to VM %s" % vm_name,
        'failed':"Failed to install guest tools to VM %s" % vm_name,
    }
    cmd_run(["prlctl", "installtools", vm_name],False,msg)
   

def run_virt_v2v(vm_name):
    msg={
        'start':"Starting virt-v2v for %s" % vm_name,
        'finish':"Finished virt-v2v for %s" % vm_name,
        'debug':"Virt-v2v VM %s:\n" % vm_name,
        'timeout':"Failed virt-v2v VM %s in set amount of time" % vm_name,
        'except':"Failed virt-v2v VM %s" % vm_name,
        'failed':"Failed virt-v2v VM %s" % vm_name,
    }
    if os.path.exists("/usr/lib64/guestfs/appliance9"):
        logger.debug("Using libguestfs appliance9 for virt-v2v")
        cmd_run(["virt-v2v", "--in-place", "-i", "libvirt", vm_name],True,msg, env={"LIBGUESTFS_PATH": "/usr/lib64/guestfs/appliance9"})
    else:
        logger.debug("Using libguestfs appliance7 for virt-v2v")
        cmd_run(["virt-v2v", "--in-place", "-i", "libvirt", vm_name],True,msg)
        
def start_vm(vm_name):
    msg={
        'start':"Starting VM %s after conversion" % vm_name,
        'finish':"Started VM %s after conversion" % vm_name,
        'timeout':"Failed VM %s startup in set amount of time" % vm_name,
        'except':"Failed to start VM %s" % vm_name,
        'failed':"Failed to start VM %s" % vm_name,
    }
    cmd_run(["prlctl", "start", vm_name],True,msg)

def wait_vm_to_exit(vm_name):
    firstboot_timeout=600
    while True:
        time.sleep(15)
        result=cmd_run(["prlctl", "status",vm_name],True,{})
        if "exist stopped" in result.stdout:
            return True
        firstboot_timeout=firstboot_timeout-15
        if firstboot_timeout <=0:
            logger.warning("Forcefully stopping VM %s" % (vm_name))
            stop_venv(vm_name)
        else:
            logger.debug("Waiting VM %s to gracefully shutdown after firstboot %s seconds more" % (vm_name, firstboot_timeout))

def ploop_check(image):
    msg={
        'start':"Starting ploop check for %s" % image,
        'finish':"Finished ploop check for %s" % image,
        'debug':"Ploop check for %s:\n" % image,
        'timeout':"Ploop check %s took extended amount of time" % image,
        'except':"Failed ploop check for %s" % image,
        'failed':"Failed ploop check for %s" % image,
    }
    cmd_run(["ploop", "check", "-f", os.path.join(image,"DiskDescriptor.xml")],True,msg)

def check_snapshots(ct):
    raw_snaps=cmd_run(["prlctl", "snapshot-list",ct]).stdout.split("\n")
    snaps=[]
    for raw_snap in raw_snaps:
        if raw_snap.endswith("}"):
            snaps.append(raw_snap.split()[-1].replace("*",""))
    if len(snaps):
        logger.warning("The %s has snapshots." % ct)
        if args.yes or (not args.quiet and prompt("Type 'yes' if you want to merge snapshots right now. (y/n)")):
            for snap in snaps:
                cmd_run(["prlctl", "snapshot-delete", ct, "-i", snap])
        else:
            logger.error("The %s has snapshots. You need to handle with merging them manually. Skipping." % ct)
            return False
    return True

def clear_balloon(dir):
    balloon=None
    for file in os.listdir(dir):
        if file.startswith(".balloon-"):
            balloon=file
            break
    if balloon:
        os.remove(os.path.join(dir, balloon))
        logger.warning("Removed balloon file /%s" % balloon)


def convert_ct(ct,swap):
    global ST
    signal.signal(signal.SIGALRM, timeout_handler)
    signal.alarm(args.timeout*60)
    try:
        starttime=datetime.datetime.now()
        logger.info(f"Starting conversion for CT {ct}.")
        if cep:
            logger.debug(f"Sending CEP data for {ct}.")
        else:
            logger.debug(f"Not sending CEP data for {ct}.")
        vm_distro="Undetected"
        
        ST=1
        logger.info("Preconversion checks for container")
        ct_state=(get_ct_state(ct) == "running")
        if not check_snapshots(ct):
            return False
        hw_list=get_ct_config(ct)["Hardware"]
        vm_distro = get_ct_distro(ct)
        

        ST=2
        logger.info("Checking disks consistency")
        stop_venv(ct)
        for device in hw_list:
            if "image" in hw_list[device]:
                ploop_check(hw_list[device]["image"])

        ST=3
        logger.info("Installing kernel and grub, preparing CT for conversion")
        install_kernel_and_grub(ct, vm_distro)
        remove_ctpreset(ct, vm_distro)
        clear_networking(ct,hw_list)
        stop_venv(ct)

        ST=4
        logger.info("Creating the VM for migration")
        vm_name = PREFIX + ct
        if len(vm_name) > 40:
            vm_name = vm_name[:40]
            logger.warning(f"CT name: {ct} is too long. "
                           f"Converted VM name will be truncated to 40 characters: {vm_name}")
        create_vm(vm_name, vm_distro)
        adjust_cpu_vm(vm_name,hw_list)
        adjust_mem_vm(vm_name,hw_list)
        enable_vnc_vm(vm_name)
        vmuuid=getvm_uuid(vm_name)
        homedir=get_ct_config(vm_name,True)["Home"]
        

        ST=5
        logger.info("Converting non-root disks in container")
        extra_mounts={}
        for device in hw_list:
            if "image" in hw_list[device]:
                if "mnt" in  hw_list[device] and hw_list[device]["mnt"]=="/":
                    rootdisk=device
                    continue
                create_ct_disks_copy(hw_list[device],os.path.join(homedir,device))
                vm_disk=run_prl2qcow(homedir,device,False,swap)
                hw_list[device]["vm_source"]=vm_disk
                connect_nbd(vm_disk)
                initialize_partition_table()
                create_partitions(False,swap)
                create_filesystems(False)
                if "mnt" in hw_list[device]:
                    extra_mounts[hw_list[device]["mnt"]]=get_partition_uuid("/dev/nbd0p1")
                else:
                    logger.warning(f"Disk {device} were converted however there is missing information on its mount point and it will not be mounted.")                    
                disconnect_nbd()


        if extra_mounts:
            logger.warning("Extra disks to be added to fstab on %s: %s" % (rootdisk, extra_mounts))
        else:
            logger.info("No extra disks to add to fstab")
        
        ST=6
        logger.info("Converting root in container")
        create_ct_disks_copy(hw_list[rootdisk],os.path.join(homedir,rootdisk))
        vm_disk=run_prl2qcow(homedir,rootdisk,True,swap)
        hw_list[rootdisk]["vm_source"]=vm_disk
        time.sleep(2)
        connect_nbd(vm_disk)
        initialize_partition_table()
        create_partitions(True,swap)
        create_filesystems(True)
        mount_dev("/dev/nbd0p4", os.path.join(tempdir, ct, "mnt", "root"))
        clear_balloon(os.path.join(tempdir, ct, "mnt", "root"))
        configure_grub(os.path.join(tempdir,ct,"mnt"), vm_distro, extra_mounts)
        umount_dev(os.path.join(tempdir, ct, "mnt", "root"))
        disconnect_nbd()
        add_disks_to_vm(vm_name,hw_list)
        harmonize_disk_sizes(vm_name,hw_list)
        
        ST=7
        logger.info("Configuring network")
        add_networking_to_vm(vm_name,hw_list)

        ST=8
        logger.info("Installing Guest Tools and Agent")
        install_tools(vm_name)

        ST=9
        logger.info("Run v2v migration")
        run_virt_v2v(vm_name)

        ST=10
        logger.info("Start VM after v2v migration")
        start_vm(vm_name)
        wait_vm_to_exit(vm_name)

        ST=11
        logger.info("Cleanup and correct VENV names")
        stop_venv(ct)
        stop_venv(vm_name)
        add_data_to_cep(ct, True, None, 0, hw_list, starttime)
        cmd_run(["prlctl", "set",ct,"--name","C2V_"+ct])
        cmd_run(["prlctl", "set",vm_name,"--name",ct])
        cleanup(tempdir,ct)
        if args.target_state=="start" or (args.target_state=="keep" and ct_state):
            start_vm(ct)

        ST=-1
        logger.info("Conversion complete.\n\nThe source container is stopped and available as fallback.\nIf the resulting VM experiences boot issues, use prlctl start %s.\n\nThe resulting VM UUID: %s\nThe resulting VM name: %s" % ("C2V_"+ct,vmuuid,ct))
        return True
    except subprocess.TimeoutExpired:
        logger.error("%s conversion timed out." % ct)
        add_data_to_cep(ct, False, "Timeout",ST, hw_list, starttime)
        signal.alarm(0)
        cleanup(tempdir,ct)
        ST=-1
        return False
    except Exception as e:
        import traceback
        logger.error("Failed to convert CT %s. Cleaning up." % ct)
        if args.debug:
            logger.debug(traceback.format_exc())
        add_data_to_cep(ct, False, traceback.format_exc(), ST, hw_list, starttime)
        ST=-1
        signal.alarm(0)
        cleanup(tempdir,ct)
        return False

def cleanup(tempdir,ct):
    ct_root_path=os.path.join(tempdir,ct,"mnt","root")
    if ct_root_path in str(subprocess.run(["mount"], stdout=subprocess.PIPE, stderr=subprocess.PIPE).stdout):
        umount_dev(ct_root_path)
    if "nbd0" in str(subprocess.run(["lsblk"], stdout=subprocess.PIPE, stderr=subprocess.PIPE).stdout):
        disconnect_nbd()
    if not args.debug and os.path.exists(os.path.join(tempdir,ct)):
        shutil.rmtree(os.path.join(tempdir,ct))
        

def is_valid_uuid(uuid_to_test, version=4):
    from uuid import UUID
    try:
        UUID(uuid_to_test, version=version)
    except ValueError:
        return False
    return True
    

def get_ct_config(ct_id,resync=False):
    global current_ct_config
    if resync or ct_id not in current_ct_config:
        try:
            ret=cmd_run(["prlctl", "list","--full","-j","-i",ct_id])
            current_ct_config[ct_id]=json.loads(ret.stdout)[0]
            if current_ct_config[ct_id]["Type"]=="CT":
                result=str(cmd_run(["vzlist", ct_id, "-H", "-o", "ostemplate"]).stdout).split("\n")[0][1:].strip()
                logger.info("CT %s template is %s" % (ct_id, result))
                current_ct_config[ct_id]["Template"]=result
        except:
            raise ValueError("Failed to get config for container %s" % ct_id)
    return current_ct_config[ct_id]

def get_ct_name(ct_id):
    try:
        return get_ct_config(ct_id)["Name"]
    except ValueError:
        return False

def get_ct_app(ct_id):
    config_file=os.path.join(get_ct_config(ct_id)["Home"],"ve.conf")
    with open(config_file,"r") as f:
        lines=f.readlines()
        for line in lines:
            if "TEMPLATES" in line:
                return line.replace('TEMPLATES="','').replace('"','').replace(".","").strip()
    return ""

def add_data_to_cep(ct_id, success, reason ,fail_step, hw_list, starttime):
    #attempt_id: <salt from CTID>
    # date: <date>
    # success: false
    # fail_step: 3
    # ct_os: centos-6-x86_64
    # ct_app: .plesk
    # nics: 1
    # disks: 2
    if not cep:
        return
    
    disks=[]
    nics=[]
    for device in hw_list:
        if "image" in hw_list[device]:
            disks.append({"name": device, "size": hw_list[device]["size"]})
        elif device.startswith('venet'):
            nics.append({"name": device, "type": "routed"})
        elif device.startswith('net'):
            if "network" in hw_list[device]:
                nics.append({"name": device, "network": hw_list[device]["network"]})
            
    data={
        "ct_id": getvm_uuid(ct_id),
        "date": str(datetime.datetime.now()),
        "success": success,
        "reason": reason, 
        "fail_step": fail_step,
        "ct_os": get_ct_config(ct_id)["Template"],
        "ct_app": get_ct_app(ct_id),
        "nics": nics,
        "disks": disks,
        "execution_time": ((datetime.datetime.now()-starttime).seconds)
    }
    print ("Saving following information for CEP: %s" % data)
    try: 
        with open(cepfile,"r") as f:
            cep_data = json.load(f)
    except FileNotFoundError as e:
        cep_data=[]            
    except json.decoder.JSONDecodeError as e:
        cep_data=[]
    cep_data.append(data)
    try:
        with open(cepfile, "w") as f:
            f.write(json.dumps(cep_data))
    except:
        pass

def main():
    global tempdir
    global cep
    global logdir
    global CT
    global CT_count
    global args
    global log_formater
    helptext="This option specifies the desired target state of VM(s) after conversion.\n\n"\
                        "* keep: the resulting VM(s) will keep the state of converted container. If container was started before the conversion, resulting VM will be started. If a container was stopped before the conversion, it will be stopped.\n\n"\
                        "* stop: the resulting VM(s) will be stopped, regardless of the container state prior to conversion.\n\n"\
                        "* start: the resulting VM(s) will be started, regardless of the container state prior to conversion. \n"\
                        "By default: keep"

    parser = argparse.ArgumentParser(description="C2V migration")
    parser.add_argument("--version", action="version", version=f"C2V migration {PACKAGE_VERSION}", help="Specifies the program's version number.")
    parser.add_argument("-q", "--quiet", action='store_true', help="Skips the confirmation prompt and suppresses the output.\The -q option assumes “Ask me later” for Data Collection by CEP.")
    parser.add_argument("-y", "--yes", action='store_true', help="The system automatically selects 'Y' for all requests.\nThe -y option assumes “Ask me later” for Data Collection by CEP.")
    parser.add_argument("-v", "--verbose", action='store_true', help="The verbose mode provides detailed processing information.")
    parser.add_argument("-d", "--debug", action='store_true', help="Enable debug output")
    # parser.add_argument("-l", action='count', default=0, help="The log level provides log records for the container subject to conversion.")
    parser.add_argument("-b","--batch_log", action='store_true', help="Creates a batch log file that provides log records for the containers subject to conversion.")
    parser.add_argument("--timeout", type=int, default=30, help="The operation timeout in minutes [5..360]. By default, it is 30 minutes.")
    parser.add_argument("--swap", type=int, default=2048, help="The swap size in MB. By default, it is 2,048 MB.")
    parser.add_argument("--target-state", type=str, choices=["keep", "start", "stop"], default="keep", help=helptext)
    parser.add_argument("--log", type=str, help="The name of the folder to store log files.")
    parser.add_argument("--temp", type=str, default="/vz/tmp/c2v", help="The name of the folder to store temp files.")
    parser.add_argument("-f", "--from-file", type=str, help="The name for the batchfile.")
    parser.add_argument("ct", nargs="*", type=str, help="The name of the container to be converted to a virtual machine.")
    args = parser.parse_args()

    ct=args.ct
    swap=args.swap
    tempdir=args.temp

    

    if args.verbose or args.debug :
        log_level=logging.DEBUG
    elif args.quiet and not args.log:
        log_level=logging.ERROR
    else:
        log_level=logging.INFO
    
    if args.verbose and args.quiet:
        logger.error("Can't execute both Verbose/Debug and Quiet flags")
        parser.print_help()
        sys.exit(201)

    logger.setLevel(logging.DEBUG)
    std_handler=logging.StreamHandler()
    std_handler.setFormatter(log_formater)
    std_handler.setLevel(log_level)
    logger.addHandler(std_handler)

    logger.debug("Starting conversion with %s" % args)

    if args.log:
        if os.path.isdir(args.log):
            logdir=args.log
        else:
            logger.error("Logging dir(%s) does not exists" % args.log)
            sys.exit(202)
    
    batch_handler=None
    if args.batch_log:
        batch_handler=logging.FileHandler(os.path.join(logdir,"%s-batch-conversion.log"%(datetime.datetime.now().strftime("%Y%m%d-%H-%M"))))
        batch_handler.setFormatter(log_formater)
        batch_handler.setLevel(logging.INFO)
        logger.addHandler(batch_handler)

    if (args.timeout > 360) or (args.timeout < 5):
        logger.error("Timeout value is out of allowed interval")
        parser.print_help()
        sys.exit(203)


    if not ct:
        if args.from_file:
            if os.path.exists(args.from_file):
                logger.debug("Reading the list from %s" % args.from_file)
                try:
                    with open(args.from_file,"r") as batchfile:
                        for batch in batchfile.readlines():
                            ct.append(batch.replace("\n",""))
                except Exception as e:
                    logger.error("Failed to read CT list from file %s. Exception: %s" % (args.from_file,e))
            else:
                logger.error("Unable to read CT list from file %s" % args.from_file)
    elif args.from_file:
        logger.error("Can't use both CT list and --from-file option %s" % args.from_file)
        parser.print_help()
        sys.exit(251)
    
    cep=cep_running()
    if not cep:
        if not args.quiet and not args.yes:
            if config_get(conffile)["cep"]!="never":
                cep=cep_prompt()

    #Preliminary actions detecting postactions, and parameters
    try:
        load_nbd_module()
    except:
        sys.exit(230)
    
    try:
        create_temp_dir(tempdir)
    except:
        sys.exit(231)
    exclusion_list={}
    logger.info("Validating the list of CTs")
    if ct:
        newct=[]
        for c in ct:
            name=get_ct_name(c)
            if not name:
                exclusion_list[c]="Non existent"
            elif get_ct_config(name)["Type"] != "CT":
                exclusion_list[name]="not a container"
            elif name in ["va-mn", "vstorage-ui", "va-pp"]:
                exclusion_list[name]="not required"
            elif get_ct_config(name)["State"] not in ["running", "stopped", "paused", "suspended"]:
                exclusion_list[c]="container state improper (%s)" % get_ct_config(name)["State"]
            elif get_ct_config(name)["Name"] == get_ct_config(name)["ID"]:
                exclusion_list[c]="Container UUID %s is the same as container name. Please rename the container before converting it." % get_ct_config(name)["ID"]
            elif name not in newct:
                newct.append(name)
            else:
                logger.warning("Dropping duplicate entry from list %s" % name)
        ct=newct
    if exclusion_list:
        logger.warning("The following containers will be excluded from conversion: %s" % exclusion_list)
    else:
        logger.info("Container list successfully validated")
    if not ct:
        logger.error("No containers suitable for conversion were found, please review the conversion log to learn why.")
        sys.exit(232)
    else:
        logger.debug("Container list is : %s" % ct)
    CT_count=len(ct)
    if not args.quiet and not args.yes:        
        if not prompt("The conversion process will restart the containers multiple times and may take up to %s minutes to complete.\nProceed? (y/n)\n" % (args.timeout*CT_count)):
            sys.exit(233)
    if len(ct)==1:
        if not convert_ct(ct[0],swap):
            sys.exit(1)
    else:
        converted_list=[]
        failed_list=[]
        logger.info("Starting batch conversion of: %s" % ", ".join(ct))
        for c in ct:
            CT=ct.index(c)+1
            handler=logging.FileHandler(os.path.join(logdir,"%s-%s-conversion.log"%(datetime.datetime.now().strftime("%Y%m%d-%H-%M"),c)))
            handler.setFormatter(ct_log_formater)
            handler.setLevel(logging.DEBUG)
            logger.addHandler(handler)
            if convert_ct(c,swap):
                converted_list.append(c)
            else:
                failed_list.append(c)
            logger.removeHandler(handler)
        if converted_list:
            logger.info("Containers successfully converted: %s" % " ".join(converted_list))
        if failed_list:
            logger.warning("Containers failed to convert: %s" % " ".join(failed_list))
            if (len(failed_list) % 100) != 0:
                sys.exit(len(failed_list) % 100)
            else:
                sys.exit(1)
            

if __name__ == "__main__":
    main()

