1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693
|
# 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.
"""
The :class:`~openstack.connection.Connection` class is the primary interface
to the Python SDK. It maintains a context for a connection to a region of
a cloud provider. The :class:`~openstack.connection.Connection` has an
attribute to access each OpenStack service.
At a minimum, the :class:`~openstack.connection.Connection` class needs to be
created with a config or the parameters to build one.
While the overall system is very flexible, there are four main use cases
for different ways to create a :class:`~openstack.connection.Connection`.
* Using config settings and keyword arguments as described in
:ref:`openstack-config`
* Using only keyword arguments passed to the constructor ignoring config files
and environment variables.
* Using an existing authenticated `keystoneauth1.session.Session`, such as
might exist inside of an OpenStack service operational context.
* Using an existing :class:`~openstack.config.cloud_region.CloudRegion`.
Creating the Connection
-----------------------
Using config settings
~~~~~~~~~~~~~~~~~~~~~
For users who want to create a :class:`~openstack.connection.Connection` making
use of named clouds in ``clouds.yaml`` files, ``OS_`` environment variables
and python keyword arguments, the :func:`openstack.connect` factory function
is the recommended way to go:
.. code-block:: python
import openstack
conn = openstack.connect(cloud='example', region_name='earth1')
If the application in question is a command line application that should also
accept command line arguments, an `argparse.Namespace` can be passed to
:func:`openstack.connect` that will have relevant arguments added to it and
then subsequently consumed by the constructor:
.. code-block:: python
import argparse
import openstack
options = argparse.ArgumentParser(description='Awesome OpenStack App')
conn = openstack.connect(options=options)
Using only keyword arguments
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If the application wants to avoid loading any settings from ``clouds.yaml`` or
environment variables, use the :class:`~openstack.connection.Connection`
constructor directly. As long as the ``cloud`` argument is omitted or ``None``,
the :class:`~openstack.connection.Connection` constructor will not load
settings from files or the environment.
.. note::
This is a different default behavior than the :func:`~openstack.connect`
factory function. In :func:`~openstack.connect` if ``cloud`` is omitted
or ``None``, a default cloud will be loaded, defaulting to the ``envvars``
cloud if it exists.
.. code-block:: python
from openstack import connection
conn = connection.Connection(
region_name='example-region',
auth={
'auth_url': 'https://auth.example.com',
'username': 'amazing-user',
'password': 'super-secret-password',
'project_id': '33aa1afc-03fe-43b8-8201-4e0d3b4b8ab5',
'user_domain_id': '054abd68-9ad9-418b-96d3-3437bb376703',
},
compute_api_version='2',
identity_interface='internal',
)
Per-service settings as needed by `keystoneauth1.adapter.Adapter` such as
``api_version``, ``service_name``, and ``interface`` can be set, as seen
above, by prefixing them with the official ``service-type`` name of the
service. ``region_name`` is a setting for the entire
:class:`~openstack.config.cloud_region.CloudRegion` and cannot be set per
service.
From existing authenticated Session
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
For applications that already have an authenticated Session, simply passing
it to the :class:`~openstack.connection.Connection` constructor is all that
is needed:
.. code-block:: python
from openstack import connection
conn = connection.Connection(
session=session,
region_name='example-region',
compute_api_version='2',
identity_interface='internal',
)
From oslo.conf CONF object
~~~~~~~~~~~~~~~~~~~~~~~~~~
For applications that have an oslo.config ``CONF`` object that has been
populated with ``keystoneauth1.loading.register_adapter_conf_options`` in
groups named by the OpenStack service's project name, it is possible to
construct a Connection with the ``CONF`` object and an authenticated Session.
.. note::
This is primarily intended for use by OpenStack services to talk amongst
themselves.
.. code-block:: python
from keystoneauth1 import loading as ks_loading
from oslo_config import cfg
from openstack import connection
CONF = cfg.CONF
group = cfg.OptGroup('neutron')
ks_loading.register_session_conf_options(CONF, group)
ks_loading.register_auth_conf_options(CONF, group)
ks_loading.register_adapter_conf_options(CONF, group)
CONF()
auth = ks_loading.load_auth_from_conf_options(CONF, 'neutron')
sess = ks_loading.load_session_from_conf_options(CONF, 'neutron', auth=auth)
conn = connection.Connection(
session=sess,
oslo_conf=CONF,
)
This can then be used with an appropriate configuration file.
.. code-block:: ini
[neutron]
region_name = RegionOne
auth_strategy = keystone
project_domain_name = Default
project_name = service
user_domain_name = Default
password = password
username = neutron
auth_url = http://10.0.110.85/identity
auth_type = password
service_metadata_proxy = True
default_floating_pool = public
You may also wish to configure a service user. As discussed in the `Keystone
documentation`__, service users are users with specific roles that identify the
user as a service. The use of service users can avoid issues caused by the
expiration of the original user's token during long running operations, as a
fresh token issued for the service user will always accompany the user's token,
which may have expired.
.. code-block:: python
from keystoneauth1 import loading as ks_loading
from keystoneauth1 import service_token
from oslo_config import cfg
import openstack
from openstack import connection
CONF = cfg.CONF
neutron_group = cfg.OptGroup('neutron')
ks_loading.register_session_conf_options(CONF, neutron_group)
ks_loading.register_auth_conf_options(CONF, neutron_group)
ks_loading.register_adapter_conf_options(CONF, neutron_group)
service_group = cfg.OptGroup('service_user')
ks_loading.register_session_conf_options(CONF, service_group)
ks_loading.register_auth_conf_options(CONF, service_group)
CONF()
user_auth = ks_loading.load_auth_from_conf_options(CONF, 'neutron')
service_auth = ks_loading.load_auth_from_conf_options(CONF, 'service_user')
auth = service_token.ServiceTokenAuthWrapper(user_auth, service_auth)
sess = ks_loading.load_session_from_conf_options(CONF, 'neutron', auth=auth)
conn = connection.Connection(
session=sess,
oslo_conf=CONF,
)
This will necessitate an additional section in the configuration file used.
.. code-block:: ini
[service_user]
auth_strategy = keystone
project_domain_name = Default
project_name = service
user_domain_name = Default
password = password
username = nova
auth_url = http://10.0.110.85/identity
auth_type = password
.. __: https://docs.openstack.org/keystone/latest/admin/manage-services.html
From existing CloudRegion
~~~~~~~~~~~~~~~~~~~~~~~~~
If you already have an :class:`~openstack.config.cloud_region.CloudRegion`
you can pass it in instead:
.. code-block:: python
from openstack import connection
import openstack.config
config = openstack.config.get_cloud_region(
cloud='example',
region_name='earth',
)
conn = connection.Connection(config=config)
Using the Connection
--------------------
Services are accessed through an attribute named after the service's official
service-type.
List
~~~~
An iterator containing a list of all the projects is retrieved in this manner:
.. code-block:: python
projects = conn.identity.projects()
Find or create
~~~~~~~~~~~~~~
If you wanted to make sure you had a network named 'zuul', you would first
try to find it and if that fails, you would create it::
network = conn.network.find_network("zuul")
if network is None:
network = conn.network.create_network(name="zuul")
Additional information about the services can be found in the
:ref:`service-proxies` documentation.
"""
import copy
import importlib.metadata as importlib_metadata
import warnings
import keystoneauth1.exceptions
import requestsexceptions
from openstack import _log
from openstack.cloud import _accelerator
from openstack.cloud import _baremetal
from openstack.cloud import _block_storage
from openstack.cloud import _coe
from openstack.cloud import _compute
from openstack.cloud import _dns
from openstack.cloud import _identity
from openstack.cloud import _image
from openstack.cloud import _network
from openstack.cloud import _object_store
from openstack.cloud import _orchestration
from openstack.cloud import _shared_file_system
from openstack import config as _config
import openstack.config.cloud_region
from openstack import exceptions
from openstack import service_description
__all__ = [
'from_config',
'Connection',
]
if requestsexceptions.SubjectAltNameWarning:
warnings.filterwarnings(
'ignore', category=requestsexceptions.SubjectAltNameWarning
)
_logger = _log.setup_logging('openstack')
def from_config(cloud=None, config=None, options=None, **kwargs):
"""Create a Connection using openstack.config
:param str cloud:
Use the `cloud` configuration details when creating the Connection.
:param openstack.config.cloud_region.CloudRegion config:
An existing CloudRegion configuration. If no `config` is provided,
`openstack.config.OpenStackConfig` will be called, and the provided
`name` will be used in determining which cloud's configuration
details will be used in creation of the `Connection` instance.
:param argparse.Namespace options:
Allows direct passing in of options to be added to the cloud config.
This does not have to be an actual instance of argparse.Namespace,
despite the naming of the
`openstack.config.loader.OpenStackConfig.get_one` argument to which
it is passed.
:rtype: :class:`~openstack.connection.Connection`
"""
# TODO(mordred) Backwards compat while we transition
cloud = kwargs.pop('cloud_name', cloud)
config = kwargs.pop('cloud_config', config)
if config is None:
config = _config.OpenStackConfig().get_one(
cloud=cloud, argparse=options, **kwargs
)
return Connection(config=config)
class Connection(
_accelerator.AcceleratorCloudMixin,
_baremetal.BaremetalCloudMixin,
_block_storage.BlockStorageCloudMixin,
_compute.ComputeCloudMixin,
_coe.CoeCloudMixin,
_dns.DnsCloudMixin,
_identity.IdentityCloudMixin,
_image.ImageCloudMixin,
_network.NetworkCloudMixin,
_object_store.ObjectStoreCloudMixin,
_orchestration.OrchestrationCloudMixin,
_shared_file_system.SharedFileSystemCloudMixin,
):
def __init__(
self,
cloud=None,
config=None,
session=None,
app_name=None,
app_version=None,
extra_services=None,
strict=False,
use_direct_get=None,
task_manager=None,
rate_limit=None,
oslo_conf=None,
service_types=None,
global_request_id=None,
strict_proxies=False,
pool_executor=None,
**kwargs,
):
"""Create a connection to a cloud.
A connection needs information about how to connect, how to
authenticate and how to select the appropriate services to use.
The recommended way to provide this information is by referencing
a named cloud config from an existing `clouds.yaml` file. The cloud
name ``envvars`` may be used to consume a cloud configured via ``OS_``
environment variables.
A pre-existing :class:`~openstack.config.cloud_region.CloudRegion`
object can be passed in lieu of a cloud name, for cases where the user
already has a fully formed CloudRegion and just wants to use it.
Similarly, if for some reason the user already has a
:class:`~keystoneauth1.session.Session` and wants to use it, it may be
passed in.
:param str cloud: Name of the cloud from config to use.
:param config: CloudRegion object representing the config for the
region of the cloud in question.
:type config: :class:`~openstack.config.cloud_region.CloudRegion`
:param session: A session object compatible with
:class:`~keystoneauth1.session.Session`.
:type session: :class:`~keystoneauth1.session.Session`
:param str app_name: Name of the application to be added to User Agent.
:param str app_version: Version of the application to be added to
User Agent.
:param extra_services: List of
:class:`~openstack.service_description.ServiceDescription`
objects describing services that openstacksdk otherwise does not
know about.
:param bool use_direct_get:
For get methods, make specific REST calls for server-side
filtering instead of making list calls and filtering client-side.
Default false.
:param task_manager:
Ignored. Exists for backwards compat during transition. Rate limit
parameters should be passed directly to the `rate_limit` parameter.
:param rate_limit:
Client-side rate limit, expressed in calls per second. The
parameter can either be a single float, or it can be a dict with
keys as service-type and values as floats expressing the calls
per second for that service. Defaults to None, which means no
rate-limiting is performed.
:param oslo_conf: An oslo.config ``CONF`` object that has been
populated with
``keystoneauth1.loading.register_adapter_conf_options`` in
groups named by the OpenStack service's project name.
:type oslo_conf: :class:`~oslo_config.cfg.ConfigOpts`
:param service_types:
A list/set of service types this Connection should support. All
other service types will be disabled (will error if used).
**Currently only supported in conjunction with the ``oslo_conf``
kwarg.**
:param strict_proxies:
Throw an ``openstack.exceptions.ServiceDiscoveryException`` if the
endpoint for a given service doesn't work. This is useful for
OpenStack services using sdk to talk to other OpenStack services
where it can be expected that the deployer config is correct and
errors should be reported immediately.
Default false.
:type strict_proxies: bool
:param global_request_id: A Request-id to send with all interactions.
:type global_request_id: str
:param pool_executor:
A futurist ``Executor`` object to be used for concurrent background
activities. Defaults to None in which case a ThreadPoolExecutor
will be created if needed.
:type pool_executor: :class:`~futurist.Executor`
:param kwargs: If a config is not provided, the rest of the parameters
provided are assumed to be arguments to be passed to the
CloudRegion constructor.
"""
super().__init__(
cloud=cloud,
config=config,
session=session,
app_name=app_name,
app_version=app_version,
extra_services=extra_services,
strict=strict,
use_direct_get=use_direct_get,
task_manager=task_manager,
rate_limit=rate_limit,
oslo_conf=oslo_conf,
service_types=service_types,
global_request_id=global_request_id,
strict_proxies=strict_proxies,
pool_executor=pool_executor,
**kwargs,
)
# Allow vendors to provide hooks. They will normally only receive a
# connection object and a responsible to register additional services
vendor_hook = kwargs.get('vendor_hook')
if not vendor_hook and 'vendor_hook' in self.config.config:
# Get the one from profile
vendor_hook = self.config.config.get('vendor_hook')
if vendor_hook:
try:
# NOTE(gtema): no class name in the hook, plain module:function
# Split string hook into module and function
try:
package_name, function = vendor_hook.rsplit(':')
if package_name and function:
ep = importlib_metadata.EntryPoint(
name='vendor_hook',
value=vendor_hook,
group='vendor_hook',
)
hook = ep.load()
hook(self)
except ValueError:
self.log.warning(
'Hook should be in the entrypoint '
'module:attribute format'
)
except (ImportError, TypeError, AttributeError) as e:
self.log.warning(
'Configured hook %s cannot be executed: %s', vendor_hook, e
)
# Add additional metrics into the configuration according to the
# selected connection. We don't want to deal with overall config in the
# proxy, just pass required part.
if (
self.config._influxdb_config
and 'additional_metric_tags' in self.config.config
):
self.config._influxdb_config['additional_metric_tags'] = (
self.config.config['additional_metric_tags']
)
def add_service(self, service):
"""Add a service to the Connection.
Attaches an instance of the :class:`~openstack.proxy.Proxy`
class contained in
:class:`~openstack.service_description.ServiceDescription`.
The :class:`~openstack.proxy.Proxy` will be attached to the
`Connection` by its ``service_type`` and by any ``aliases`` that
may be specified.
:param openstack.service_description.ServiceDescription service:
Object describing the service to be attached. As a convenience,
if ``service`` is a string it will be treated as a ``service_type``
and a basic
:class:`~openstack.service_description.ServiceDescription`
will be created.
"""
# If we don't have a proxy, just instantiate Proxy so that
# we get an adapter.
if isinstance(service, str):
service = service_description.ServiceDescription(service)
# Directly invoke descriptor of the ServiceDescription
def getter(self):
return service.__get__(self, service)
# Register the ServiceDescription class (as property)
# with every known alias for a "runtime descriptor"
for attr_name in service.all_types:
setattr(
self.__class__,
attr_name.replace('-', '_'),
property(fget=getter),
)
self.config.enable_service(service.service_type)
def authorize(self):
"""Authorize this Connection
.. note::
This method is optional. When an application makes a call to any
OpenStack service, this method allows you to request a token
manually before attempting to do anything else.
:returns: A string token.
:raises: :class:`~openstack.exceptions.HttpException` if the
authorization fails due to reasons like the credentials
provided are unable to be authorized or the `auth_type`
argument is missing, etc.
"""
try:
return self.session.get_token()
except keystoneauth1.exceptions.ClientException as e:
raise exceptions.SDKException(str(e))
def connect_as(self, **kwargs):
"""Make a new Connection object with new auth context.
Take the existing settings from the current cloud and construct a new
Connection object with some of the auth settings overridden. This
is useful for getting an object to perform tasks with as another user,
or in the context of a different project.
.. code-block:: python
conn = openstack.connect(cloud='example')
# Work normally
servers = conn.list_servers()
conn2 = conn.connect_as(username='different-user', password='')
# Work as different-user
servers = conn2.list_servers()
:param kwargs: keyword arguments can contain anything that would
normally go in an auth dict. They will override the same settings
from the parent cloud as appropriate. Entries that do not want to
be overridden can be ommitted.
"""
if self.config._openstack_config:
config = self.config._openstack_config
else:
# TODO(mordred) Replace this with from_session
config = openstack.config.OpenStackConfig(
app_name=self.config._app_name,
app_version=self.config._app_version,
load_yaml_config=False,
)
params = copy.deepcopy(self.config.config)
# Remove profile from current cloud so that overridding works
params.pop('profile', None)
# Utility function to help with the stripping below.
def pop_keys(params, auth, name_key, id_key):
if name_key in auth or id_key in auth:
params['auth'].pop(name_key, None)
params['auth'].pop(id_key, None)
# If there are user, project or domain settings in the incoming auth
# dict, strip out both id and name so that a user can say:
# cloud.connect_as(project_name='foo')
# and have that work with clouds that have a project_id set in their
# config.
for prefix in ('user', 'project'):
if prefix == 'user':
name_key = 'username'
else:
name_key = 'project_name'
id_key = f'{prefix}_id'
pop_keys(params, kwargs, name_key, id_key)
id_key = f'{prefix}_domain_id'
name_key = f'{prefix}_domain_name'
pop_keys(params, kwargs, name_key, id_key)
for key, value in kwargs.items():
params['auth'][key] = value
cloud_region = config.get_one(**params)
# Attach the discovery cache from the old session so we won't
# double discover.
cloud_region._discovery_cache = self.session._discovery_cache
# Override the cloud name so that logging/location work right
cloud_region._name = self.name
cloud_region.config['profile'] = self.name
# Use self.__class__ so that we return whatever this if, like if it's
# a subclass in the case of shade wrapping sdk.
return self.__class__(config=cloud_region)
def connect_as_project(self, project):
"""Make a new Connection object with a new project.
Take the existing settings from the current cloud and construct a new
Connection object with the project settings overridden. This
is useful for getting an object to perform tasks with as another user,
or in the context of a different project.
.. code-block:: python
cloud = openstack.connect(cloud='example')
# Work normally
servers = cloud.list_servers()
cloud2 = cloud.connect_as_project('different-project')
# Work in different-project
servers = cloud2.list_servers()
:param project: Either a project name or a project dict as returned by
``list_projects``.
"""
auth = {}
if isinstance(project, dict):
auth['project_id'] = project.get('id')
auth['project_name'] = project.get('name')
if project.get('domain_id'):
auth['project_domain_id'] = project['domain_id']
else:
auth['project_name'] = project
return self.connect_as(**auth)
def endpoint_for(self, service_type, interface=None, region_name=None):
"""Return the endpoint for a given service.
Respects config values for Connection, including
``*_endpoint_override``. For direct values from the catalog regardless
of overrides, see
:meth:`~openstack.config.cloud_region.CloudRegion.get_endpoint_from_catalog`
:param service_type: Service Type of the endpoint to search for.
:param interface: Interface of the endpoint to search for. Optional,
defaults to the configured value for interface for this Connection.
:param region_name: Region Name of the endpoint to search for.
Optional, defaults to the configured value for region_name for this
Connection.
:returns: The endpoint of the service, or None if not found.
"""
endpoint_override = self.config.get_endpoint(service_type)
if endpoint_override:
return endpoint_override
return self.config.get_endpoint_from_catalog(
service_type=service_type,
interface=interface,
region_name=region_name,
)
|