# Copyright 2012,2014 Hewlett-Packard Development Company, L.P.
# Copyright (c) 2012 NTT DOCOMO, INC.
# All Rights Reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

"""
Class for PXE bare-metal nodes.
"""

import datetime
import os

import jinja2
from oslo.config import cfg

from nova.compute import flavors
from nova import exception
from nova.i18n import _
from nova import objects
from nova.openstack.common.db import exception as db_exc
from nova.openstack.common import fileutils
from nova.openstack.common import log as logging
from nova.openstack.common import loopingcall
from nova.openstack.common import timeutils
from nova import utils
from nova.virt.baremetal import baremetal_states
from nova.virt.baremetal import base
from nova.virt.baremetal import db
from nova.virt.baremetal import utils as bm_utils

pxe_opts = [
    cfg.StrOpt('deploy_kernel',
               help='Default kernel image ID used in deployment phase'),
    cfg.StrOpt('deploy_ramdisk',
               help='Default ramdisk image ID used in deployment phase'),
    cfg.StrOpt('net_config_template',
               default='$pybasedir/nova/virt/baremetal/'
                            'net-dhcp.ubuntu.template',
               help='Template file for injected network config'),
    cfg.StrOpt('pxe_append_params',
               default='nofb nomodeset vga=normal',
               help='Additional append parameters for baremetal PXE boot'),
    cfg.StrOpt('pxe_config_template',
               default='$pybasedir/nova/virt/baremetal/pxe_config.template',
               help='Template file for PXE configuration'),
    cfg.BoolOpt('use_file_injection',
                help='If True, enable file injection for network info, '
                'files and admin password',
                default=False),
    cfg.IntOpt('pxe_deploy_timeout',
                help='Timeout for PXE deployments. Default: 0 (unlimited)',
                default=0),
    cfg.BoolOpt('pxe_network_config',
                help='If set, pass the network configuration details to the '
                'initramfs via cmdline.',
                default=False),
    cfg.StrOpt('pxe_bootfile_name',
               help='This gets passed to Neutron as the bootfile dhcp '
               'parameter.',
               default='pxelinux.0'),
    ]

LOG = logging.getLogger(__name__)

baremetal_group = cfg.OptGroup(name='baremetal',
                               title='Baremetal Options')

CONF = cfg.CONF
CONF.register_group(baremetal_group)
CONF.register_opts(pxe_opts, baremetal_group)
CONF.import_opt('use_ipv6', 'nova.netconf')


def build_pxe_network_config(network_info):
    interfaces = bm_utils.map_network_interfaces(network_info, CONF.use_ipv6)
    template = None
    if not CONF.use_ipv6:
        template = "ip=%(address)s::%(gateway)s:%(netmask)s::%(name)s:off"
    else:
        template = ("ip=[%(address_v6)s]::[%(gateway_v6)s]:"
                    "[%(netmask_v6)s]::%(name)s:off")

    net_config = [template % iface for iface in interfaces]
    return ' '.join(net_config)


def build_pxe_config(deployment_id, deployment_key, deployment_iscsi_iqn,
                      deployment_aki_path, deployment_ari_path,
                      aki_path, ari_path, network_info):
    """Build the PXE config file for a node

    This method builds the PXE boot configuration file for a node,
    given all the required parameters.

    The resulting file has both a "deploy" and "boot" label, which correspond
    to the two phases of booting. This may be extended later.

    """
    LOG.debug("Building PXE config for deployment %s.", deployment_id)

    network_config = None
    if network_info and CONF.baremetal.pxe_network_config:
        network_config = build_pxe_network_config(network_info)

    pxe_options = {
            'deployment_id': deployment_id,
            'deployment_key': deployment_key,
            'deployment_iscsi_iqn': deployment_iscsi_iqn,
            'deployment_aki_path': deployment_aki_path,
            'deployment_ari_path': deployment_ari_path,
            'aki_path': aki_path,
            'ari_path': ari_path,
            'pxe_append_params': CONF.baremetal.pxe_append_params,
            'pxe_network_config': network_config,
            }
    tmpl_path, tmpl_file = os.path.split(CONF.baremetal.pxe_config_template)
    env = jinja2.Environment(loader=jinja2.FileSystemLoader(tmpl_path))
    template = env.get_template(tmpl_file)
    return template.render({'pxe_options': pxe_options,
                            'ROOT': '${ROOT}'})


def build_network_config(network_info):
    interfaces = bm_utils.map_network_interfaces(network_info, CONF.use_ipv6)
    tmpl_path, tmpl_file = os.path.split(CONF.baremetal.net_config_template)
    env = jinja2.Environment(loader=jinja2.FileSystemLoader(tmpl_path))
    template = env.get_template(tmpl_file)
    return template.render({'interfaces': interfaces,
                            'use_ipv6': CONF.use_ipv6})


def get_deploy_aki_id(flavor):
    return flavor.get('extra_specs', {}).\
            get('baremetal:deploy_kernel_id', CONF.baremetal.deploy_kernel)


def get_deploy_ari_id(flavor):
    return flavor.get('extra_specs', {}).\
            get('baremetal:deploy_ramdisk_id', CONF.baremetal.deploy_ramdisk)


def get_image_dir_path(instance):
    """Generate the dir for an instances disk."""
    return os.path.join(CONF.instances_path, instance['name'])


def get_image_file_path(instance):
    """Generate the full path for an instances disk."""
    return os.path.join(CONF.instances_path, instance['name'], 'disk')


def get_pxe_config_file_path(instance):
    """Generate the path for an instances PXE config file."""
    return os.path.join(CONF.baremetal.tftp_root, instance['uuid'], 'config')


def get_partition_sizes(instance):
    flavor = flavors.extract_flavor(instance)
    root_mb = flavor['root_gb'] * 1024
    swap_mb = flavor['swap']
    ephemeral_mb = flavor['ephemeral_gb'] * 1024

    # NOTE(deva): For simpler code paths on the deployment side,
    #             we always create a swap partition. If the flavor
    #             does not specify any swap, we default to 1MB
    if swap_mb < 1:
        swap_mb = 1

    return (root_mb, swap_mb, ephemeral_mb)


def get_pxe_mac_path(mac):
    """Convert a MAC address into a PX
E config file name."""
    return os.path.join(
            CONF.baremetal.tftp_root,
            'pxelinux.cfg',
            "01-" + mac.replace(":", "-").lower()
        )


def get_tftp_image_info(instance, flavor):
    """Generate the paths for tftp files for this instance

    Raises NovaException if
    - instance does not contain kernel_id or ramdisk_id
    - deploy_kernel_id or deploy_ramdisk_id can not be read from
    flavor['extra_s
pecs'] and defaults are not set

    """
    image_info = {
            'kernel': [None, None],
            'ramdisk': [None, None],
            'deploy_kernel': [None, None],
            'deploy_ramdisk': [None, None],
            }
    try:
        image_info['kernel'][0] = str(instance['kernel_id'])
        image_info['ramdisk'][0] = str(instance['ramdisk_id'])
        image_info['deploy_kernel'][0] = get_deploy_aki_id(flavor)
        image_info['deploy_ramdisk'][0] = get_deploy_ari_id(flavor)
    except KeyError:
        pass

    missing_labels = []
    for label in image_info.keys():
        (uuid, path) = image_info[label]
        if not uuid:
            missing_labels.append(label)
        else:
            image_info[label][1] = os.path.join(CONF.baremetal.tftp_root,
                            instance['uuid'], label)
    if missing_labels:
        raise exce