1# Licensed to the Apache Software Foundation (ASF) under one or more 2# contributor license agreements. See the NOTICE file distributed with 3# this work for additional information regarding copyright ownership. 4# The ASF licenses this file to You under the Apache License, Version 2.0 5# (the "License"); you may not use this file except in compliance with 6# the License. You may obtain 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, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16""" 17Common utilities for OpenStack 18""" 19 20from libcloud.utils.py3 import ET 21from libcloud.utils.py3 import httplib 22 23from libcloud.common.base import ConnectionUserAndKey, Response 24from libcloud.common.exceptions import BaseHTTPError 25from libcloud.common.types import ProviderError 26from libcloud.compute.types import (LibcloudError, MalformedResponseError) 27from libcloud.compute.types import KeyPairDoesNotExistError 28from libcloud.common.openstack_identity import (AUTH_TOKEN_HEADER, 29 get_class_for_auth_version) 30 31# Imports for backward compatibility reasons 32from libcloud.common.openstack_identity import (OpenStackServiceCatalog, 33 OpenStackIdentityTokenScope) 34 35 36try: 37 import simplejson as json 38except ImportError: 39 import json # type: ignore 40 41AUTH_API_VERSION = '1.1' 42 43# Auth versions which contain token expiration information. 44AUTH_VERSIONS_WITH_EXPIRES = [ 45 '1.1', 46 '2.0', 47 '2.0_apikey', 48 '2.0_password', 49 '3.x', 50 '3.x_password' 51] 52 53__all__ = [ 54 'OpenStackBaseConnection', 55 'OpenStackResponse', 56 'OpenStackException', 57 'OpenStackDriverMixin' 58] 59 60 61class OpenStackBaseConnection(ConnectionUserAndKey): 62 63 """ 64 Base class for OpenStack connections. 65 66 :param user_id: User name to use when authenticating 67 :type user_id: ``str`` 68 69 :param key: Secret to use when authenticating. 70 :type key: ``str`` 71 72 :param secure: Use HTTPS? (True by default.) 73 :type secure: ``bool`` 74 75 :param ex_force_base_url: Base URL for connection requests. If 76 not specified, this will be determined by 77 authenticating. 78 :type ex_force_base_url: ``str`` 79 80 :param ex_force_auth_url: Base URL for authentication requests. 81 :type ex_force_auth_url: ``str`` 82 83 :param ex_force_auth_version: Authentication version to use. If 84 not specified, defaults to AUTH_API_VERSION. 85 :type ex_force_auth_version: ``str`` 86 87 :param ex_force_auth_token: Authentication token to use for connection 88 requests. If specified, the connection will 89 not attempt to authenticate, and the value 90 of ex_force_base_url will be used to 91 determine the base request URL. If 92 ex_force_auth_token is passed in, 93 ex_force_base_url must also be provided. 94 :type ex_force_auth_token: ``str`` 95 96 :param token_scope: Whether to scope a token to a "project", a 97 "domain" or "unscoped". 98 :type token_scope: ``str`` 99 100 :param ex_domain_name: When authenticating, provide this domain name to 101 the identity service. A scoped token will be 102 returned. Some cloud providers require the domain 103 name to be provided at authentication time. Others 104 will use a default domain if none is provided. 105 :type ex_domain_name: ``str`` 106 107 :param ex_tenant_name: When authenticating, provide this tenant name to the 108 identity service. A scoped token will be returned. 109 Some cloud providers require the tenant name to be 110 provided at authentication time. Others will use a 111 default tenant if none is provided. 112 :type ex_tenant_name: ``str`` 113 114 :param ex_tenant_domain_id: When authenticating, provide this tenant 115 domain id to the identity service. 116 A scoped token will be returned. 117 Some cloud providers require the tenant 118 domain id to be provided at authentication 119 time. Others will use a default tenant 120 domain id if none is provided. 121 :type ex_tenant_domain_id: ``str`` 122 123 :param ex_force_service_type: Service type to use when selecting an 124 service. If not specified, a provider 125 specific default will be used. 126 :type ex_force_service_type: ``str`` 127 128 :param ex_force_service_name: Service name to use when selecting an 129 service. If not specified, a provider 130 specific default will be used. 131 :type ex_force_service_name: ``str`` 132 133 :param ex_force_service_region: Region to use when selecting an service. 134 If not specified, a provider specific 135 default will be used. 136 :type ex_force_service_region: ``str`` 137 138 :param ex_auth_cache: External cache where authentication tokens are 139 stored for reuse by other processes. Tokens are 140 always cached in memory on the driver instance. To 141 share tokens among multiple drivers, processes, or 142 systems, pass a cache here. 143 :type ex_auth_cache: :class:`OpenStackAuthenticationCache` 144 """ 145 146 auth_url = None # type: str 147 auth_token = None # type: str 148 auth_token_expires = None 149 auth_user_info = None 150 service_catalog = None 151 service_type = None 152 service_name = None 153 service_region = None 154 accept_format = None 155 _auth_version = None # type: str 156 157 def __init__(self, user_id, key, secure=True, 158 host=None, port=None, timeout=None, proxy_url=None, 159 ex_force_base_url=None, 160 ex_force_auth_url=None, 161 ex_force_auth_version=None, 162 ex_force_auth_token=None, 163 ex_token_scope=OpenStackIdentityTokenScope.PROJECT, 164 ex_domain_name='Default', 165 ex_tenant_name=None, 166 ex_tenant_domain_id='default', 167 ex_force_service_type=None, 168 ex_force_service_name=None, 169 ex_force_service_region=None, 170 ex_auth_cache=None, 171 retry_delay=None, backoff=None): 172 super(OpenStackBaseConnection, self).__init__( 173 user_id, key, secure=secure, timeout=timeout, 174 retry_delay=retry_delay, backoff=backoff, proxy_url=proxy_url) 175 176 if ex_force_auth_version: 177 self._auth_version = ex_force_auth_version 178 179 self.base_url = ex_force_base_url 180 self._ex_force_base_url = ex_force_base_url 181 self._ex_force_auth_url = ex_force_auth_url 182 self._ex_force_auth_token = ex_force_auth_token 183 self._ex_token_scope = ex_token_scope 184 self._ex_domain_name = ex_domain_name 185 self._ex_tenant_name = ex_tenant_name 186 self._ex_tenant_domain_id = ex_tenant_domain_id 187 self._ex_force_service_type = ex_force_service_type 188 self._ex_force_service_name = ex_force_service_name 189 self._ex_force_service_region = ex_force_service_region 190 self._ex_auth_cache = ex_auth_cache 191 self._osa = None 192 193 if ex_force_auth_token and not ex_force_base_url: 194 raise LibcloudError( 195 'Must also provide ex_force_base_url when specifying ' 196 'ex_force_auth_token.') 197 198 if ex_force_auth_token: 199 self.auth_token = ex_force_auth_token 200 201 if not self._auth_version: 202 self._auth_version = AUTH_API_VERSION 203 204 auth_url = self._get_auth_url() 205 206 if not auth_url: 207 raise LibcloudError('OpenStack instance must ' + 208 'have auth_url set') 209 210 def get_auth_class(self): 211 """ 212 Retrieve identity / authentication class instance. 213 214 :rtype: :class:`OpenStackIdentityConnection` 215 """ 216 if not self._osa: 217 auth_url = self._get_auth_url() 218 219 cls = get_class_for_auth_version(auth_version=self._auth_version) 220 self._osa = cls(auth_url=auth_url, 221 user_id=self.user_id, 222 key=self.key, 223 tenant_name=self._ex_tenant_name, 224 tenant_domain_id=self._ex_tenant_domain_id, 225 domain_name=self._ex_domain_name, 226 token_scope=self._ex_token_scope, 227 timeout=self.timeout, 228 proxy_url=self.proxy_url, 229 parent_conn=self, 230 auth_cache=self._ex_auth_cache) 231 232 return self._osa 233 234 def request(self, action, params=None, data='', headers=None, 235 method='GET', raw=False): 236 headers = headers or {} 237 params = params or {} 238 239 # Include default content-type for POST and PUT request (if available) 240 default_content_type = getattr(self, 'default_content_type', None) 241 if method.upper() in ['POST', 'PUT'] and default_content_type: 242 headers = {'Content-Type': default_content_type} 243 244 try: 245 return super().request(action=action, params=params, data=data, 246 method=method, headers=headers, raw=raw) 247 except BaseHTTPError as ex: 248 # Evict cached auth token if we receive Unauthorized while using it 249 if (ex.code == httplib.UNAUTHORIZED 250 and self._ex_force_auth_token is None): 251 self.get_auth_class().clear_cached_auth_context() 252 raise 253 254 def _get_auth_url(self): 255 """ 256 Retrieve auth url for this instance using either "ex_force_auth_url" 257 constructor kwarg of "auth_url" class variable. 258 """ 259 auth_url = self.auth_url 260 261 if self._ex_force_auth_url is not None: 262 auth_url = self._ex_force_auth_url 263 264 return auth_url 265 266 def get_service_catalog(self): 267 if self.service_catalog is None: 268 self._populate_hosts_and_request_paths() 269 270 return self.service_catalog 271 272 def get_service_name(self): 273 """ 274 Gets the service name used to look up the endpoint in the service 275 catalog. 276 277 :return: name of the service in the catalog 278 """ 279 if self._ex_force_service_name: 280 return self._ex_force_service_name 281 282 return self.service_name 283 284 def get_endpoint(self): 285 """ 286 Selects the endpoint to use based on provider specific values, 287 or overrides passed in by the user when setting up the driver. 288 289 :returns: url of the relevant endpoint for the driver 290 """ 291 service_type = self.service_type 292 service_name = self.service_name 293 service_region = self.service_region 294 295 if self._ex_force_service_type: 296 service_type = self._ex_force_service_type 297 if self._ex_force_service_name: 298 service_name = self._ex_force_service_name 299 if self._ex_force_service_region: 300 service_region = self._ex_force_service_region 301 302 endpoint = self.service_catalog.get_endpoint(service_type=service_type, 303 name=service_name, 304 region=service_region) 305 306 url = endpoint.url 307 308 if not url: 309 raise LibcloudError('Could not find specified endpoint') 310 311 return url 312 313 def add_default_headers(self, headers): 314 headers[AUTH_TOKEN_HEADER] = self.auth_token 315 headers['Accept'] = self.accept_format 316 return headers 317 318 def morph_action_hook(self, action): 319 self._populate_hosts_and_request_paths() 320 return super(OpenStackBaseConnection, self).morph_action_hook(action) 321 322 def _set_up_connection_info(self, url): 323 result = self._tuple_from_url(url) 324 (self.host, self.port, self.secure, self.request_path) = result 325 self.connect() 326 327 def _populate_hosts_and_request_paths(self): 328 """ 329 OpenStack uses a separate host for API calls which is only provided 330 after an initial authentication request. 331 """ 332 osa = self.get_auth_class() 333 334 if self._ex_force_auth_token: 335 # If ex_force_auth_token is provided we always hit the api directly 336 # and never try to authenticate. 337 # 338 # Note: When ex_force_auth_token is provided, ex_force_base_url 339 # must be provided as well. 340 self._set_up_connection_info(url=self._ex_force_base_url) 341 return 342 343 if not osa.is_token_valid(): 344 # Token is not available or it has expired. Need to retrieve a 345 # new one. 346 if self._auth_version == '2.0_apikey': 347 kwargs = {'auth_type': 'api_key'} 348 elif self._auth_version == '2.0_password': 349 kwargs = {'auth_type': 'password'} 350 else: 351 kwargs = {} 352 353 # pylint: disable=unexpected-keyword-arg 354 osa = osa.authenticate(**kwargs) # may throw InvalidCreds 355 # pylint: enable=unexpected-keyword-arg 356 357 self.auth_token = osa.auth_token 358 self.auth_token_expires = osa.auth_token_expires 359 self.auth_user_info = osa.auth_user_info 360 361 # Pull out and parse the service catalog 362 osc = OpenStackServiceCatalog(service_catalog=osa.urls, 363 auth_version=self._auth_version) 364 self.service_catalog = osc 365 366 url = self._ex_force_base_url or self.get_endpoint() 367 self._set_up_connection_info(url=url) 368 369 370class OpenStackException(ProviderError): 371 pass 372 373 374class OpenStackResponse(Response): 375 node_driver = None 376 377 def success(self): 378 i = int(self.status) 379 return 200 <= i <= 299 380 381 def has_content_type(self, content_type): 382 content_type_value = self.headers.get('content-type') or '' 383 content_type_value = content_type_value.lower() 384 return content_type_value.find(content_type.lower()) > -1 385 386 def parse_body(self): 387 if self.status == httplib.NO_CONTENT or not self.body: 388 return None 389 390 if self.has_content_type('application/xml'): 391 try: 392 return ET.XML(self.body) 393 except Exception: 394 raise MalformedResponseError( 395 'Failed to parse XML', 396 body=self.body, 397 driver=self.node_driver) 398 399 elif self.has_content_type('application/json'): 400 try: 401 return json.loads(self.body) 402 except Exception: 403 raise MalformedResponseError( 404 'Failed to parse JSON', 405 body=self.body, 406 driver=self.node_driver) 407 else: 408 return self.body 409 410 def parse_error(self): 411 body = self.parse_body() 412 413 if self.has_content_type('application/xml'): 414 text = '; '.join([err.text or '' for err in body.getiterator() 415 if err.text]) 416 elif self.has_content_type('application/json'): 417 values = list(body.values()) 418 419 context = self.connection.context 420 driver = self.connection.driver 421 key_pair_name = context.get('key_pair_name', None) 422 423 if len(values) > 0 and 'code' in values[0] and \ 424 values[0]['code'] == 404 and key_pair_name: 425 raise KeyPairDoesNotExistError(name=key_pair_name, 426 driver=driver) 427 elif len(values) > 0 and 'message' in values[0]: 428 text = ';'.join([fault_data['message'] for fault_data 429 in values]) 430 else: 431 text = body 432 else: 433 # while we hope a response is always one of xml or json, we have 434 # seen html or text in the past, its not clear we can really do 435 # something to make it more readable here, so we will just pass 436 # it along as the whole response body in the text variable. 437 text = body 438 439 return '%s %s %s' % (self.status, self.error, text) 440 441 442class OpenStackDriverMixin(object): 443 444 def __init__(self, 445 ex_force_base_url=None, 446 ex_force_auth_url=None, 447 ex_force_auth_version=None, 448 ex_force_auth_token=None, 449 ex_token_scope=OpenStackIdentityTokenScope.PROJECT, 450 ex_domain_name='Default', 451 ex_tenant_name=None, 452 ex_tenant_domain_id='default', 453 ex_force_service_type=None, 454 ex_force_service_name=None, 455 ex_force_service_region=None, 456 ex_auth_cache=None, *args, **kwargs): 457 self._ex_force_base_url = ex_force_base_url 458 self._ex_force_auth_url = ex_force_auth_url 459 self._ex_force_auth_version = ex_force_auth_version 460 self._ex_force_auth_token = ex_force_auth_token 461 self._ex_token_scope = ex_token_scope 462 self._ex_domain_name = ex_domain_name 463 self._ex_tenant_name = ex_tenant_name 464 self._ex_tenant_domain_id = ex_tenant_domain_id 465 self._ex_force_service_type = ex_force_service_type 466 self._ex_force_service_name = ex_force_service_name 467 self._ex_force_service_region = ex_force_service_region 468 self._ex_auth_cache = ex_auth_cache 469 470 def openstack_connection_kwargs(self): 471 """ 472 Returns certain ``ex_*`` parameters for this connection. 473 474 :rtype: ``dict`` 475 """ 476 rv = {} 477 if self._ex_force_base_url: 478 rv['ex_force_base_url'] = self._ex_force_base_url 479 if self._ex_force_auth_token: 480 rv['ex_force_auth_token'] = self._ex_force_auth_token 481 if self._ex_force_auth_url: 482 rv['ex_force_auth_url'] = self._ex_force_auth_url 483 if self._ex_force_auth_version: 484 rv['ex_force_auth_version'] = self._ex_force_auth_version 485 if self._ex_token_scope: 486 rv['ex_token_scope'] = self._ex_token_scope 487 if self._ex_domain_name: 488 rv['ex_domain_name'] = self._ex_domain_name 489 if self._ex_tenant_name: 490 rv['ex_tenant_name'] = self._ex_tenant_name 491 if self._ex_tenant_domain_id: 492 rv['ex_tenant_domain_id'] = self._ex_tenant_domain_id 493 if self._ex_force_service_type: 494 rv['ex_force_service_type'] = self._ex_force_service_type 495 if self._ex_force_service_name: 496 rv['ex_force_service_name'] = self._ex_force_service_name 497 if self._ex_force_service_region: 498 rv['ex_force_service_region'] = self._ex_force_service_region 499 if self._ex_auth_cache is not None: 500 rv['ex_auth_cache'] = self._ex_auth_cache 501 return rv 502