#!/usr/bin/python3
# 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.

import argparse
import jinja2
import os
import shutil
import six
import sys
import yaml

__tht_root_dir = os.path.dirname(os.path.dirname(__file__))


def _shutil_copy_if_not_same(src, dst):
    """Copy with shutil ignoring the same file errors."""
    if hasattr(shutil, 'SameFileError'):
        try:
            shutil.copy(src, dst)
        except shutil.SameFileError:
            pass
    else:
        try:
            shutil.copy(src, dst)
        except Exception as ex:
            if 'are the same file' in six.text_type(ex):
                pass
            else:
                raise


def parse_opts(argv):
    parser = argparse.ArgumentParser(
        description='Configure host network interfaces using a JSON'
        ' config file format.')
    parser.add_argument('-p', '--base_path', metavar='BASE_PATH',
                        help="""base path of templates to process.""",
                        default='.')
    parser.add_argument('-r', '--roles-data', metavar='ROLES_DATA',
                        help="""relative path to the roles_data.yaml file.""",
                        default='roles_data.yaml')
    parser.add_argument('-n', '--network-data', metavar='NETWORK_DATA',
                        help=("""relative path to the network_data.yaml """
                              """file."""),
                        default='network_data.yaml')
    parser.add_argument('--safe',
                        action='store_true',
                        help="""Enable safe mode (do not overwrite files).""",
                        default=False)
    parser.add_argument('-o', '--output-dir', metavar='OUTPUT_DIR',
                        help="""Output dir for all the templates""",
                        default='')
    parser.add_argument('-c', '--clean',
                        action='store_true',
                        help=("""clean the templates dir by deleting """
                              """generated templates"""))
    parser.add_argument('-d', '--dry-run',
                        action='store_true',
                        help=("""only output file names normally generated """
                              """from j2 templates"""))
    opts = parser.parse_args(argv[1:])

    return opts


def _j2_render_to_file(j2_template, j2_data, outfile_name=None,
                       overwrite=True, dry_run=False):
    yaml_f = outfile_name or j2_template.replace('.j2.yaml', '.yaml')
    if dry_run:
        amend = 'dry run processing'
    else:
        amend = 'rendering'
    print('%s j2 template to file: %s' % (amend, outfile_name))

    if not overwrite and os.path.exists(outfile_name):
        print('ERROR: path already exists for file: %s' % outfile_name)
        sys.exit(1)

    # Search for templates relative to the current template path first
    template_base = os.path.dirname(yaml_f)
    j2_loader = \
        jinja2.loaders.FileSystemLoader([template_base, __tht_root_dir])

    try:
        # Render the j2 template
        template = jinja2.Environment(loader=j2_loader).from_string(
            j2_template)
        r_template = template.render(**j2_data)
    except jinja2.exceptions.TemplateError as ex:
        error_msg = ("Error rendering template %s : %s"
                     % (yaml_f, six.text_type(ex)))
        print(error_msg)
        raise Exception(error_msg)
    if not dry_run:
        with open(outfile_name, 'w') as out_f:
            out_f.write(r_template)


def _set_tags_based_on_role_name(role_data):
    for role in role_data:
        role['tags'] = role.get('tags', [])
        role_name = role.get('name', str())

        if ((role_name.startswith('Compute') or role_name.startswith('HciCeph')
             or role_name.startswith('DistributedCompute'))
                and 'compute' not in role['tags']):
            role['tags'].append('compute')
            print("DEPRECATED: Role '%s' without the 'compute' tag "
                  "detected, the tag was added automatically. Please "
                  "add the 'compute' tag in roles data. The function to "
                  "automatically add tags based on role name will be "
                  "removed in the next release." % role_name)
        if role_name.startswith('Ceph') and 'ceph' not in role['tags']:
            role['tags'].append('ceph')
            print("DEPRECATED: Role '%s' without the 'ceph' tag "
                  "detected, the tag was added automatically. Please "
                  "add the 'ceph' tag in roles data. The function to "
                  "automatically add tags based on role name will be "
                  "removed in the next release." % role_name)
        if (role_name.startswith('ComputeOvsDpdk')
                and 'ovsdpdk' not in role['tags']):
            role['tags'].append('ovsdpdk')
            print("DEPRECATED: Role '%s' without the 'ovsdpdk' tag "
                  "detected, the tag was added automatically. Please "
                  "add the 'ovsdpdk' tag in roles data. The function to "
                  "automatically add tags based on role name will be "
                  "removed in the next release." % role_name)
        if ((role_name.startswith('ObjectStorage')
             or role_name.startswith('BlockStorage')
             or role_name.startswith('Ceph'))
                and 'storage' not in role['tags']):
            role['tags'].append('storage')
            print("DEPRECATED: Role '%s' without the 'storage' tag "
                  "detected, the tag was added automatically. Please "
                  "add the 'storage' tag in roles data. The function to "
                  "automatically add tags based on role name will be "
                  "removed in the next release." % role_name)


def process_templates(template_path, role_data_path, output_dir,
                      network_data_path, overwrite, dry_run):

    with open(role_data_path) as role_data_file:
        role_data = yaml.safe_load(role_data_file)

    with open(network_data_path) as network_data_file:
        network_data = yaml.safe_load(network_data_file)
        if network_data is None:
            network_data = []

    # Set internal network index key for each network, network resources
    # are created with a tag tripleo_net_idx
    for idx, net in enumerate(network_data):
        network_data[idx].update({'idx': idx})

    j2_excludes = {}
    j2_excludes_path = os.path.join(template_path, 'j2_excludes.yaml')
    if os.path.exists(j2_excludes_path):
        with open(j2_excludes_path) as role_data_file:
            j2_excludes = yaml.safe_load(role_data_file)

    if output_dir and not os.path.isdir(output_dir):
        if os.path.exists(output_dir):
            raise RuntimeError('Output dir %s is not a directory' % output_dir)
        os.mkdir(output_dir)

    # TODO(hjensas): In next release remove the function to automatically add
    # tags based on role name.
    _set_tags_based_on_role_name(role_data)

    role_names = [r.get('name') for r in role_data]
    r_map = {}
    for r in role_data:
        r_map[r.get('name')] = r

    n_map = {}
    for n in network_data:
        if (n.get('enabled') is not False):
            n_map[n.get('name')] = n
            if not n.get('name_lower'):
                n_map[n.get('name')]['name_lower'] = n.get('name').lower()
        else:
            print("skipping %s network: network is disabled" % n.get('name'))

    excl_templates = ['%s/%s' % (template_path, e)
                      for e in j2_excludes.get('name', [])]

    if os.path.isdir(template_path):
        for subdir, dirs, files in os.walk(template_path):

            # NOTE(flaper87): Ignore hidden dirs as we don't
            # generate templates for those.
            # Note the slice assignment for `dirs` is necessary
            # because we need to modify the *elements* in the
            # dirs list rather than the reference to the list.
            # This way we'll make sure os.walk will iterate over
            # the shrunk list. os.walk doesn't have an API for
            # filtering dirs at this point.
            dirs[:] = [d for d in dirs if not d[0] == '.']
            files = [f for f in files if not f[0] == '.']

            # NOTE(flaper87): We could have used shutil.copytree
            # but it requires the dst dir to not be present. This
            # approach is safer as it doesn't require us to delete
            # the output_dir in advance and it allows for running
            # the command multiple times with the same output_dir.
            out_dir = subdir
            if output_dir:
                if template_path != '.':
                    # strip out base path if not default
                    temp = out_dir.split(template_path)[1]
                    out_dir = temp[1:] if temp.startswith('/') else temp
                out_dir = os.path.join(output_dir, out_dir)
                if not os.path.exists(out_dir):
                    os.mkdir(out_dir)

            # Ensure template is on its expected search path
            # for upcoming parsing and rendering
            for f in files:
                if f.endswith('.j2') and output_dir:
                    _shutil_copy_if_not_same(os.path.join(subdir, f), out_dir)

            for f in files:
                file_path = os.path.join(subdir, f)
                # We do three templating passes here:
                # 1. *.role.j2.yaml - we template just the role name
                #    and create multiple files (one per role)
                # 2  *.network.j2.yaml - we template the network name and
                #    data and create multiple files for networks and
                #    network ports (one per network)
                # 3. *.j2.yaml - we template with all roles_data,
                #    and create one file common to all roles
                if f.endswith('.role.j2.yaml'):
                    print("jinja2 rendering role template %s" % f)
                    with open(file_path) as j2_template:
                        template_data = j2_template.read()
                        print("jinja2 rendering roles %s" % ","
                              .join(role_names))
                        for role in role_names:
                            j2_data = {'role': r_map[role]}
                            out_f = "-".join(
                                [role.lower(),
                                 os.path.basename(f).replace('.role.j2.yaml',
                                                             '.yaml')])
                            out_f_path = os.path.join(out_dir, out_f)
                            if ('network/config' in file_path and
                                    r_map[role].get(
                                        'deprecated_nic_config_name')):
                                d_name = r_map[role].get(
                                    'deprecated_nic_config_name')
                                out_f_path = os.path.join(out_dir, d_name)
                            elif ('network/config' in file_path):
                                d_name = "%s.yaml" % role.lower()
                                out_f_path = os.path.join(out_dir, d_name)
                            if not (out_f_path in excl_templates):
                                if '{{role.name}}' in template_data:
                                    j2_data = {'role': r_map[role],
                                               'networks': network_data}
                                    _j2_render_to_file(template_data, j2_data,
                                                       out_f_path, overwrite,
                                                       dry_run)
                                else:
                                    # Backwards compatibility with templates
                                    # that specify {{role}} vs {{role.name}}
                                    j2_data = {'role': role,
                                               'networks': network_data}
                                    _j2_render_to_file(
                                        template_data, j2_data,
                                        out_f_path, overwrite, dry_run)

                            else:
                                print('skipping rendering of %s' % out_f_path)

                elif f.endswith('.network.j2.yaml'):
                    print("jinja2 rendering network template %s" % f)
                    with open(file_path) as j2_template:
                        template_data = j2_template.read()
                    print("jinja2 rendering networks %s" % ",".join(n_map))
                    for network in n_map:
                        j2_data = {'network': n_map[network]}
                        # Output file names in "<name>.yaml" format
                        out_f = os.path.basename(f).replace('.network.j2.yaml',
                                                            '.yaml')
                        if os.path.dirname(file_path).endswith('ports'):
                            out_f = out_f.replace('port',
                                                  n_map[network]['name_lower'])
                        else:
                            out_f = out_f.replace('network',
                                                  n_map[network]['name_lower'])
                        out_f_path = os.path.join(out_dir, out_f)
                        if not (out_f_path in excl_templates):
                            _j2_render_to_file(template_data, j2_data,
                                               out_f_path, overwrite, dry_run)
                        else:
                            print('skipping rendering of %s' % out_f_path)

                elif f.endswith('.j2.yaml'):
                    print("jinja2 rendering normal template %s" % f)
                    with open(file_path) as j2_template:
                        template_data = j2_template.read()
                        j2_data = {'roles': role_data,
                                   'networks': network_data}
                        out_f = os.path.basename(f).replace('.j2.yaml',
                                                            '.yaml')
                        out_f_path = os.path.join(out_dir, out_f)
                        _j2_render_to_file(template_data, j2_data, out_f_path,
                                           overwrite, dry_run)
                elif output_dir:
                    _shutil_copy_if_not_same(os.path.join(subdir, f), out_dir)

    else:
        print('Unexpected argument %s' % template_path)


def clean_templates(base_path, role_data_path, network_data_path):

    def delete(f):
        if os.path.exists(f):
            print("Deleting %s" % f)
            os.unlink(f)

    for root, dirs, files in os.walk(base_path):
        for f in files:
            if f.endswith('.j2.yaml'):
                rendered_path = os.path.join(
                    root, '%s.yaml' % f.split('.j2.yaml')[0])
                delete(rendered_path)

    with open(network_data_path) as network_data_file:
        network_data = yaml.safe_load(network_data_file)

    for network in network_data:
        network_path = os.path.join(
            'network', '%s.yaml' % network['name_lower'])
        network_from_pool_path = os.path.join(
            'network', '%s_from_pool.yaml' % network['name_lower'])
        network_v6_path = os.path.join(
            'network', '%s_v6.yaml' % network['name_lower'])
        network_from_pool_v6_path = os.path.join(
            'network', '%s_from_pool_v6.yaml' % network['name_lower'])
        ports_path = os.path.join(
            'network', 'ports', '%s.yaml' % network['name_lower'])
        external_resource_ports_path = os.path.join(
            'network', 'ports',
            'external_resource_%s.yaml' % network['name_lower'])
        external_resource_ports_v6_path = os.path.join(
            'network', 'ports',
            'external_resource_%s_v6.yaml' % network['name_lower'])
        ports_from_pool_path = os.path.join(
            'network', 'ports', '%s_from_pool.yaml' % network['name_lower'])
        ports_v6_path = os.path.join(
            'network', 'ports', '%s_v6.yaml' % network['name_lower'])
        ports_from_pool_v6_path = os.path.join(
            'network', 'ports', '%s_from_pool_v6.yaml' % network['name_lower'])
        deployed_ports_path = os.path.join(
            'network', 'ports', 'deployed_%s.yaml' % network['name_lower'])
        deployed_vip_ports_path = os.path.join(
            'network', 'ports', 'deployed_vip_%s.yaml' % network['name_lower'])

        delete(network_path)
        delete(network_from_pool_path)
        delete(network_v6_path)
        delete(network_from_pool_v6_path)
        delete(ports_path)
        delete(external_resource_ports_path)
        delete(external_resource_ports_v6_path)
        delete(ports_from_pool_path)
        delete(ports_v6_path)
        delete(ports_from_pool_v6_path)
        delete(deployed_ports_path)
        delete(deployed_vip_ports_path)

    with open(role_data_path) as role_data_file:
        role_data = yaml.safe_load(role_data_file)

    for role in role_data:
        role_path = os.path.join(
            'puppet', '%s-role.yaml' % role['name'].lower())
        host_config_and_reboot_path = os.path.join(
            'extraconfig', 'pre_network',
            '%s-host_config_and_reboot.yaml' % role['name'].lower())
        krb_service_principals_path = os.path.join(
            'extraconfig', 'nova_metadata', 'krb-service-principals',
            '%s-role.yaml' % role['name'].lower())
        common_services_path = os.path.join(
            'common', 'services', '%s-role.yaml' % role['name'].lower())

        delete(role_path)
        delete(host_config_and_reboot_path)
        delete(krb_service_principals_path)
        delete(common_services_path)


opts = parse_opts(sys.argv)

role_data_path = os.path.join(opts.base_path, opts.roles_data)
network_data_path = os.path.join(opts.base_path, opts.network_data)

if opts.clean:
    clean_templates(opts.base_path, role_data_path, network_data_path)
else:
    process_templates(opts.base_path, role_data_path, opts.output_dir,
                      network_data_path, (not opts.safe), opts.dry_run)
