Coverage for manila/share/drivers/windows/service_instance.py: 99%
138 statements
« prev ^ index » next coverage.py v7.11.0, created at 2026-02-18 22:19 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2026-02-18 22:19 +0000
1# Copyright (c) 2015 Cloudbase Solutions SRL
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
16import os
17import re
19from oslo_concurrency import processutils
20from oslo_config import cfg
21from oslo_log import log
23from manila import exception
24from manila.i18n import _
25from manila.share.drivers import service_instance
26from manila.share.drivers.windows import windows_utils
27from manila.share.drivers.windows import winrm_helper
30CONF = cfg.CONF
31LOG = log.getLogger(__name__)
33windows_share_server_opts = [
34 cfg.StrOpt(
35 "winrm_cert_pem_path",
36 default="~/.ssl/cert.pem",
37 help="Path to the x509 certificate used for accessing the service "
38 "instance."),
39 cfg.StrOpt(
40 "winrm_cert_key_pem_path",
41 default="~/.ssl/key.pem",
42 help="Path to the x509 certificate key."),
43 cfg.BoolOpt(
44 "winrm_use_cert_based_auth",
45 default=False,
46 help="Use x509 certificates in order to authenticate to the "
47 "service instance.")
48]
50CONF = cfg.CONF
51CONF.register_opts(windows_share_server_opts)
54class WindowsServiceInstanceManager(service_instance.ServiceInstanceManager):
55 """"Manages Windows Nova instances."""
56 _INSTANCE_CONNECTION_PROTO = "WinRM"
57 _CBS_INIT_RUN_PLUGIN_AFTER_REBOOT = 2
58 _CBS_INIT_WINRM_PLUGIN = "ConfigWinRMListenerPlugin"
59 _DEFAULT_MINIMUM_PASS_LENGTH = 6
61 def __init__(self, driver_config=None, remote_execute=None):
62 super(WindowsServiceInstanceManager, self).__init__(
63 driver_config=driver_config)
64 driver_config.append_config_values(windows_share_server_opts)
66 self._use_cert_auth = self.get_config_option(
67 "winrm_use_cert_based_auth")
68 self._cert_pem_path = self.get_config_option(
69 "winrm_cert_pem_path")
70 self._cert_key_pem_path = self.get_config_option(
71 "winrm_cert_key_pem_path")
72 self._check_auth_mode()
74 self._remote_execute = (remote_execute or
75 winrm_helper.WinRMHelper(
76 configuration=driver_config).execute)
77 self._windows_utils = windows_utils.WindowsUtils(
78 remote_execute=self._remote_execute)
80 def _check_auth_mode(self):
81 if self._use_cert_auth:
82 if not (os.path.exists(self._cert_pem_path) and
83 os.path.exists(self._cert_key_pem_path)):
84 msg = _("Certificate based authentication was configured "
85 "but one or more certificates are missing.")
86 raise exception.ServiceInstanceException(msg)
87 LOG.debug("Using certificate based authentication for "
88 "service instances.")
89 else:
90 instance_password = self.get_config_option(
91 "service_instance_password")
92 if not self._check_password_complexity(instance_password):
93 msg = _("The configured service instance password does not "
94 "match the minimum complexity requirements. "
95 "The password must contain at least %s characters. "
96 "Also, it must contain at least one digit, "
97 "one lower case and one upper case character.")
98 raise exception.ServiceInstanceException(
99 msg % self._DEFAULT_MINIMUM_PASS_LENGTH)
100 LOG.debug("Using password based authentication for "
101 "service instances.")
103 def _get_auth_info(self):
104 auth_info = {'use_cert_auth': self._use_cert_auth}
105 if self._use_cert_auth:
106 auth_info.update(cert_pem_path=self._cert_pem_path,
107 cert_key_pem_path=self._cert_key_pem_path)
108 return auth_info
110 def get_common_server(self):
111 data = super(WindowsServiceInstanceManager, self).get_common_server()
112 data['backend_details'].update(self._get_auth_info())
113 return data
115 def _get_new_instance_details(self, server):
116 instance_details = super(WindowsServiceInstanceManager,
117 self)._get_new_instance_details(server)
118 instance_details.update(self._get_auth_info())
119 return instance_details
121 def _check_password_complexity(self, password):
122 # Make sure that the Windows complexity requirements are met:
123 # http://technet.microsoft.com/en-us/library/cc786468(v=ws.10).aspx
124 if len(password) < self._DEFAULT_MINIMUM_PASS_LENGTH:
125 return False
126 for r in ("[a-z]", "[A-Z]", "[0-9]"):
127 if not re.search(r, password):
128 return False
129 return True
131 def _test_server_connection(self, server):
132 try:
133 self._remote_execute(server, "whoami", retry=False)
134 LOG.debug("Service VM %s is available via WinRM",
135 server['ip'])
136 return True
137 except Exception as ex:
138 LOG.debug("Server %(ip)s is not available via WinRM. "
139 "Exception: %(ex)s ",
140 dict(ip=server['ip'],
141 ex=ex))
142 return False
144 def _get_service_instance_create_kwargs(self):
145 create_kwargs = {}
146 if self._use_cert_auth:
147 # At the moment, we pass the x509 certificate via user data.
148 # We'll use keypairs instead as soon as the nova client will
149 # support x509 certificates.
150 with open(self._cert_pem_path, 'r') as f:
151 cert_pem_data = f.read()
152 create_kwargs['user_data'] = cert_pem_data
153 else:
154 # The admin password has to be specified via instance metadata in
155 # order to be passed to the instance via the metadata service or
156 # configdrive.
157 admin_pass = self.get_config_option("service_instance_password")
158 create_kwargs['meta'] = {'admin_pass': admin_pass}
159 return create_kwargs
161 def set_up_service_instance(self, context, network_info):
162 instance_details = super(WindowsServiceInstanceManager,
163 self).set_up_service_instance(context,
164 network_info)
165 security_services = network_info['security_services']
166 security_service = self.get_valid_security_service(security_services)
167 if security_service: 167 ↛ 170line 167 didn't jump to line 170 because the condition on line 167 was always true
168 self._setup_security_service(instance_details, security_service)
170 instance_details['joined_domain'] = bool(security_service)
171 return instance_details
173 def _setup_security_service(self, server, security_service):
174 domain = security_service['domain']
175 admin_username = security_service['user']
176 admin_password = security_service['password']
177 dns_ip = security_service['dns_ip']
179 self._windows_utils.set_dns_client_search_list(server, [domain])
181 if_index = self._windows_utils.get_interface_index_by_ip(server,
182 server['ip'])
183 self._windows_utils.set_dns_client_server_addresses(server,
184 if_index,
185 [dns_ip])
186 # Joining an AD domain will alter the WinRM Listener configuration.
187 # Cloudbase-init is required to be running on the Windows service
188 # instance, so we re-enable the plugin configuring the WinRM listener.
189 #
190 # TODO(lpetrut): add a config option so that we may rely on the AD
191 # group policies taking care of the WinRM configuration.
192 self._run_cloudbase_init_plugin_after_reboot(
193 server, plugin_name=self._CBS_INIT_WINRM_PLUGIN)
194 self._join_domain(server, domain, admin_username, admin_password)
196 def _join_domain(self, server, domain, admin_username, admin_password):
197 # As the WinRM configuration may be altered and existing connections
198 # closed, we may not be able to retrieve the result of this operation.
199 # Instead, we'll ensure that the instance actually joined the domain
200 # after the reboot.
201 try:
202 self._windows_utils.join_domain(server, domain, admin_username,
203 admin_password)
204 except processutils.ProcessExecutionError:
205 raise
206 except Exception as exc:
207 LOG.debug("Unexpected error while attempting to join domain "
208 "%(domain)s. Verifying the result of the operation "
209 "after instance reboot. Exception: %(exc)s",
210 dict(domain=domain, exc=exc))
211 # We reboot the service instance using the Compute API so that
212 # we can wait for it to become active.
213 self.reboot_server(server, soft_reboot=True)
214 self.wait_for_instance_to_be_active(
215 server['instance_id'],
216 timeout=self.max_time_to_build_instance)
217 if not self._check_server_availability(server):
218 raise exception.ServiceInstanceException(
219 _('%(conn_proto)s connection has not been '
220 'established to %(server)s in %(time)ss. Giving up.') % {
221 'conn_proto': self._INSTANCE_CONNECTION_PROTO,
222 'server': server['ip'],
223 'time': self.max_time_to_build_instance})
225 current_domain = self._windows_utils.get_current_domain(server)
226 if current_domain != domain:
227 err_msg = _("Failed to join domain %(requested_domain)s. "
228 "Current domain: %(current_domain)s")
229 raise exception.ServiceInstanceException(
230 err_msg % dict(requested_domain=domain,
231 current_domain=current_domain))
233 def get_valid_security_service(self, security_services):
234 if not security_services:
235 LOG.info("No security services provided.")
236 elif len(security_services) > 1:
237 LOG.warning("Multiple security services provided. Only one "
238 "security service of type 'active_directory' "
239 "is supported.")
240 else:
241 security_service = security_services[0]
242 security_service_type = security_service['type']
243 if security_service_type == 'active_directory':
244 return security_service
245 else:
246 LOG.warning("Only security services of type "
247 "'active_directory' are supported. "
248 "Retrieved security "
249 "service type: %(sec_type)s.",
250 {'sec_type': security_service_type})
251 return None
253 def _run_cloudbase_init_plugin_after_reboot(self, server, plugin_name):
254 cbs_init_reg_section = self._get_cbs_init_reg_section(server)
255 plugin_key_path = "%(cbs_init_section)s\\%(instance_id)s\\Plugins" % {
256 'cbs_init_section': cbs_init_reg_section,
257 'instance_id': server['instance_id']
258 }
259 self._windows_utils.set_win_reg_value(
260 server, path=plugin_key_path, key=plugin_name,
261 value=self._CBS_INIT_RUN_PLUGIN_AFTER_REBOOT)
263 def _get_cbs_init_reg_section(self, server):
264 base_path = 'hklm:\\SOFTWARE'
265 cbs_section = 'Cloudbase Solutions\\Cloudbase-Init'
267 for upper_section in ('', 'Wow6432Node'):
268 cbs_init_section = self._windows_utils.normalize_path(
269 os.path.join(base_path, upper_section, cbs_section))
270 try:
271 self._windows_utils.get_win_reg_value(
272 server, path=cbs_init_section)
273 return cbs_init_section
274 except processutils.ProcessExecutionError as ex:
275 # The exit code will always be '1' in case of errors, so the
276 # only way to determine the error type is checking stderr.
277 if 'Cannot find path' in ex.stderr:
278 continue
279 else:
280 raise
281 raise exception.ServiceInstanceException(
282 _("Could not retrieve Cloudbase Init registry section"))