manila/manila/share/drivers/windows/service_instance.py

283 lines
12 KiB
Python

# 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"))