#!/usr/bin/env python3

# provisioner.py - PVC CLI client function library, Provisioner functions
# Part of the Parallel Virtual Cluster (PVC) system
#
#    Copyright (C) 2018-2020 Joshua M. Boniface <joshua@boniface.me>
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <https://www.gnu.org/licenses/>.
#
###############################################################################

import time
import re
import subprocess
import ast

import cli_lib.ansiprint as ansiprint
from cli_lib.common import call_api

#
# Primary functions
#
def template_info(config, template, template_type):
    """
    Get information about template

    API endpoint: GET /api/v1/provisioner/template/{template_type}/{template}
    API arguments:
    API schema: {json_template_object}
    """
    response = call_api(config, 'get', '/provisioner/template/{template_type}/{template}'.format(template_type=template_type, template=template))

    if response.status_code == 200:
        return True, response.json()
    else:
        return False, response.json()['message']

def template_list(config, limit, template_type=None):
    """
    Get list information about templates (limited by {limit})

    API endpoint: GET /api/v1/provisioner/template/{template_type}
    API arguments: limit={limit}
    API schema: [{json_template_object},{json_template_object},etc.]
    """
    params = dict()
    if limit:
        params['limit'] = limit

    if template_type is not None:
        response = call_api(config, 'get', '/provisioner/template/{template_type}'.format(template_type=template_type), params=params)
    else:
        response = call_api(config, 'get', '/provisioner/template', params=params)

    if response.status_code == 200:
        return True, response.json()
    else:
        return False, response.json()['message']

def template_add(config, params, template_type=None):
    """
    Add a new template of {template_type} with {params}

    API endpoint: POST /api/v1/provisioner/template/{template_type}
    API_arguments: args
    API schema: {message}
    """
    response = call_api(config, 'post', '/provisioner/template/{template_type}'.format(template_type=template_type), params=params)

    if response.status_code == 200:
        retvalue = True
    else:
        retvalue = False
        
    return retvalue, response.json()['message']

def template_remove(config, name, template_type=None):
    """
    Remove template {name} of {template_type}

    API endpoint: DELETE /api/v1/provisioner/template/{template_type}/{name}
    API_arguments:
    API schema: {message}
    """
    response = call_api(config, 'delete', '/provisioner/template/{template_type}/{name}'.format(template_type=template_type, name=name))

    if response.status_code == 200:
        retvalue = True
    else:
        retvalue = False
        
    return retvalue, response.json()['message']

def template_element_add(config, name, element_id, params, element_type=None, template_type=None):
    """
    Add a new template element of {element_type} with {params} to template {name} of {template_type}

    API endpoint: POST /api/v1/provisioner/template/{template_type}/{name}/{element_type}/{element_id}
    API_arguments: args
    API schema: {message}
    """
    response = call_api(config, 'post', '/provisioner/template/{template_type}/{name}/{element_type}/{element_id}'.format(template_type=template_type, name=name, element_type=element_type, element_id=element_id), params=params)

    if response.status_code == 200:
        retvalue = True
    else:
        retvalue = False
        
    return retvalue, response.json()['message']

def template_element_remove(config, name, element_id, element_type=None, template_type=None):
    """
    Remove template element {element_id} of {element_type} from template {name} of {template_type}

    API endpoint: DELETE /api/v1/provisioner/template/{template_type}/{name}/{element_type}/{element_id}
    API_arguments:
    API schema: {message}
    """
    response = call_api(config, 'delete', '/provisioner/template/{template_type}/{name}/{element_type}/{element_id}'.format(template_type=template_type, name=name, element_type=element_type, element_id=element_id))

    if response.status_code == 200:
        retvalue = True
    else:
        retvalue = False
        
    return retvalue, response.json()['message']

def userdata_info(config, userdata):
    """
    Get information about userdata

    API endpoint: GET /api/v1/provisioner/userdata/{userdata}
    API arguments:
    API schema: {json_data_object}
    """
    response = call_api(config, 'get', '/provisioner/userdata/{userdata}'.format(userdata=userdata))

    if response.status_code == 200:
        return True, response.json()[0]
    else:
        return False, response.json()['message']

def userdata_list(config, limit):
    """
    Get list information about userdatas (limited by {limit})

    API endpoint: GET /api/v1/provisioner/userdata
    API arguments: limit={limit}
    API schema: [{json_data_object},{json_data_object},etc.]
    """
    params = dict()
    if limit:
        params['limit'] = limit

    response = call_api(config, 'get', '/provisioner/userdata', params=params)

    if response.status_code == 200:
        return True, response.json()
    else:
        return False, response.json()['message']

def userdata_add(config, params):
    """
    Add a new userdata with {params}

    API endpoint: POST /api/v1/provisioner/userdata
    API_arguments: args
    API schema: {message}
    """
    name = params.get('name')
    userdata_data = params.get('data')

    params = {
        'name': name
    }
    data = {
        'data': userdata_data
    }
    response = call_api(config, 'post', '/provisioner/userdata', params=params, data=data)

    if response.status_code == 200:
        retvalue = True
    else:
        retvalue = False
        
    return retvalue, response.json()['message']

def userdata_modify(config, name, params):
    """
    Modify userdata {name} with {params}

    API endpoint: PUT /api/v1/provisioner/userdata/{name}
    API_arguments: args
    API schema: {message}
    """
    userdata_data = params.get('data')

    params = {
        'name': name
    }
    data = {
        'data': userdata_data
    }
    response = call_api(config, 'put', '/provisioner/userdata/{name}'.format(name=name), params=params, data=data)

    if response.status_code == 200:
        retvalue = True
    else:
        retvalue = False
        
    return retvalue, response.json()['message']

def userdata_remove(config, name):
    """
    Remove userdata {name}

    API endpoint: DELETE /api/v1/provisioner/userdata/{name}
    API_arguments:
    API schema: {message}
    """
    response = call_api(config, 'delete', '/provisioner/userdata/{name}'.format(name=name))

    if response.status_code == 200:
        retvalue = True
    else:
        retvalue = False
        
    return retvalue, response.json()['message']

def script_info(config, script):
    """
    Get information about script

    API endpoint: GET /api/v1/provisioner/script/{script}
    API arguments:
    API schema: {json_data_object}
    """
    response = call_api(config, 'get', '/provisioner/script/{script}'.format(script=script))

    if response.status_code == 200:
        return True, response.json()[0]
    else:
        return False, response.json()['message']

def script_list(config, limit):
    """
    Get list information about scripts (limited by {limit})

    API endpoint: GET /api/v1/provisioner/script
    API arguments: limit={limit}
    API schema: [{json_data_object},{json_data_object},etc.]
    """
    params = dict()
    if limit:
        params['limit'] = limit

    response = call_api(config, 'get', '/provisioner/script', params=params)

    if response.status_code == 200:
        return True, response.json()
    else:
        return False, response.json()['message']

def script_add(config, params):
    """
    Add a new script with {params}

    API endpoint: POST /api/v1/provisioner/script
    API_arguments: args
    API schema: {message}
    """
    name = params.get('name')
    script_data = params.get('data')

    params = {
        'name': name
    }
    data = {
        'data': script_data
    }
    response = call_api(config, 'post', '/provisioner/script', params=params, data=data)

    if response.status_code == 200:
        retvalue = True
    else:
        retvalue = False
        
    return retvalue, response.json()['message']

def script_modify(config, name, params):
    """
    Modify script {name} with {params}

    API endpoint: PUT /api/v1/provisioner/script/{name}
    API_arguments: args
    API schema: {message}
    """
    script_data = params.get('data')

    params = {
        'name': name
    }
    data = {
        'data': script_data
    }
    response = call_api(config, 'put', '/provisioner/script/{name}'.format(name=name), params=params, data=data)

    if response.status_code == 200:
        retvalue = True
    else:
        retvalue = False
        
    return retvalue, response.json()['message']

def script_remove(config, name):
    """
    Remove script {name}

    API endpoint: DELETE /api/v1/provisioner/script/{name}
    API_arguments:
    API schema: {message}
    """
    response = call_api(config, 'delete', '/provisioner/script/{name}'.format(name=name))

    if response.status_code == 200:
        retvalue = True
    else:
        retvalue = False
        
    return retvalue, response.json()['message']

def ova_info(config, name):
    """
    Get information about OVA image {name}

    API endpoint: GET /api/v1/provisioner/ova/{name}
    API arguments:
    API schema: {json_data_object}
    """
    response = call_api(config, 'get', '/provisioner/ova/{name}'.format(name=name))

    if response.status_code == 200:
        return True, response.json()[0]
    else:
        return False, response.json()['message']

def ova_list(config, limit):
    """
    Get list information about OVA images (limited by {limit})

    API endpoint: GET /api/v1/provisioner/ova
    API arguments: limit={limit}
    API schema: [{json_data_object},{json_data_object},etc.]
    """
    params = dict()
    if limit:
        params['limit'] = limit

    response = call_api(config, 'get', '/provisioner/ova', params=params)

    if response.status_code == 200:
        return True, response.json()
    else:
        return False, response.json()['message']

def ova_upload(config, name, ova_file, params):
    """
    Upload an OVA image to the cluster

    API endpoint: POST /api/v1/provisioner/ova/{name}
    API arguments: pool={pool}, ova_size={ova_size}
    API schema: {"message":"{data}"}
    """
    files = {
        'file': open(ova_file,'rb')
    }
    response = call_api(config, 'post', '/provisioner/ova/{}'.format(name), params=params, files=files)

    if response.status_code == 200:
        retstatus = True
    else:
        retstatus = False

    return retstatus, response.json()['message']

def ova_remove(config, name):
    """
    Remove OVA image {name}

    API endpoint: DELETE /api/v1/provisioner/ova/{name}
    API_arguments:
    API schema: {message}
    """
    response = call_api(config, 'delete', '/provisioner/ova/{name}'.format(name=name))

    if response.status_code == 200:
        retvalue = True
    else:
        retvalue = False
        
    return retvalue, response.json()['message']

def profile_info(config, profile):
    """
    Get information about profile

    API endpoint: GET /api/v1/provisioner/profile/{profile}
    API arguments:
    API schema: {json_data_object}
    """
    response = call_api(config, 'get', '/provisioner/profile/{profile}'.format(profile=profile))

    if response.status_code == 200:
        return True, response.json()[0]
    else:
        return False, response.json()['message']

def profile_list(config, limit):
    """
    Get list information about profiles (limited by {limit})

    API endpoint: GET /api/v1/provisioner/profile/{profile_type}
    API arguments: limit={limit}
    API schema: [{json_data_object},{json_data_object},etc.]
    """
    params = dict()
    if limit:
        params['limit'] = limit

    response = call_api(config, 'get', '/provisioner/profile', params=params)

    if response.status_code == 200:
        return True, response.json()
    else:
        return False, response.json()['message']

def profile_add(config, params):
    """
    Add a new profile with {params}

    API endpoint: POST /api/v1/provisioner/profile
    API_arguments: args
    API schema: {message}
    """
    response = call_api(config, 'post', '/provisioner/profile', params=params)

    if response.status_code == 200:
        retvalue = True
    else:
        retvalue = False
        
    return retvalue, response.json()['message']

def profile_modify(config, name, params):
    """
    Modify profile {name} with {params}

    API endpoint: PUT /api/v1/provisioner/profile/{name}
    API_arguments: args
    API schema: {message}
    """
    response = call_api(config, 'put', '/provisioner/profile/{name}'.format(name=name), params=params)

    if response.status_code == 200:
        retvalue = True
    else:
        retvalue = False
        
    return retvalue, response.json()['message']

def profile_remove(config, name):
    """
    Remove profile {name}

    API endpoint: DELETE /api/v1/provisioner/profile/{name}
    API_arguments:
    API schema: {message}
    """
    response = call_api(config, 'delete', '/provisioner/profile/{name}'.format(name=name))

    if response.status_code == 200:
        retvalue = True
    else:
        retvalue = False
        
    return retvalue, response.json()['message']

def vm_create(config, name, profile, wait_flag, define_flag, start_flag):
    """
    Create a new VM named {name} with profile {profile}

    API endpoint: POST /api/v1/provisioner/create
    API_arguments: name={name}, profile={profile}
    API schema: {message}
    """
    params = {
        'name': name,
        'profile': profile,
        'start_vm': start_flag,
        'define_vm': define_flag
    }
    response = call_api(config, 'post', '/provisioner/create', params=params)

    if response.status_code == 202:
        retvalue = True
        if not wait_flag:
            retdata = 'Task ID: {}'.format(response.json()['task_id'])
        else:
            # Just return the task_id raw, instead of formatting it
            retdata = response.json()['task_id']
    else:
        retvalue = False
        retdata = response.json()['message']
        
    return retvalue, retdata

def task_status(config, task_id=None, is_watching=False):
    """
    Get information about provisioner job {task_id} or all tasks if None

    API endpoint: GET /api/v1/provisioner/status
    API arguments:
    API schema: {json_data_object}
    """
    if task_id is not None:
        response = call_api(config, 'get', '/provisioner/status/{task_id}'.format(task_id=task_id))
    else:
        response = call_api(config, 'get', '/provisioner/status')

    if task_id is not None:
        if response.status_code == 200:
            retvalue = True
            respjson = response.json()

            if is_watching:
                # Just return the raw JSON to the watching process instead of formatting it
                return respjson

            job_state = respjson['state']
            if job_state == 'RUNNING':
                retdata = 'Job state: RUNNING\nStage: {}/{}\nStatus: {}'.format(
                    respjson['current'],
                    respjson['total'],
                    respjson['status']
                )
            elif job_state == 'FAILED':
                retdata = 'Job state: FAILED\nStatus: {}'.format(
                    respjson['status']
                )
            elif job_state == 'COMPLETED':
                retdata = 'Job state: COMPLETED\nStatus: {}'.format(
                    respjson['status']
                )
            else:
                retdata = 'Job state: {}\nStatus: {}'.format(
                    respjson['state'],
                    respjson['status']
                )
        else:
            retvalue = False
            retdata = response.json()['message']
    else:
        retvalue = True
        task_data_raw = response.json()
        # Format the Celery data into a more useful data structure
        task_data = list()
        for task_type in ['active', 'reserved', 'scheduled']:
            type_data = task_data_raw[task_type]
            if not type_data:
                type_data = dict()
            for task_host in type_data:
                for task_job in task_data_raw[task_type][task_host]:
                    task = dict()
                    if task_type == 'reserved':
                        task['type'] = 'pending'
                    else:
                        task['type'] = task_type
                    task['worker'] = task_host
                    task['id'] = task_job.get('id')
                    task_args = ast.literal_eval(task_job.get('args'))
                    task['vm_name'] = task_args[0]
                    task['vm_profile'] = task_args[1]
                    task_kwargs = ast.literal_eval(task_job.get('kwargs'))
                    task['vm_define'] = str(bool(task_kwargs['define_vm']))
                    task['vm_start'] = str(bool(task_kwargs['start_vm']))
                    task_data.append(task)
        retdata = task_data

    return retvalue, retdata

#
# Format functions
#
def format_list_template(template_data, template_type=None):
    """
    Format the returned template template

    template_type can be used to only display part of the full list, allowing function
    reuse with more limited output options.
    """
    template_types = [ 'system', 'network', 'storage' ]
    normalized_template_data = dict()
    ainformation = list()

    if template_type in template_types:
        template_types = [ template_type ]
        template_data_type = '{}_templates'.format(template_type)
        normalized_template_data[template_data_type] = template_data
    else:
        normalized_template_data = template_data

    if 'system' in template_types:
        ainformation.append('System templates:')
        ainformation.append('')
        ainformation.append(format_list_template_system(normalized_template_data['system_templates']))
        if len(template_types) > 1:
            ainformation.append('')

    if 'network' in template_types:
        ainformation.append('Network templates:')
        ainformation.append('')
        ainformation.append(format_list_template_network(normalized_template_data['network_templates']))
        if len(template_types) > 1:
            ainformation.append('')

    if 'storage' in template_types:
        ainformation.append('Storage templates:')
        ainformation.append('')
        ainformation.append(format_list_template_storage(normalized_template_data['storage_templates']))

    return '\n'.join(ainformation)

def format_list_template_system(template_data):
    if isinstance(template_data, dict):
        template_data = [ template_data ]

    template_list_output = []

    # Determine optimal column widths
    template_name_length = 5
    template_id_length = 3
    template_vcpu_length = 6
    template_vram_length = 10
    template_serial_length = 7
    template_vnc_length = 4
    template_vnc_bind_length = 10
    template_node_limit_length = 9
    template_node_selector_length = 11
    template_node_autostart_length = 11

    for template in template_data:
        # template_name column
        _template_name_length = len(str(template['name'])) + 1
        if _template_name_length > template_name_length:
            template_name_length = _template_name_length
        # template_id column
        _template_id_length = len(str(template['id'])) + 1
        if _template_id_length > template_id_length:
            template_id_length = _template_id_length
        # template_vcpu column
        _template_vcpu_length = len(str(template['vcpu_count'])) + 1
        if _template_vcpu_length > template_vcpu_length:
            template_vcpu_length = _template_vcpu_length
        # template_vram column
        _template_vram_length = len(str(template['vram_mb'])) + 1
        if _template_vram_length > template_vram_length:
            template_vram_length = _template_vram_length
        # template_serial column
        _template_serial_length = len(str(template['serial'])) + 1
        if _template_serial_length > template_serial_length:
            template_serial_length = _template_serial_length
        # template_vnc column
        _template_vnc_length = len(str(template['vnc'])) + 1
        if _template_vnc_length > template_vnc_length:
            template_vnc_length = _template_vnc_length
        # template_vnc_bind column
        _template_vnc_bind_length = len(str(template['vnc_bind'])) + 1
        if _template_vnc_bind_length > template_vnc_bind_length:
            template_vnc_bind_length = _template_vnc_bind_length
        # template_node_limit column
        _template_node_limit_length = len(str(template['node_limit'])) + 1
        if _template_node_limit_length > template_node_limit_length:
            template_node_limit_length = _template_node_limit_length
        # template_node_selector column
        _template_node_selector_length = len(str(template['node_selector'])) + 1
        if _template_node_selector_length > template_node_selector_length:
            template_node_selector_length = _template_node_selector_length
        # template_node_autostart column
        _template_node_autostart_length = len(str(template['node_autostart'])) + 1
        if _template_node_autostart_length > template_node_autostart_length:
            template_node_autostart_length = _template_node_autostart_length

    # Format the string (header)
    template_list_output_header = '{bold}{template_name: <{template_name_length}} {template_id: <{template_id_length}} \
{template_vcpu: <{template_vcpu_length}} \
{template_vram: <{template_vram_length}} \
Consoles: {template_serial: <{template_serial_length}} \
{template_vnc: <{template_vnc_length}} \
{template_vnc_bind: <{template_vnc_bind_length}} \
Metadata: {template_node_limit: <{template_node_limit_length}} \
{template_node_selector: <{template_node_selector_length}} \
{template_node_autostart: <{template_node_autostart_length}}{end_bold}'.format(
            template_name_length=template_name_length,
            template_id_length=template_id_length,
            template_vcpu_length=template_vcpu_length,
            template_vram_length=template_vram_length,
            template_serial_length=template_serial_length,
            template_vnc_length=template_vnc_length,
            template_vnc_bind_length=template_vnc_bind_length,
            template_node_limit_length=template_node_limit_length,
            template_node_selector_length=template_node_selector_length,
            template_node_autostart_length=template_node_autostart_length,
            bold=ansiprint.bold(),
            end_bold=ansiprint.end(),
            template_state_colour='',
            end_colour='',
            template_name='Name',
            template_id='ID',
            template_vcpu='vCPUs',
            template_vram='vRAM [MB]',
            template_serial='Serial',
            template_vnc='VNC',
            template_vnc_bind='VNC bind',
            template_node_limit='Limit',
            template_node_selector='Selector',
            template_node_autostart='Autostart'
        )

    # Keep track of nets we found to be valid to cut down on duplicate API hits
    valid_net_list = []
    # Format the string (elements)

    for template in sorted(template_data, key=lambda i: i.get('name', None)):
        template_list_output.append(
            '{bold}{template_name: <{template_name_length}} {template_id: <{template_id_length}} \
{template_vcpu: <{template_vcpu_length}} \
{template_vram: <{template_vram_length}} \
          {template_serial: <{template_serial_length}} \
{template_vnc: <{template_vnc_length}} \
{template_vnc_bind: <{template_vnc_bind_length}} \
          {template_node_limit: <{template_node_limit_length}} \
{template_node_selector: <{template_node_selector_length}} \
{template_node_autostart: <{template_node_autostart_length}}{end_bold}'.format(
                template_name_length=template_name_length,
                template_id_length=template_id_length,
                template_vcpu_length=template_vcpu_length,
                template_vram_length=template_vram_length,
                template_serial_length=template_serial_length,
                template_vnc_length=template_vnc_length,
                template_vnc_bind_length=template_vnc_bind_length,
                template_node_limit_length=template_node_limit_length,
                template_node_selector_length=template_node_selector_length,
                template_node_autostart_length=template_node_autostart_length,
                bold='',
                end_bold='',
                template_name=str(template['name']),
                template_id=str(template['id']),
                template_vcpu=str(template['vcpu_count']),
                template_vram=str(template['vram_mb']),
                template_serial=str(template['serial']),
                template_vnc=str(template['vnc']),
                template_vnc_bind=str(template['vnc_bind']),
                template_node_limit=str(template['node_limit']),
                template_node_selector=str(template['node_selector']),
                template_node_autostart=str(template['node_autostart'])
            )
        )

    return '\n'.join([template_list_output_header] + template_list_output)

    return True, ''

def format_list_template_network(template_template):
    if isinstance(template_template, dict):
        template_template = [ template_template ]

    template_list_output = []

    # Determine optimal column widths
    template_name_length = 5
    template_id_length = 3
    template_mac_template_length = 13
    template_networks_length = 10

    for template in template_template:
        # Join the networks elements into a single list of VNIs
        network_list = list()
        for network in template['networks']:
            network_list.append(str(network['vni']))
        template['networks_csv'] = ','.join(network_list)

    for template in template_template:
        # template_name column
        _template_name_length = len(str(template['name'])) + 1
        if _template_name_length > template_name_length:
            template_name_length = _template_name_length
        # template_id column
        _template_id_length = len(str(template['id'])) + 1
        if _template_id_length > template_id_length:
            template_id_length = _template_id_length
        # template_mac_template column
        _template_mac_template_length = len(str(template['mac_template'])) + 1
        if _template_mac_template_length > template_mac_template_length:
            template_mac_template_length = _template_mac_template_length
        # template_networks column
        _template_networks_length = len(str(template['networks_csv'])) + 1
        if _template_networks_length > template_networks_length:
            template_networks_length = _template_networks_length

    # Format the string (header)
    template_list_output_header = '{bold}{template_name: <{template_name_length}} {template_id: <{template_id_length}} \
{template_mac_template: <{template_mac_template_length}} \
{template_networks: <{template_networks_length}}{end_bold}'.format(
            template_name_length=template_name_length,
            template_id_length=template_id_length,
            template_mac_template_length=template_mac_template_length,
            template_networks_length=template_networks_length,
            bold=ansiprint.bold(),
            end_bold=ansiprint.end(),
            template_name='Name',
            template_id='ID',
            template_mac_template='MAC template',
            template_networks='Network VNIs'
        )

    # Format the string (elements)
    for template in sorted(template_template, key=lambda i: i.get('name', None)):
        template_list_output.append(
            '{bold}{template_name: <{template_name_length}} {template_id: <{template_id_length}} \
{template_mac_template: <{template_mac_template_length}} \
{template_networks: <{template_networks_length}}{end_bold}'.format(
                template_name_length=template_name_length,
                template_id_length=template_id_length,
                template_mac_template_length=template_mac_template_length,
                template_networks_length=template_networks_length,
                bold='',
                end_bold='',
                template_name=str(template['name']),
                template_id=str(template['id']),
                template_mac_template=str(template['mac_template']),
                template_networks=str(template['networks_csv'])
            )
        )

    return '\n'.join([template_list_output_header] + template_list_output)

def format_list_template_storage(template_template):
    if isinstance(template_template, dict):
        template_template = [ template_template ]

    template_list_output = []

    # Determine optimal column widths
    template_name_length = 5
    template_id_length = 3
    template_disk_id_length = 8
    template_disk_pool_length = 8
    template_disk_source_length = 14
    template_disk_size_length = 10
    template_disk_filesystem_length = 11
    template_disk_fsargs_length = 10
    template_disk_mountpoint_length = 10

    for template in template_template:
        # template_name column
        _template_name_length = len(str(template['name'])) + 1
        if _template_name_length > template_name_length:
            template_name_length = _template_name_length
        # template_id column
        _template_id_length = len(str(template['id'])) + 1
        if _template_id_length > template_id_length:
            template_id_length = _template_id_length

        for disk in template['disks']:
            # template_disk_id column
            _template_disk_id_length = len(str(disk['disk_id'])) + 1
            if _template_disk_id_length > template_disk_id_length:
                template_disk_id_length = _template_disk_id_length
            # template_disk_pool column
            _template_disk_pool_length = len(str(disk['pool'])) + 1
            if _template_disk_pool_length > template_disk_pool_length:
                template_disk_pool_length = _template_disk_pool_length
            # template_disk_source column
            _template_disk_source_length = len(str(disk['source_volume'])) + 1
            if _template_disk_source_length > template_disk_source_length:
                template_disk_source_length = _template_disk_source_length
            # template_disk_size column
            _template_disk_size_length = len(str(disk['disk_size_gb'])) + 1
            if _template_disk_size_length > template_disk_size_length:
                template_disk_size_length = _template_disk_size_length
            # template_disk_filesystem column
            _template_disk_filesystem_length = len(str(disk['filesystem'])) + 1
            if _template_disk_filesystem_length > template_disk_filesystem_length:
                template_disk_filesystem_length = _template_disk_filesystem_length
            # template_disk_fsargs column
            _template_disk_fsargs_length = len(str(disk['filesystem_args'])) + 1
            if _template_disk_fsargs_length > template_disk_fsargs_length:
                template_disk_fsargs_length = _template_disk_fsargs_length
            # template_disk_mountpoint column
            _template_disk_mountpoint_length = len(str(disk['mountpoint'])) + 1
            if _template_disk_mountpoint_length > template_disk_mountpoint_length:
                template_disk_mountpoint_length = _template_disk_mountpoint_length

    # Format the string (header)
    template_list_output_header = '{bold}{template_name: <{template_name_length}} {template_id: <{template_id_length}} \
{template_disk_id: <{template_disk_id_length}} \
{template_disk_pool: <{template_disk_pool_length}} \
{template_disk_source: <{template_disk_source_length}} \
{template_disk_size: <{template_disk_size_length}} \
{template_disk_filesystem: <{template_disk_filesystem_length}} \
{template_disk_fsargs: <{template_disk_fsargs_length}} \
{template_disk_mountpoint: <{template_disk_mountpoint_length}}{end_bold}'.format(
            template_name_length=template_name_length,
            template_id_length=template_id_length,
            template_disk_id_length=template_disk_id_length,
            template_disk_pool_length=template_disk_pool_length,
            template_disk_source_length=template_disk_source_length,
            template_disk_size_length=template_disk_size_length,
            template_disk_filesystem_length=template_disk_filesystem_length,
            template_disk_fsargs_length=template_disk_fsargs_length,
            template_disk_mountpoint_length=template_disk_mountpoint_length,
            bold=ansiprint.bold(),
            end_bold=ansiprint.end(),
            template_name='Name',
            template_id='ID',
            template_disk_id='Disk ID',
            template_disk_pool='Pool',
            template_disk_source='Source Volume',
            template_disk_size='Size [GB]',
            template_disk_filesystem='Filesystem',
            template_disk_fsargs='Arguments',
            template_disk_mountpoint='Mountpoint'
        )

    # Format the string (elements)
    for template in sorted(template_template, key=lambda i: i.get('name', None)):
        template_list_output.append(
            '{bold}{template_name: <{template_name_length}} {template_id: <{template_id_length}}{end_bold}'.format(
                template_name_length=template_name_length,
                template_id_length=template_id_length,
                bold='',
                end_bold='',
                template_name=str(template['name']),
                template_id=str(template['id'])
            )
        )
        for disk in sorted(template['disks'], key=lambda i: i.get('disk_id', None)):
            template_list_output.append(
                '{bold}{template_name: <{template_name_length}} {template_id: <{template_id_length}} \
{template_disk_id: <{template_disk_id_length}} \
{template_disk_pool: <{template_disk_pool_length}} \
{template_disk_source: <{template_disk_source_length}} \
{template_disk_size: <{template_disk_size_length}} \
{template_disk_filesystem: <{template_disk_filesystem_length}} \
{template_disk_fsargs: <{template_disk_fsargs_length}} \
{template_disk_mountpoint: <{template_disk_mountpoint_length}}{end_bold}'.format(
                    template_name_length=template_name_length,
                    template_id_length=template_id_length,
                    template_disk_id_length=template_disk_id_length,
                    template_disk_pool_length=template_disk_pool_length,
                    template_disk_source_length=template_disk_source_length,
                    template_disk_size_length=template_disk_size_length,
                    template_disk_filesystem_length=template_disk_filesystem_length,
                    template_disk_fsargs_length=template_disk_fsargs_length,
                    template_disk_mountpoint_length=template_disk_mountpoint_length,
                    bold='',
                    end_bold='',
                    template_name='',
                    template_id='',
                    template_disk_id=str(disk['disk_id']),
                    template_disk_pool=str(disk['pool']),
                    template_disk_source=str(disk['source_volume']),
                    template_disk_size=str(disk['disk_size_gb']),
                    template_disk_filesystem=str(disk['filesystem']),
                    template_disk_fsargs=str(disk['filesystem_args']),
                    template_disk_mountpoint=str(disk['mountpoint'])
                )
            )

    return '\n'.join([template_list_output_header] + template_list_output)

def format_list_userdata(userdata_data, lines=None):
    if isinstance(userdata_data, dict):
        userdata_data = [ userdata_data ]

    userdata_list_output = []

    # Determine optimal column widths
    userdata_name_length = 5
    userdata_id_length = 3
    userdata_useruserdata_length = 8

    for userdata in userdata_data:
        # userdata_name column
        _userdata_name_length = len(str(userdata['name'])) + 1
        if _userdata_name_length > userdata_name_length:
            userdata_name_length = _userdata_name_length
        # userdata_id column
        _userdata_id_length = len(str(userdata['id'])) + 1
        if _userdata_id_length > userdata_id_length:
            userdata_id_length = _userdata_id_length

    # Format the string (header)
    userdata_list_output_header = '{bold}{userdata_name: <{userdata_name_length}} {userdata_id: <{userdata_id_length}} \
{userdata_data}{end_bold}'.format(
            userdata_name_length=userdata_name_length,
            userdata_id_length=userdata_id_length,
            bold=ansiprint.bold(),
            end_bold=ansiprint.end(),
            userdata_name='Name',
            userdata_id='ID',
            userdata_data='Document'
        )

    # Format the string (elements)
    for data in sorted(userdata_data, key=lambda i: i.get('name', None)):
        line_count = 0
        for line in data['userdata'].split('\n'):
            if line_count < 1:
                userdata_name = data['name']
                userdata_id = data['id']
            else:
                userdata_name = ''
                userdata_id = ''
            line_count += 1

            if lines and line_count > lines:
                userdata_list_output.append(
                    '{bold}{userdata_name: <{userdata_name_length}} {userdata_id: <{userdata_id_length}} \
{userdata_data}{end_bold}'.format(
                        userdata_name_length=userdata_name_length,
                        userdata_id_length=userdata_id_length,
                        bold='',
                        end_bold='',
                        userdata_name=userdata_name,
                        userdata_id=userdata_id,
                        userdata_data='[...]'
                    )
                )
                break

            userdata_list_output.append(
                '{bold}{userdata_name: <{userdata_name_length}} {userdata_id: <{userdata_id_length}} \
{userdata_data}{end_bold}'.format(
                    userdata_name_length=userdata_name_length,
                    userdata_id_length=userdata_id_length,
                    bold='',
                    end_bold='',
                    userdata_name=userdata_name,
                    userdata_id=userdata_id,
                    userdata_data=str(line)
                )
            )

    return '\n'.join([userdata_list_output_header] + userdata_list_output)

def format_list_script(script_data, lines=None):
    if isinstance(script_data, dict):
        script_data = [ script_data ]

    script_list_output = []

    # Determine optimal column widths
    script_name_length = 5
    script_id_length = 3
    script_script_length = 8

    for script in script_data:
        # script_name column
        _script_name_length = len(str(script['name'])) + 1
        if _script_name_length > script_name_length:
            script_name_length = _script_name_length
        # script_id column
        _script_id_length = len(str(script['id'])) + 1
        if _script_id_length > script_id_length:
            script_id_length = _script_id_length

    # Format the string (header)
    script_list_output_header = '{bold}{script_name: <{script_name_length}} {script_id: <{script_id_length}} \
{script_data}{end_bold}'.format(
            script_name_length=script_name_length,
            script_id_length=script_id_length,
            bold=ansiprint.bold(),
            end_bold=ansiprint.end(),
            script_name='Name',
            script_id='ID',
            script_data='Script'
        )

    # Format the string (elements)
    for script in sorted(script_data, key=lambda i: i.get('name', None)):
        line_count = 0
        for line in script['script'].split('\n'):
            if line_count < 1:
                script_name = script['name']
                script_id = script['id']
            else:
                script_name = ''
                script_id = ''
            line_count += 1

            if lines and line_count > lines:
                script_list_output.append(
                    '{bold}{script_name: <{script_name_length}} {script_id: <{script_id_length}} \
{script_data}{end_bold}'.format(
                        script_name_length=script_name_length,
                        script_id_length=script_id_length,
                        bold='',
                        end_bold='',
                        script_name=script_name,
                        script_id=script_id,
                        script_data='[...]'
                    )
                )
                break

            script_list_output.append(
                '{bold}{script_name: <{script_name_length}} {script_id: <{script_id_length}} \
{script_data}{end_bold}'.format(
                    script_name_length=script_name_length,
                    script_id_length=script_id_length,
                    bold='',
                    end_bold='',
                    script_name=script_name,
                    script_id=script_id,
                    script_data=str(line)
                )
            )

    return '\n'.join([script_list_output_header] + script_list_output)

def format_list_ova(ova_data):
    if isinstance(ova_data, dict):
        ova_data = [ ova_data ]

    ova_list_output = []

    # Determine optimal column widths
    ova_name_length = 5
    ova_id_length = 3
    ova_disk_id_length = 8
    ova_disk_size_length = 10
    ova_disk_pool_length = 5
    ova_disk_volume_format_length = 7
    ova_disk_volume_name_length = 13

    for ova in ova_data:
        # ova_name column
        _ova_name_length = len(str(ova['name'])) + 1
        if _ova_name_length > ova_name_length:
            ova_name_length = _ova_name_length
        # ova_id column
        _ova_id_length = len(str(ova['id'])) + 1
        if _ova_id_length > ova_id_length:
            ova_id_length = _ova_id_length

        for disk in ova['volumes']:
            # ova_disk_id column
            _ova_disk_id_length = len(str(disk['disk_id'])) + 1
            if _ova_disk_id_length > ova_disk_id_length:
                ova_disk_id_length = _ova_disk_id_length
            # ova_disk_size column
            _ova_disk_size_length = len(str(disk['disk_size_gb'])) + 1
            if _ova_disk_size_length > ova_disk_size_length:
                ova_disk_size_length = _ova_disk_size_length
            # ova_disk_pool column
            _ova_disk_pool_length = len(str(disk['pool'])) + 1
            if _ova_disk_pool_length > ova_disk_pool_length:
                ova_disk_pool_length = _ova_disk_pool_length
            # ova_disk_volume_format column
            _ova_disk_volume_format_length = len(str(disk['volume_format'])) + 1
            if _ova_disk_volume_format_length > ova_disk_volume_format_length:
                ova_disk_volume_format_length = _ova_disk_volume_format_length
            # ova_disk_volume_name column
            _ova_disk_volume_name_length = len(str(disk['volume_name'])) + 1
            if _ova_disk_volume_name_length > ova_disk_volume_name_length:
                ova_disk_volume_name_length = _ova_disk_volume_name_length

    # Format the string (header)
    ova_list_output_header = '{bold}{ova_name: <{ova_name_length}} {ova_id: <{ova_id_length}} \
{ova_disk_id: <{ova_disk_id_length}} \
{ova_disk_size: <{ova_disk_size_length}} \
{ova_disk_pool: <{ova_disk_pool_length}} \
{ova_disk_volume_format: <{ova_disk_volume_format_length}} \
{ova_disk_volume_name: <{ova_disk_volume_name_length}}{end_bold}'.format(
            ova_name_length=ova_name_length,
            ova_id_length=ova_id_length,
            ova_disk_id_length=ova_disk_id_length,
            ova_disk_pool_length=ova_disk_pool_length,
            ova_disk_size_length=ova_disk_size_length,
            ova_disk_volume_format_length=ova_disk_volume_format_length,
            ova_disk_volume_name_length=ova_disk_volume_name_length,
            bold=ansiprint.bold(),
            end_bold=ansiprint.end(),
            ova_name='Name',
            ova_id='ID',
            ova_disk_id='Disk ID',
            ova_disk_size='Size [GB]',
            ova_disk_pool='Pool',
            ova_disk_volume_format='Format',
            ova_disk_volume_name='Source Volume',
        )

    # Format the string (elements)
    for ova in sorted(ova_data, key=lambda i: i.get('name', None)):
        ova_list_output.append(
            '{bold}{ova_name: <{ova_name_length}} {ova_id: <{ova_id_length}}{end_bold}'.format(
                ova_name_length=ova_name_length,
                ova_id_length=ova_id_length,
                bold='',
                end_bold='',
                ova_name=str(ova['name']),
                ova_id=str(ova['id'])
            )
        )
        for disk in sorted(ova['volumes'], key=lambda i: i.get('disk_id', None)):
            ova_list_output.append(
                '{bold}{ova_name: <{ova_name_length}} {ova_id: <{ova_id_length}} \
{ova_disk_id: <{ova_disk_id_length}} \
{ova_disk_size: <{ova_disk_size_length}} \
{ova_disk_pool: <{ova_disk_pool_length}} \
{ova_disk_volume_format: <{ova_disk_volume_format_length}} \
{ova_disk_volume_name: <{ova_disk_volume_name_length}}{end_bold}'.format(
                    ova_name_length=ova_name_length,
                    ova_id_length=ova_id_length,
                    ova_disk_id_length=ova_disk_id_length,
                    ova_disk_size_length=ova_disk_size_length,
                    ova_disk_pool_length=ova_disk_pool_length,
                    ova_disk_volume_format_length=ova_disk_volume_format_length,
                    ova_disk_volume_name_length=ova_disk_volume_name_length,
                    bold='',
                    end_bold='',
                    ova_name='',
                    ova_id='',
                    ova_disk_id=str(disk['disk_id']),
                    ova_disk_size=str(disk['disk_size_gb']),
                    ova_disk_pool=str(disk['pool']),
                    ova_disk_volume_format=str(disk['volume_format']),
                    ova_disk_volume_name=str(disk['volume_name']),
                )
            )

    return '\n'.join([ova_list_output_header] + ova_list_output)

def format_list_profile(profile_data):
    if isinstance(profile_data, dict):
        profile_data = [ profile_data ]

    # Format the profile "source" from the type and, if applicable, OVA profile name
    for profile in profile_data:
        profile_type = profile['type']
        if 'ova' in profile_type:
            # Set the source to the name of the OVA:
            profile['source'] = 'OVA {}'.format(profile['ova'])
        else:
            # Set the source to be the type
            profile['source'] = profile_type

    profile_list_output = []

    # Determine optimal column widths
    profile_name_length = 5
    profile_id_length = 3
    profile_source_length = 7

    profile_system_template_length = 7
    profile_network_template_length = 8
    profile_storage_template_length = 8
    profile_userdata_length = 9
    profile_script_length = 7

    for profile in profile_data:
        # profile_name column
        _profile_name_length = len(str(profile['name'])) + 1
        if _profile_name_length > profile_name_length:
            profile_name_length = _profile_name_length
        # profile_id column
        _profile_id_length = len(str(profile['id'])) + 1
        if _profile_id_length > profile_id_length:
            profile_id_length = _profile_id_length
        # profile_source column
        _profile_source_length = len(str(profile['source'])) + 1
        if _profile_source_length > profile_source_length:
            profile_source_length = _profile_source_length
        # profile_system_template column
        _profile_system_template_length = len(str(profile['system_template'])) + 1
        if _profile_system_template_length > profile_system_template_length:
            profile_system_template_length = _profile_system_template_length
        # profile_network_template column
        _profile_network_template_length = len(str(profile['network_template'])) + 1
        if _profile_network_template_length > profile_network_template_length:
            profile_network_template_length = _profile_network_template_length
        # profile_storage_template column
        _profile_storage_template_length = len(str(profile['storage_template'])) + 1
        if _profile_storage_template_length > profile_storage_template_length:
            profile_storage_template_length = _profile_storage_template_length
        # profile_userdata column
        _profile_userdata_length = len(str(profile['userdata'])) + 1
        if _profile_userdata_length > profile_userdata_length:
            profile_userdata_length = _profile_userdata_length
        # profile_script column
        _profile_script_length = len(str(profile['script'])) + 1
        if _profile_script_length > profile_script_length:
            profile_script_length = _profile_script_length

    # Format the string (header)
    profile_list_output_header = '{bold}{profile_name: <{profile_name_length}} {profile_id: <{profile_id_length}} {profile_source: <{profile_source_length}} \
Templates: {profile_system_template: <{profile_system_template_length}} \
{profile_network_template: <{profile_network_template_length}} \
{profile_storage_template: <{profile_storage_template_length}} \
Data: {profile_userdata: <{profile_userdata_length}} \
{profile_script: <{profile_script_length}} \
{profile_arguments}{end_bold}'.format(
            profile_name_length=profile_name_length,
            profile_id_length=profile_id_length,
            profile_source_length=profile_source_length,
            profile_system_template_length=profile_system_template_length,
            profile_network_template_length=profile_network_template_length,
            profile_storage_template_length=profile_storage_template_length,
            profile_userdata_length=profile_userdata_length,
            profile_script_length=profile_script_length,
            bold=ansiprint.bold(),
            end_bold=ansiprint.end(),
            profile_name='Name',
            profile_id='ID',
            profile_source='Source',
            profile_system_template='System',
            profile_network_template='Network',
            profile_storage_template='Storage',
            profile_userdata='Userdata',
            profile_script='Script',
            profile_arguments='Script Arguments'
        )

    # Format the string (elements)
    for profile in sorted(profile_data, key=lambda i: i.get('name', None)):
        profile_list_output.append(
            '{bold}{profile_name: <{profile_name_length}} {profile_id: <{profile_id_length}} {profile_source: <{profile_source_length}} \
           {profile_system_template: <{profile_system_template_length}} \
{profile_network_template: <{profile_network_template_length}} \
{profile_storage_template: <{profile_storage_template_length}} \
      {profile_userdata: <{profile_userdata_length}} \
{profile_script: <{profile_script_length}} \
{profile_arguments}{end_bold}'.format(
                profile_name_length=profile_name_length,
                profile_id_length=profile_id_length,
                profile_source_length=profile_source_length,
                profile_system_template_length=profile_system_template_length,
                profile_network_template_length=profile_network_template_length,
                profile_storage_template_length=profile_storage_template_length,
                profile_userdata_length=profile_userdata_length,
                profile_script_length=profile_script_length,
                bold='',
                end_bold='',
                profile_name=profile['name'],
                profile_id=profile['id'],
                profile_source=profile['source'],
                profile_system_template=profile['system_template'],
                profile_network_template=profile['network_template'],
                profile_storage_template=profile['storage_template'],
                profile_userdata=profile['userdata'],
                profile_script=profile['script'],
                profile_arguments=', '.join(profile['arguments'])
            )
        )

    return '\n'.join([profile_list_output_header] + profile_list_output)

def format_list_task(task_data):
    task_list_output = []

    # Determine optimal column widths
    task_id_length = 7
    task_type_length = 7
    task_vm_name_length = 5
    task_vm_profile_length = 8
    task_vm_define_length = 8
    task_vm_start_length = 7
    task_worker_length = 8

    for task in task_data:
        # task_id column
        _task_id_length = len(str(task['id'])) + 1
        if _task_id_length > task_id_length:
            task_id_length = _task_id_length
        # task_type column
        _task_type_length = len(str(task['type'])) + 1
        if _task_type_length > task_type_length:
            task_type_length = _task_type_length
        # task_vm_name column
        _task_vm_name_length = len(str(task['vm_name'])) + 1
        if _task_vm_name_length > task_vm_name_length:
            task_vm_name_length = _task_vm_name_length
        # task_vm_profile column
        _task_vm_profile_length = len(str(task['vm_profile'])) + 1
        if _task_vm_profile_length > task_vm_profile_length:
            task_vm_profile_length = _task_vm_profile_length
        # task_vm_define column
        _task_vm_define_length = len(str(task['vm_define'])) + 1
        if _task_vm_define_length > task_vm_define_length:
            task_vm_define_length = _task_vm_define_length
        # task_vm_start column
        _task_vm_start_length = len(str(task['vm_start'])) + 1
        if _task_vm_start_length > task_vm_start_length:
            task_vm_start_length = _task_vm_start_length
        # task_worker column
        _task_worker_length = len(str(task['worker'])) + 1
        if _task_worker_length > task_worker_length:
            task_worker_length = _task_worker_length

    # Format the string (header)
    task_list_output_header = '{bold}{task_id: <{task_id_length}} {task_type: <{task_type_length}} \
{task_worker: <{task_worker_length}} \
VM: {task_vm_name: <{task_vm_name_length}} \
{task_vm_profile: <{task_vm_profile_length}} \
{task_vm_define: <{task_vm_define_length}} \
{task_vm_start: <{task_vm_start_length}}{end_bold}'.format(
            task_id_length=task_id_length,
            task_type_length=task_type_length,
            task_worker_length=task_worker_length,
            task_vm_name_length=task_vm_name_length,
            task_vm_profile_length=task_vm_profile_length,
            task_vm_define_length=task_vm_define_length,
            task_vm_start_length=task_vm_start_length,
            bold=ansiprint.bold(),
            end_bold=ansiprint.end(),
            task_id='Job ID',
            task_type='Status',
            task_worker='Worker',
            task_vm_name='Name',
            task_vm_profile='Profile',
            task_vm_define='Define?',
            task_vm_start='Start?'
        )

    # Format the string (elements)
    for task in sorted(task_data, key=lambda i: i.get('type', None)):
        task_list_output.append(
            '{bold}{task_id: <{task_id_length}} {task_type: <{task_type_length}} \
{task_worker: <{task_worker_length}} \
    {task_vm_name: <{task_vm_name_length}} \
{task_vm_profile: <{task_vm_profile_length}} \
{task_vm_define: <{task_vm_define_length}} \
{task_vm_start: <{task_vm_start_length}}{end_bold}'.format(
                task_id_length=task_id_length,
                task_type_length=task_type_length,
                task_worker_length=task_worker_length,
                task_vm_name_length=task_vm_name_length,
                task_vm_profile_length=task_vm_profile_length,
                task_vm_define_length=task_vm_define_length,
                task_vm_start_length=task_vm_start_length,
                bold='',
                end_bold='',
                task_id=task['id'],
                task_type=task['type'],
                task_worker=task['worker'],
                task_vm_name=task['vm_name'],
                task_vm_profile=task['vm_profile'],
                task_vm_define=task['vm_define'],
                task_vm_start=task['vm_start']
            )
        )

    return '\n'.join([task_list_output_header] + task_list_output)