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

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. 

15 

16import os 

17import re 

18 

19from oslo_concurrency import processutils 

20from oslo_config import cfg 

21from oslo_log import log 

22 

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 

28 

29 

30CONF = cfg.CONF 

31LOG = log.getLogger(__name__) 

32 

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] 

49 

50CONF = cfg.CONF 

51CONF.register_opts(windows_share_server_opts) 

52 

53 

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 

60 

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) 

65 

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() 

73 

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) 

79 

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

102 

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 

109 

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 

114 

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 

120 

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 

130 

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 

143 

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 

160 

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) 

169 

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

171 return instance_details 

172 

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'] 

178 

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

180 

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) 

195 

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}) 

224 

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

232 

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 

252 

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) 

262 

263 def _get_cbs_init_reg_section(self, server): 

264 base_path = 'hklm:\\SOFTWARE' 

265 cbs_section = 'Cloudbase Solutions\\Cloudbase-Init' 

266 

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