#!/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.0.24-1.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

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)

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)
        if messages['debug']:
            logger.debug(messages['debug']+result.stdout)
        if check:
            if not result.returncode or result.returncode in checklist:
                logger.info(" %s returncode %s"%(" ".join(command),result.returncode))
            else:
                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):
    start_ct(ct)
    pretty=""
    try:
        result=subprocess.run(["prlctl", "exec", ct,"cat","/etc/os-release"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True, universal_newlines=True)
        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)
    logger.debug("%s CT OS %s" % (ct,pretty))
    pretty=pretty.replace('PRETTY_NAME="','').replace('"','')
    logger.debug("%s CT OS is %s" % (ct,pretty))
    distr="unknown"
    if "almalinux 8" in pretty.lower():
        distr="almalinux8"
    elif "almalinux 9" in pretty.lower():
        distr="almalinux9"
    elif "centos" in pretty.lower():
        if " 7." in pretty or " 7 " in pretty:
            distr="centos7"
        elif " 8." in pretty:
            distr="centos8"
        else:
            distr="centos"
    elif "rocky" in pretty.lower() and " 8." in pretty:
        distr="rockylinux8"
    elif "ubuntu" in pretty.lower():
        if "18.04" in pretty:
            distr="ubuntu18.04"
        elif "20.04" in pretty:
            distr="ubuntu20.04"
        else:
            distr="ubuntu"
    elif "debian" in pretty.lower():
        distr="debian"
    if distr=="unknown":
        logger.error("Unsupported container template in CT %s" % ct)
        raise ValueError("Unsupported container template")
    logger.debug("CT %s distro is %s" % (ct, distr))
    return distr

def fix_ubuntu18_kernel_setup(ct):
    logger.warning("Removing X flag from /usr/share/initramfs-tools/hooks/fixrtc to ensure proper kernel installation")
    try:
        subprocess.run(
            ["prlctl", "exec", ct, "chmod", "644", "/usr/share/initramfs-tools/hooks/fixrtc"],
            stdout=subprocess.PIPE, stderr=subprocess.PIPE,
            check=True,
            universal_newlines=True
        )
    except:
        logger.error("Failed to remove X flag from /usr/share/initramfs-tools/hooks/fixrtc")
    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:
        subprocess.run(
            ["prlctl", "exec", ct, "echo", "'#!/bin/bash'", ">","/etc/kernel/postinst.d/apt-auto-removal"],
            stdout=subprocess.PIPE, stderr=subprocess.PIPE,
            check=True,
            universal_newlines=True
        )
        subprocess.run(
            ["prlctl", "exec", ct, "echo", "'echo \"Not needed to remove kernel\"'", ">>","/etc/kernel/postinst.d/apt-auto-removal"],
            stdout=subprocess.PIPE, stderr=subprocess.PIPE,
            check=True,
            universal_newlines=True
        )
        subprocess.run(
            ["prlctl", "exec", ct, "chmod", "+x", "/etc/kernel/postinst.d/apt-auto-removal"],
            stdout=subprocess.PIPE, stderr=subprocess.PIPE,
            check=True,
            universal_newlines=True
        )
    except:
        logger.error("Failed to create /etc/kernel/postinst.d/apt-auto-removal script")
    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 ct_exists(ct):
    try:
        result = subprocess.run(
            ["prlctl", "list", ct],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            check=True,
            universal_newlines=True
        )
        return ct in result.stdout
    except:
        return False

def vm_exists(vm):
    try:
        result = subprocess.run(
            ["prlctl", "list", "-a", "-o", "name", "--vmtype", "vm"],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            check=True,
            universal_newlines=True
        )
        return vm in result.stdout
    except Exception as e:
        logger.error(e)
        return False

def is_ct(ct):
    try:
        result = subprocess.run(
            ["prlctl", "list", ct],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            check=True,
            universal_newlines=True
        )
        return result.stdout.split('\n')[1].split()[-2] == "CT"
    except subprocess.CalledProcessError:
        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=subprocess.run(
            ["prlctl", "create", vm_name, "-d", os_template],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            check=False,
            universal_newlines=True
        )
        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


def get_hw_list(ct):
    try:
        result = subprocess.run(
            ["prlctl", "list", "--full", "-i", ct],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            check=True,
            universal_newlines=True
        )
        if "Hardware:" in result.stdout:
            hw_list= result.stdout.split("Hardware:\n")[1].split("\nFeatures:")[0].replace("  ","").split("\n")
            logger.debug("CT RAW HW list %s:\n\t\t%s" % (ct,"\n\t\t".join(hw_list)))
            dev_list={"disk":{}, "net": {}}
            for dev_line in hw_list:
                if dev_line.startswith("memory "):
                    hotplug="hotplug" in dev_line
                    size=dev_line.split(" ")[1]
                    dev_list["memory"]={"size": size, "hotplug": hotplug}
                elif dev_line.startswith("cpu "):
                    dev=dict()
                    for conf in dev_line.replace("cpu ","").split(" "):
                        if "=" in conf:
                            conf_parsed=conf.split("=")
                            dev[conf_parsed[0]]=conf_parsed[1]
                        else:
                            dev[conf]=True
                    dev_list["cpu"]=dev
                elif dev_line.startswith("video "):
                    dev_list["video"]=dev_line.replace("video ","")
                elif dev_line.startswith("memory_guarantee "):
                    dev_list["memory_guarantee"]=dev_line.replace("memory_guarantee ","")
                elif dev_line.startswith("hdd"):
                    dev_parsed=dev_line.split(" ")
                    name=dev_parsed[0]
                    dev=dict()
                    for conf in dev_parsed:
                        if conf.startswith("scsi:"):
                            dev["scsi"]=conf.replace("scsi:","")
                        elif conf.endswith("Mb"):
                            dev["size"]=conf.replace("Mb","")
                        elif "=" in conf:
                            conf_parsed=conf.split("=")
                            dev[conf_parsed[0]]=conf_parsed[1].replace("'","")
                    dev_list["disk"][name]=dev
                elif dev_line.startswith("net"):
                    dev_parsed=dev_line.split(" ")
                    name=dev_parsed[0]
                    dev=dict()
                    for conf in dev_parsed:
                        if "=" in conf:
                            conf_parsed=conf.split("=")
                            dev[conf_parsed[0]]=conf_parsed[1].replace("'","")
                    dev_list["net"][name]=dev
                elif dev_line.startswith("venet"):
                    dev_parsed=dev_line.split(" ")
                    name=dev_parsed[0]
                    dev=dict()
                    for conf in dev_parsed:
                        if "=" in conf:
                            conf_parsed=conf.split("=")
                            dev[conf_parsed[0]]=conf_parsed[1].replace("'","")
                    if 'type' in dev.keys():
                        if dev['type']=='routed' and 'ips' in dev:
                            dev_list["net"][name]=dev
            logger.debug("Detected following HW list: %s" % dev_list)
            return dev_list
        else:
            logger.error(f"No hardware list in prlctl -i output for CT '{ct}'")
            raise ValueError(f"No hardware list in prlctl -i output for CT '{ct}'")
    except subprocess.CalledProcessError as e:
        logger.error(f"Failed to get hardware list for container '{ct}': {e}")
        raise ValueError(f"Failed to get hardware list for container '{ct}': {e}")
    
    
def get_ct_disks(ct):
    hw_list=get_hw_list(ct)
    disk_list={}
    for dev_line in hw_list:
        if dev_line.startswith("hdd"):
            dev_parsed=dev_line.split(" ")
            name=dev_parsed[0]
            dev=dict()
            for conf in dev_parsed:
                if conf.startswith("scsi:"):
                    dev["scsi"]=conf.replace("scsi:","")
                elif "=" in conf:
                    conf_parsed=conf.split("=")
                    dev[conf_parsed[0]]=conf_parsed[1]
            disk_list[name]=dev
    if len(dev):
        logger.debug("CT %s Disks list : %s" % (ct,str(disk_list)))
        return disk_list
    else:
        logger.error(f"Detected no disks for CT '{ct}'")
        raise ValueError(f"Detected no disks for CT '{ct}'")


def run_prl2qcow(tempdir, rootdisk=True, swapsize=2048) -> None:
    logger.debug("Starting prl2qcow for %s" % tempdir)
    swap=str(1025+swapsize)+"M"
    if rootdisk:
        command = ["prl2qcow", "-s", swap, "--meta", os.path.join(tempdir,"root.hds.meta"), os.path.join(tempdir,"root.hds")]
    else:
        command = ["prl2qcow", "--meta", os.path.join(tempdir,"root.hds.meta"), os.path.join(tempdir,"root.hds")]
    logger.debug("Executing prl2cow with optionset %s" % command[1:])
    try:
        result=subprocess.run(
            command,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            check=True,
            universal_newlines=True
        )
        logger.debug("prl2qcow output: " + ("\n"+result.stdout).replace("\n","\n\t\t"))
        if not result.returncode:
            logger.debug("Converted %s to QCOW" % tempdir)
            return os.path.join(tempdir,"root.hds")
        else:
            logger.error(f"Failed to convert disk '{tempdir}': Returncode is {result.returncode}")
            raise ValueError(f"Failed to convert disk '{tempdir}': Returncode is {result.returncode}")
    except subprocess.CalledProcessError as e:
        logger.error(f"Failed prl2qcow '{tempdir}': {e}")
        raise ValueError(f"Failed prl2qcow '{tempdir}': {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,tempdir):
    try:
        subprocess.run(
            ["mkdir", "-p", tempdir],
            check=True,
            universal_newlines=True
        ) 
    except subprocess.CalledProcessError as e:
        logger.error(f"Failed to create temp dir '{ct}': {e}")
        raise
    disk_path=disk["image"]+"/root.hds"
    try:
        subprocess.run(
            ["cp", disk_path, tempdir],
            check=True,
            universal_newlines=True
        )
        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:
        subprocess.run(
            ["qemu-nbd", "-c", "/dev/nbd0", vm_disk],
            check=True,
            universal_newlines=True
        )
        logger.debug(f"Connected {vm_disk} to /dev/nbd0 successfully.")
    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 = subprocess.run(
            ["lsblk"],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            universal_newlines=True
        )
        if "osprober" not in result.stdout:
            break
        attempt+=1
        logger.warning("Waiting osprober to exit gracefully (Attempt #%s)" % attempt)
        if (attempt % 3) == 0:
            result = subprocess.run(
               ["dmsetup", "remove", "-f", "osprober-linux-nbd0p1"],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                universal_newlines=True
            )
        time.sleep(5)
    try:
        result=subprocess.run(["qemu-nbd", "-d", "/dev/nbd0"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
        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 = subprocess.run(
            ["lsblk"],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            universal_newlines=True
        )
        if "nbd0" not in result.stdout:
            break
        logger.warning("Waiting nbd0 to be dismounted")
        time.sleep(5)


def initialize_partition_table() -> None:
    while not os.path.exists("/dev/nbd0"):
        logger.warning("/dev/nbd0 not found, waiting...")
        time.sleep(5)
    try:
        result = subprocess.run(
            ["sgdisk", "-o", "/dev/nbd0"],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            universal_newlines=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 = subprocess.run(
            command,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            check=True,
            universal_newlines=True
        )
        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=True)
            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) -> 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(["mount", "/dev/nbd0p4", root_mntdir], check=True)
        cmd_run(["mkdir", "-p", root_boot_mntdir], check=True)
        cmd_run(["mkdir", "-p", boot_mntdir], check=True)
        cmd_run(["mount", "/dev/nbd0p2", boot_mntdir], check=True)
        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))
        cmd_run(["umount", boot_mntdir], check=True)
        cmd_run(["mount", "/dev/nbd0p2", root_boot_mntdir], check=True)

        create_fstab(root_mntdir)

        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"})
        cmd_run(["umount", "-R", root_mntdir], False)
        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
    finally:
        logger.debug("Partitions unmounted successfully.")


def get_partition_uuid(partition: str) -> str:
    try:
        result = subprocess.run(["blkid", "-o", "value", "-s", "UUID", partition], stdout=subprocess.PIPE,
                                stderr=subprocess.PIPE, universal_newlines=True)
        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) -> 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(("%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 update_fstab(mntdir, extra_mounts) -> None:
    root_mntdir=os.path.join(mntdir,"root")
    logger.debug("Adding extra mounts: %s" % extra_mounts)
    fstab_add=""
    for mount in extra_mounts:
        fstab_add+=f"UUID={extra_mounts[mount]}    {mount}    ext4    defaults        1 1\n"
    fstab_add+=f"\n"
    logger.debug(("Extra mountpoints to be added: \n%s" % fstab_add).replace("\n","\n\t\t"))
    try:
        while True:
            result=subprocess.run(["mount", "/dev/nbd0p4", root_mntdir], check=True, universal_newlines=True)
            if result.returncode ==0:
                break
            logger.warning("Waiting nbd partition to be mounted")
            time.sleep(5)
        with open(os.path.join(root_mntdir,"etc", "fstab"), "a") as fstab_file:
            fstab_file.write(fstab_add)
        with open(os.path.join(root_mntdir,"etc", "fstab"), "r") as fstab_file:
            logger.debug(("Reading updated fstab: \n %s" % fstab_file.readlines()).replace("\n","\n\t\t"))
        logger.debug("/etc/fstab updated successfully.")
        subprocess.run(["umount", "-R", root_mntdir], check=True, universal_newlines=True)
    except Exception as e:
        subprocess.run(["lsblk"], check=True, universal_newlines=True)
        logger.error(f"Error updatting /etc/fstab: {e}")
        return



def unmount_partitions(mntdir) -> None:
    try:
        subprocess.run(["umount", "-R", os.path.join(mntdir,"root")], check=True, universal_newlines=True)
        logger.debug("Partitions unmounted successfully.")
    except subprocess.CalledProcessError as e:
        logger.error(f"Failed to unmount partitions: {e}")
        raise

def clear_networking(ct, networks):
    for net in networks:
        cmd_run(["prlctl", "set", ct,"--device-del",net],True)

def add_networking_to_vm(vm_name, networks):
    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:
        subprocess.run(
            ["prlctl", "set", vm_name,"--device-del","net0"],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            check=True,
            universal_newlines=True
        )
        for net in networks:
            logger.debug("Adding the %s with config: %s" % (net, networks[net]))
            command=["prlctl", "set", vm_name,"--device-add","net"]
            if "network" in networks[net]:
                command.append("--network")
                command.append(networks[net]["network"])
            else:
                command.append("--type")
                command.append("routed")
            for value in networks[net]:
                if value in commands:
                    command.append(commands[value])
                    command.append(networks[net][value])
            logger.debug("Command keys: %s" % command[3:])
            subprocess.run(
                command,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                check=True,
                universal_newlines=True
            )
    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, cpu):
    command=["prlctl", "set", vm_name]
    if "sockets" in cpu:
        command.append("--cpu-sockets")
        command.append(cpu["sockets"])
    if "cpus" in cpu:
        command.append("--cpus")
        if cpu["cpus"]=="unlimited":
            command.append("2")
        else:
            command.append(cpu["cpus"])
    try:
        subprocess.run(
            command,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            check=True,
            universal_newlines=True
            )
        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")
        command.append(hw["memory_guarantee"])
    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:
        subprocess.run(
            command,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            check=True,
            universal_newlines=True
            )
        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, disks):
    try:
        subprocess.run(
            ["prlctl", "set", vm_name,"--device-del","hdd0"],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            check=True,
            universal_newlines=True
        )
        subprocess.run(
            ["prlctl", "set", vm_name,"--device-del","cdrom0"],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            check=True,
            universal_newlines=True
        )
        result=subprocess.run(
            ["prlctl", "list", "-i", vm_name],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            check=True,
            universal_newlines=True
        )
        home_dir=None
        for line in result.stdout.split('\n'):
            if "Home:" in line:
                home_dir=line.split()[1]
        for disk in disks:
            if home_dir:
                shutil.move(disks[disk]["vm_source"],os.path.join(home_dir,disk+".hds"))
                shutil.move(disks[disk]["vm_source"]+".meta",os.path.join(home_dir,disk+".hds.meta"))
                diskimage=os.path.join(home_dir,disk+".hds")
            else:
                diskimage=disks[disk]["vm_source"]
            result = subprocess.run(
                ["prlctl", "set", vm_name,"--device-add","hdd","--image",diskimage],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                check=True,
                universal_newlines=True
            )
            logger.debug("Added disk %s with image %s" % (disk,diskimage))
    except:
        raise

def getvm_uuid(vmname):
    uuid="Unknown"
    result=cmd_run(["prlctl","list","-i",vmname],True)
    for line in result.stdout.split('\n'):
            if "ID:" in line:
                uuid=line.split()[1].replace("{","").replace("}","").replace(" ","")
    return uuid
   

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 validate_ct(ct):
    if not ct_exists(ct):
        logger.error(f"Container '{ct}' does not exist.")
        return False
    elif not is_ct(ct):
        logger.error(f"'{ct}' is not container. Please verify the name or CTID are correct.")
        return False
    return True

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 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"
        hw_list={"net":[],"disk":[]}
        
        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_hw_list(ct)
        vm_distro = get_ct_distro(ct)
        

        ST=2
        logger.info("Checking disks consistency")
        stop_venv(ct)
        for disk in hw_list["disk"]:
            ploop_check(hw_list["disk"][disk]["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["net"])
        stop_venv(ct)

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

        ST=5
        logger.info("Converting disks in container")
        extra_mounts={}
        for disk in hw_list["disk"]:
            rootdisk=hw_list["disk"][disk]["mnt"]=="/"
            create_ct_disks_copy(hw_list["disk"][disk],os.path.join(tempdir,ct,disk))
            vm_disk=run_prl2qcow(os.path.join(tempdir,ct,disk),rootdisk,swap)
            hw_list["disk"][disk]["vm_source"]=vm_disk
            connect_nbd(vm_disk)
            initialize_partition_table()
            create_partitions(rootdisk,swap)
            create_filesystems(rootdisk)
            if rootdisk:
                root_disk=disk
                configure_grub(os.path.join(tempdir,ct,"mnt"), vm_distro)
            else:
                extra_mounts[hw_list["disk"][disk]["mnt"]]=get_partition_uuid("/dev/nbd0p1")
            disconnect_nbd()

        ST=6
        logger.info("Connecting disks to VM")
        if extra_mounts:
            logger.warning("Adding extra disks to fstab on %s: %s" % (root_disk, extra_mounts))
            connect_nbd(hw_list["disk"][root_disk]["vm_source"])
            update_fstab(os.path.join(tempdir,ct,"mnt"), extra_mounts)
            disconnect_nbd()
        else:
            logger.info("No extra disks to add to fstab")
        add_disks_to_vm(vm_name,hw_list["disk"])

        ST=7
        logger.info("Configuring network")
        add_networking_to_vm(vm_name,hw_list["net"])

        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")
        shutil.rmtree(os.path.join(tempdir,ct))
        stop_venv(ct)
        stop_venv(vm_name)
        add_data_to_cep(ct, True, None, 0, vm_distro, hw_list, starttime)
        cmd_run(["prlctl","set",ct,"--name","C2V_"+ct])
        cmd_run(["prlctl","set",vm_name,"--name",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))
        disconnect_nbd()
        return True
    except subprocess.TimeoutExpired:
        logger.error("%s conversion timed out." % ct)
        add_data_to_cep(ct, False, "Timeout",ST, vm_distro, hw_list, starttime)
        signal.alarm(0)
        cmd_run(["umount", "-R", os.path.join(tempdir,ct,"mnt","root")], False)
        disconnect_nbd()
        ST=-1
        return False
    except Exception as e:
        import traceback
        logger.error("Failed to convert CT %s. Cleaning up." % ct)
        add_data_to_cep(ct, False, traceback.format_exc(), ST, vm_distro, hw_list, starttime)
        ST=-1
        signal.alarm(0)
        cmd_run(["umount", "-R", os.path.join(tempdir,ct,"mnt","root")], False)
        disconnect_nbd()
        return False



def is_valid_uuid(uuid_to_test, version=4):
    from uuid import UUID
    try:
        uuid_obj = UUID(uuid_to_test, version=version)
    except ValueError:
        return False
    return True
    
def validate_name(ct):
    if is_valid_uuid(ct):
        logger.debug("UUID provided, receiving CT name: %s" % ct)
        try:
            result=subprocess.run(["prlctl", "list", ct], stdout=subprocess.PIPE, stderr=subprocess.PIPE,check=True, universal_newlines=True)
            if '\n' in result.stdout:
                ret=result.stdout.split('\n')[1].split()[4]
                logger.debug("Got CT name: %s" % ret)
            else:
                ret=ct.split("-")[0]
                logger.warning("Failed to get CT name using: %s" % ret)
            return ret
        except:
            ret=ct.split("-")[0]
            logger.error("Failed to get CT name using: %s" % ret)
            return ret
    else:
        return ct

def get_ct_config(ct_id):
    ret=cmd_run(["prlctl","list","-j","-i",ct_id])
    config=json.loads(ret.stdout)
    return config[0]

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, ct_os, 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 disk in hw_list["disk"]:
        disks.append({"name": disk, "size": hw_list["disk"][disk]["size"]})
    for nic in hw_list["net"]:
        if "network" in hw_list["net"][nic]:
            nics.append({"name": nic, "network": hw_list["net"][nic]["network"]})
        else:
            nics.append({"name": nic, "type": "routed"})

    data={
        "ct_id": getvm_uuid(ct_id),
        "date": str(datetime.datetime.now()),
        "success": success,
        "reason": reason, 
        "fail_step": fail_step,
        "ct_os": ct_os,
        "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("-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:
        log_level=logging.DEBUG
    elif args.quiet and not args.log:
        log_level=logging.ERROR
    else:
        log_level=logging.INFO

    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)
            return 98
    
    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.verbose and args.quiet:
        logger.error("Can't execute both Verbose and Quiet flags")
        parser.print_help()
        return 99
    if (args.timeout > 360) or (args.timeout < 5):
        logger.error("Timeout value is out of alowed interval")
        parser.print_help()
        return 99


    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()
        return 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:
        return 230
    
    try:
        create_temp_dir(tempdir)
    except:
        return 231
    if ct:
        newct=[]
        for c in ct:
            newct.append(cmd_run(["prlctl", "list", c],False).stdout.split("\n")[1].split()[4])
        ct=newct
    if not ct:
        logger.error("No CT names were provided")
        parser.print_help()
        return 252
    elif len(ct)==1:
        if validate_ct(ct[0]):
            if not args.quiet and not args.yes:
                if not prompt("The conversion process will restart the container multiple times and may take up to %s minutes to complete.\nProceed? (y/n)\n" % (args.timeout)):
                    return 1
            if get_ct_state(ct[0]) not in ["running", "stopped", "paused", "suspended", "mounted"]:
                logger.error("Container state inconsistent. Exiting.")
            convert_ct(ct[0],swap)

    else:
        exclusion_list={}
        converted_list=[]
        failed_list=[]
        logger.info("Starting batch conversion of: %s" % ", ".join(ct))
        logger.info("Validating the list of CTs")
        newcts=[]
        for c in ct:
            if c in ["va-mn", "vstorage-ui", "va-pp"]:
                exclusion_list[c]="not required"
            elif not validate_ct(c):
                exclusion_list[c]="not a container"
            elif get_ct_state(c) not in ["running", "stopped", "paused", "suspended"]:
                exclusion_list[c]="container state improper (%s)" % get_ct_state(c)
            elif c not in newcts:
                newcts.append(c)
            else:
                logger.warning("Dropping duplicate entry from list %s" % c)
        ct=newcts
        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("Container list is empty. Exiting." )
            return 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)):
                return 1
        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)
            CT=-1
        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 __name__ == "__main__":
    main()

