# Copyright (c) 2015 Cloudbase Solutions SRL
# 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.

import os
import re

from oslo_concurrency import processutils
from oslo_config import cfg
from oslo_log import log

from manila import exception
from manila.i18n import _
from manila.share.drivers import service_instance
from manila.share.drivers.windows import windows_utils
from manila.share.drivers.windows import winrm_helper


CONF = cfg.CONF
LOG = log.getLogger(__name__)

windows_share_server_opts = [
    cfg.StrOpt(
        "winrm_cert_pem_path",
        default="~/.ssl/cert.pem",
        help="Path to the x509 certificate used for accessing the service "
             "instance."),
    cfg.StrOpt(
        "winrm_cert_key_pem_path",
        default="~/.ssl/key.pem",
        help="Path to the x509 certificate key."),
    cfg.BoolOpt(
        "winrm_use_cert_based_auth",
        default=False,
        help="Use x509 certificates in order to authenticate to the "
             "service instance.")
]

CONF = cfg.CONF
CONF.register_opts(windows_share_server_opts)


class WindowsServiceInstanceManager(service_instance.ServiceInstanceManager):
    """"Manages Windows Nova instances."""
    _INSTANCE_CONNECTION_PROTO = "WinRM"
    _CBS_INIT_RUN_PLUGIN_AFTER_REBOOT = 2
    _CBS_INIT_WINRM_PLUGIN = "ConfigWinRMListenerPlugin"
    _DEFAULT_MINIMUM_PASS_LENGTH = 6

    def __init__(self, driver_config=None, remote_execute=None):
        super(WindowsServiceInstanceManager, self).__init__(
            driver_config=driver_config)
        driver_config.append_config_values(windows_share_server_opts)

        self._use_cert_auth = self.get_config_option(
            "winrm_use_cert_based_auth")
        self._cert_pem_path = self.get_config_option(
            "winrm_cert_pem_path")
        self._cert_key_pem_path = self.get_config_option(
            "winrm_cert_key_pem_path")
        self._check_auth_mode()

        self._remote_execute = (remote_execute or
                                winrm_helper.WinRMHelper(
                                    configuration=driver_config).execute)
        self._windows_utils = windows_utils.WindowsUtils(
            remote_execute=self._remote_execute)

    def _check_auth_mode(self):
        if self._use_cert_auth:
            if not (os.path.exists(self._cert_pem_path) and
                    os.path.exists(self._cert_key_pem_path)):
                msg = _("Certificate based authentication was configured "
                        "but one or more certificates are missing.")
                raise exception.ServiceInstanceException(msg)
            LOG.debug("Using certificate based authentication for "
                      "service instances.")
        else:
            instance_password = self.get_config_option(
                "service_instance_password")
            if not self._check_password_complexity(instance_password):
                msg = _("The configured service instance password does not "
                        "match the minimum complexity requirements. "
                        "The password must contain at least %s characters. "
                        "Also, it must contain at least one digit, "
                        "one lower case and one upper case character.")
                raise exception.ServiceInstanceException(
                    msg % self._DEFAULT_MINIMUM_PASS_LENGTH)
            LOG.debug("Using password based authentication for "
                      "service instances.")

    def _get_auth_info(self):
        auth_info = {'use_cert_auth': self._use_cert_auth}
        if self._use_cert_auth:
            auth_info.update(cert_pem_path=self._cert_pem_path,
                             cert_key_pem_path=self._cert_key_pem_path)
        return auth_info

    def get_common_server(self):
        data = super(WindowsServiceInstanceManager, self).get_common_server()
        data['backend_details'].update(self._get_auth_info())
        return data

    def _get_new_instance_details(self, server):
        instance_details = super(WindowsServiceInstanceManager,
                                 self)._get_new_instance_details(server)
        instance_details.update(self._get_auth_info())
        return instance_details

    def _check_password_complexity(self, password):
        # Make sure that the Windows complexity requirements are met:
        # http://technet.microsoft.com/en-us/library/cc786468(v=ws.10).aspx
        if len(password) < self._DEFAULT_MINIMUM_PASS_LENGTH:
            return False
        for r in ("[a-z]", "[A-Z]", "[0-9]"):
            if not re.search(r, password):
                return False
        return True

    def _test_server_connection(self, server):
        try:
            self._remote_execute(server, "whoami", retry=False)
            LOG.debug("Service VM %s is available via WinRM",
                      server['ip'])
            return True
        except Exception as ex:
            LOG.debug("Server %(ip)s is not available via WinRM. "
                      "Exception: %(ex)s ",
                      dict(ip=server['ip'],
                           ex=ex))
            return False

    def _get_service_instance_create_kwargs(self):
        create_kwargs = {}
        if self._use_cert_auth:
            # At the moment, we pass the x509 certificate via user data.
            # We'll use keypairs instead as soon as the nova client will
            # support x509 certificates.
            with open(self._cert_pem_path, 'r') as f:
                cert_pem_data = f.read()
            create_kwargs['user_data'] = cert_pem_data
        else:
            # The admin password has to be specified via instance metadata in
            # order to be passed to the instance via the metadata service or
            # configdrive.
            admin_pass = self.get_config_option("service_instance_password")
            create_kwargs['meta'] = {'admin_pass': admin_pass}
        return create_kwargs

    def set_up_service_instance(self, context, network_info):
        instance_details = super(WindowsServiceInstanceManager,
                                 self).set_up_service_instance(context,
                                                               network_info)
        security_services = network_info['security_services']
        security_service = self.get_valid_security_service(security_services)
        if security_service:
            self._setup_security_service(instance_details, security_service)

        instance_details['joined_domain'] = bool(security_service)
        return instance_details

    def _setup_security_service(self, server, security_service):
        domain = security_service['domain']
        admin_username = security_service['user']
        admin_password = security_service['password']
        dns_ip = security_service['dns_ip']

        self._windows_utils.set_dns_client_search_list(server, [domain])

        if_index = self._windows_utils.get_interface_index_by_ip(server,
                                                                 server['ip'])
        self._windows_utils.set_dns_client_server_addresses(server,
                                                            if_index,
                                                            [dns_ip])
        # Joining an AD domain will alter the WinRM Listener configuration.
        # Cloudbase-init is required to be running on the Windows service
        # instance, so we re-enable the plugin configuring the WinRM listener.
        #
        # TODO(lpetrut): add a config option so that we may rely on the AD
        # group policies taking care of the WinRM configuration.
        self._run_cloudbase_init_plugin_after_reboot(
            server, plugin_name=self._CBS_INIT_WINRM_PLUGIN)
        self._join_domain(server, domain, admin_username, admin_password)

    def _join_domain(self, server, domain, admin_username, admin_password):
        # As the WinRM configuration may be altered and existing connections
        # closed, we may not be able to retrieve the result of this operation.
        # Instead, we'll ensure that the instance actually joined the domain
        # after the reboot.
        try:
            self._windows_utils.join_domain(server, domain, admin_username,
                                            admin_password)
        except processutils.ProcessExecutionError:
            raise
        except Exception as exc:
            LOG.debug("Unexpected error while attempting to join domain "
                      "%(domain)s. Verifying the result of the operation "
                      "after instance reboot. Exception: %(exc)s",
                      dict(domain=domain, exc=exc))
        # We reboot the service instance using the Compute API so that
        # we can wait for it to become active.
        self.reboot_server(server, soft_reboot=True)
        self.wait_for_instance_to_be_active(
            server['instance_id'],
            timeout=self.max_time_to_build_instance)
        if not self._check_server_availability(server):
            raise exception.ServiceInstanceException(
                _('%(conn_proto)s connection has not been '
                  'established to %(server)s in %(time)ss. Giving up.') % {
                      'conn_proto': self._INSTANCE_CONNECTION_PROTO,
                      'server': server['ip'],
                      'time': self.max_time_to_build_instance})

        current_domain = self._windows_utils.get_current_domain(server)
        if current_domain != domain:
            err_msg = _("Failed to join domain %(requested_domain)s. "
                        "Current domain: %(current_domain)s")
            raise exception.ServiceInstanceException(
                err_msg % dict(requested_domain=domain,
                               current_domain=current_domain))

    def get_valid_security_service(self, security_services):
        if not security_services:
            LOG.info("No security services provided.")
        elif len(security_services) > 1:
            LOG.warning("Multiple security services provided. Only one "
                        "security service of type 'active_directory' "
                        "is supported.")
        else:
            security_service = security_services[0]
            security_service_type = security_service['type']
            if security_service_type == 'active_directory':
                return security_service
            else:
                LOG.warning("Only security services of type "
                            "'active_directory' are supported. "
                            "Retrieved security "
                            "service type: %(sec_type)s.",
                            {'sec_type': security_service_type})
        return None

    def _run_cloudbase_init_plugin_after_reboot(self, server, plugin_name):
        cbs_init_reg_section = self._get_cbs_init_reg_section(server)
        plugin_key_path = "%(cbs_init_section)s\\%(instance_id)s\\Plugins" % {
            'cbs_init_section': cbs_init_reg_section,
            'instance_id': server['instance_id']
        }
        self._windows_utils.set_win_reg_value(
            server, path=plugin_key_path, key=plugin_name,
            value=self._CBS_INIT_RUN_PLUGIN_AFTER_REBOOT)

    def _get_cbs_init_reg_section(self, server):
        base_path = 'hklm:\\SOFTWARE'
        cbs_section = 'Cloudbase Solutions\\Cloudbase-Init'

        for upper_section in ('', 'Wow6432Node'):
            cbs_init_section = self._windows_utils.normalize_path(
                os.path.join(base_path, upper_section, cbs_section))
            try:
                self._windows_utils.get_win_reg_value(
                    server, path=cbs_init_section)
                return cbs_init_section
            except processutils.ProcessExecutionError as ex:
                # The exit code will always be '1' in case of errors, so the
                # only way to determine the error type is checking stderr.
                if 'Cannot find path' in ex.stderr:
                    continue
                else:
                    raise
        raise exception.ServiceInstanceException(
            _("Could not retrieve Cloudbase Init registry section"))
