# 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.

"""Helpers for building configdrive compatible with the Bare Metal service."""

import base64
import contextlib
import gzip
import json
import os
import shutil
import subprocess
import tempfile


@contextlib.contextmanager
def populate_directory(metadata, user_data=None, versions=None,
                       network_data=None, vendor_data=None):
    """Populate a directory with configdrive files.

    :param dict metadata: Metadata.
    :param bytes user_data: Vendor-specific user data.
    :param versions: List of metadata versions to support.
    :param dict network_data: Networking configuration.
    :param dict vendor_data: Extra supplied vendor data.
    :return: a context manager yielding a directory with files
    """
    d = tempfile.mkdtemp()
    versions = versions or ('2012-08-10', 'latest')
    try:
        for version in versions:
            subdir = os.path.join(d, 'openstack', version)
            if not os.path.exists(subdir):
                os.makedirs(subdir)

            with open(os.path.join(subdir, 'meta_data.json'), 'w') as fp:
                json.dump(metadata, fp)

            if network_data:
                with open(os.path.join(subdir, 'network_data.json'),
                          'w') as fp:
                    json.dump(network_data, fp)

            if vendor_data:
                with open(os.path.join(subdir, 'vendor_data2.json'),
                          'w') as fp:
                    json.dump(vendor_data, fp)

            if user_data:
                # Strictly speaking, user data is binary, but in many cases
                # it's actually a text (cloud-init, ignition, etc).
                flag = 't' if isinstance(user_data, str) else 'b'
                with open(os.path.join(subdir, 'user_data'),
                          'w%s' % flag) as fp:
                    fp.write(user_data)

        yield d
    finally:
        shutil.rmtree(d)


def build(metadata, user_data=None, versions=None, network_data=None,
          vendor_data=None):
    """Make a configdrive compatible with the Bare Metal service.

    Requires the genisoimage utility to be available.

    :param dict metadata: Metadata.
    :param user_data: Vendor-specific user data.
    :param versions: List of metadata versions to support.
    :param dict network_data: Networking configuration.
    :param dict vendor_data: Extra supplied vendor data.
    :return: configdrive contents as a base64-encoded string.
    """
    with populate_directory(metadata, user_data, versions,
                            network_data, vendor_data) as path:
        return pack(path)


def pack(path):
    """Pack a directory with files into a Bare Metal service configdrive.

    Creates an ISO image with the files and label "config-2".

    :param str path: Path to directory with files
    :return: configdrive contents as a base64-encoded string.
    """
    with tempfile.NamedTemporaryFile() as tmpfile:
        # NOTE(toabctl): Luckily, genisoimage, mkisofs and xorrisofs understand
        # the same parameters which are currently used.
        cmds = ['genisoimage', 'mkisofs', 'xorrisofs']
        for c in cmds:
            try:
                p = subprocess.Popen([c,
                                      '-o', tmpfile.name,
                                      '-ldots', '-allow-lowercase',
                                      '-allow-multidot', '-l',
                                      '-publisher', 'metalsmith',
                                      '-quiet', '-J',
                                      '-r', '-V', 'config-2',
                                      path],
                                     stdout=subprocess.PIPE,
                                     stderr=subprocess.PIPE)
            except OSError as e:
                error = e
            else:
                error = None
                break

        if error:
            raise RuntimeError(
                'Error generating the configdrive. Make sure the '
                '"genisoimage", "mkisofs" or "xorrisofs" tool is installed. '
                'Error: %s' % error)

        stdout, stderr = p.communicate()
        if p.returncode != 0:
            raise RuntimeError(
                'Error generating the configdrive.'
                'Stdout: "%(stdout)s". Stderr: "%(stderr)s"' %
                {'stdout': stdout, 'stderr': stderr})

        tmpfile.seek(0)

        with tempfile.NamedTemporaryFile() as tmpzipfile:
            with gzip.GzipFile(fileobj=tmpzipfile, mode='wb') as gz_file:
                shutil.copyfileobj(tmpfile, gz_file)

            tmpzipfile.seek(0)
            cd = base64.b64encode(tmpzipfile.read())

    # NOTE(dtantsur): Ironic expects configdrive to be a string, but base64
    # returns bytes on Python 3.
    if not isinstance(cd, str):
        cd = cd.decode('utf-8')

    return cd
