rally/rally/osclients.py

743 lines
30 KiB
Python

# Copyright 2013: Mirantis Inc.
# 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 abc
import os
from oslo_config import cfg
from rally.common.i18n import _
from rally.common import log as logging
from rally.common import objects
from rally.common.plugin import plugin
from rally import consts
from rally import exceptions
CONF = cfg.CONF
OSCLIENTS_OPTS = [
cfg.FloatOpt("openstack_client_http_timeout", default=180.0,
help="HTTP timeout for any of OpenStack service in seconds"),
cfg.BoolOpt("https_insecure", default=False,
help="Use SSL for all OpenStack API interfaces",
deprecated_for_removal=True),
cfg.StrOpt("https_cacert",
help="Path to CA server certificate for SSL",
deprecated_for_removal=True)
]
CONF.register_opts(OSCLIENTS_OPTS)
_NAMESPACE = "openstack"
def configure(name, default_version=None, default_service_type=None,
supported_versions=None):
"""OpenStack client class wrapper.
Each client class has to be wrapped by configure() wrapper. It
sets essential configuration of client classes.
:param name: Name of the client
:param default_version: Default version for client
:param default_service_type: Default service type of endpoint(If this
variable is not specified, validation will assume that your client
doesn't allow to specify service type.
:param supported_versions: List of supported versions(If this variable is
not specified, `OSClients.validate_version` method will raise an
exception that client doesn't support setting any versions. If this
logic is wrong for your client, you should override `validate_version`
in client object)
"""
def wrapper(cls):
cls = plugin.configure(name=name, namespace=_NAMESPACE)(cls)
cls._meta_set("default_version", default_version)
cls._meta_set("default_service_type", default_service_type)
cls._meta_set("supported_versions", supported_versions or [])
return cls
return wrapper
class OSClient(plugin.Plugin):
def __init__(self, endpoint, api_info, cache_obj):
self.endpoint = endpoint
self.api_info = api_info
self.cache = cache_obj
def choose_version(self, version=None):
"""Return version string.
Choose version between transmitted(preferable value if present),
version from api_info(configured from a context) and default.
"""
# NOTE(andreykurilin): The result of choose is converted to string,
# since most of clients contain map for versioned modules, where a key
# is a string value of version. Example of map and its usage:
#
# from oslo_utils import importutils
# ...
# version_map = {"1": "someclient.v1.client.Client",
# "2": "someclient.v2.client.Client"}
#
# def Client(version, *args, **kwargs):
# cls = importutils.import_class(version_map[version])
# return cls(*args, **kwargs)
#
# That is why type of version so important and we should ensure that
# version is a string object.
# For those clients which doesn't accept string value(for example
# zaqarclient), this method should be overridden.
return str(version
or self.api_info.get(self.get_name(), {}).get("version")
or self._meta_get("default_version"))
@classmethod
def get_supported_versions(cls):
return cls._meta_get("supported_versions")
@classmethod
def validate_version(cls, version):
supported_versions = cls.get_supported_versions()
if supported_versions:
if str(version) not in supported_versions:
raise exceptions.ValidationError(_(
"'%(vers)s' is not supported. Should be one of "
"'%(supported)s'") % {"vers": version,
"supported": supported_versions})
else:
raise exceptions.RallyException(
_("Setting version is not supported."))
try:
float(version)
except ValueError:
raise exceptions.ValidationError(_(
"'%s' is invalid. Should be numeric value.") % version)
def choose_service_type(self, service_type=None):
"""Return service_type string.
Choose service type between transmitted(preferable value if present),
service type from api_info(configured from a context) and default.
"""
return (service_type
or self.api_info.get(self.get_name(), {}).get("service_type")
or self._meta_get("default_service_type"))
@classmethod
def is_service_type_configurable(cls):
"""Just checks that client supports setting service type."""
if cls._meta_get("default_service_type") is None:
raise exceptions.RallyException(_(
"Setting service type is not supported."))
def keystone(self, *args, **kwargs):
"""Make a call to keystone client."""
keystone = OSClient.get("keystone")(self.endpoint, self.api_info,
self.cache)
return keystone(*args, **kwargs)
def _get_session(self, auth=None, endpoint=None):
endpoint = endpoint or self._get_endpoint()
from keystoneclient.auth import token_endpoint
from keystoneclient import session as ks_session
kc = self.keystone()
if auth is None:
auth = token_endpoint.Token(endpoint, kc.auth_token)
return ks_session.Session(auth=auth, verify=self.endpoint.insecure,
timeout=CONF.openstack_client_http_timeout)
def _get_endpoint(self, service_type=None):
kc = self.keystone()
api_url = kc.service_catalog.url_for(
service_type=self.choose_service_type(service_type),
endpoint_type=self.endpoint.endpoint_type,
region_name=self.endpoint.region_name)
return api_url
def _get_auth_info(self, user_key="username",
password_key="password",
auth_url_key="auth_url",
project_name_key="project_id"
):
kw = {
user_key: self.endpoint.username,
password_key: self.endpoint.password,
auth_url_key: self.endpoint.auth_url
}
if project_name_key:
kw.update({project_name_key: self.endpoint.tenant_name})
return kw
@abc.abstractmethod
def create_client(self, *args, **kwargs):
"""Create new instance of client."""
def __call__(self, *args, **kwargs):
"""Return initialized client instance."""
key = "{0}{1}{2}".format(self.get_name(),
str(args) if args else "",
str(kwargs) if kwargs else "")
if key not in self.cache:
self.cache[key] = self.create_client(*args, **kwargs)
return self.cache[key]
@classmethod
def get(cls, name, namespace=_NAMESPACE):
return super(OSClient, cls).get(name, namespace)
@configure("keystone")
class Keystone(OSClient):
def keystone(self, *args, **kwargs):
raise exceptions.RallyException(_("Method 'keystone' is restricted "
"for keystoneclient. :)"))
@staticmethod
def _create_keystone_client(args):
from keystoneclient import discover as keystone_discover
discover = keystone_discover.Discover(**args)
for version_data in discover.version_data():
version = version_data["version"]
if version[0] <= 2:
from keystoneclient.v2_0 import client as keystone_v2
return keystone_v2.Client(**args)
elif version[0] == 3:
from keystoneclient.v3 import client as keystone_v3
return keystone_v3.Client(**args)
raise exceptions.RallyException("Failed to discover keystone version "
"for url %(auth_url)s.", **args)
def create_client(self):
"""Return keystone client."""
new_kw = {
"timeout": CONF.openstack_client_http_timeout,
"insecure": self.endpoint.insecure, "cacert": self.endpoint.cacert
}
kw = self.endpoint.to_dict()
kw.update(new_kw)
client = self._create_keystone_client(kw)
if client.auth_ref is None:
client.authenticate()
return client
@configure("nova", default_version="2", default_service_type="compute")
class Nova(OSClient):
@classmethod
def validate_version(cls, version):
from novaclient import api_versions
from novaclient import exceptions as nova_exc
try:
api_versions.get_api_version(version)
except nova_exc.UnsupportedVersion:
raise exceptions.RallyException(
"Version string '%s' is unsupported." % version)
def create_client(self, version=None, service_type=None):
"""Return nova client."""
from novaclient import client as nova
kc = self.keystone()
compute_api_url = kc.service_catalog.url_for(
service_type=self.choose_service_type(service_type),
endpoint_type=self.endpoint.endpoint_type,
region_name=self.endpoint.region_name)
client = nova.Client(self.choose_version(version),
auth_token=kc.auth_token,
http_log_debug=logging.is_debug(),
timeout=CONF.openstack_client_http_timeout,
insecure=self.endpoint.insecure,
cacert=self.endpoint.cacert,
**self._get_auth_info(password_key="api_key"))
client.set_management_url(compute_api_url)
return client
@configure("neutron", default_version="2.0", default_service_type="network",
supported_versions=["2.0"])
class Neutron(OSClient):
def create_client(self, version=None, service_type=None):
"""Return neutron client."""
from neutronclient.neutron import client as neutron
kc = self.keystone()
network_api_url = kc.service_catalog.url_for(
service_type=self.choose_service_type(service_type),
endpoint_type=self.endpoint.endpoint_type,
region_name=self.endpoint.region_name)
client = neutron.Client(self.choose_version(version),
token=kc.auth_token,
endpoint_url=network_api_url,
timeout=CONF.openstack_client_http_timeout,
insecure=self.endpoint.insecure,
ca_cert=self.endpoint.cacert,
**self._get_auth_info(
project_name_key="tenant_name")
)
return client
@configure("glance", default_version="1", default_service_type="image",
supported_versions=["1", "2"])
class Glance(OSClient):
def create_client(self, version=None, service_type=None):
"""Return glance client."""
import glanceclient as glance
kc = self.keystone()
image_api_url = kc.service_catalog.url_for(
service_type=self.choose_service_type(service_type),
endpoint_type=self.endpoint.endpoint_type,
region_name=self.endpoint.region_name)
client = glance.Client(self.choose_version(version),
endpoint=image_api_url,
token=kc.auth_token,
timeout=CONF.openstack_client_http_timeout,
insecure=self.endpoint.insecure,
cacert=self.endpoint.cacert)
return client
@configure("heat", default_version="1", default_service_type="orchestration",
supported_versions=["1"])
class Heat(OSClient):
def create_client(self, version=None, service_type=None):
"""Return heat client."""
from heatclient import client as heat
kc = self.keystone()
orchestration_api_url = kc.service_catalog.url_for(
service_type=self.choose_service_type(service_type),
endpoint_type=self.endpoint.endpoint_type,
region_name=self.endpoint.region_name)
client = heat.Client(self.choose_version(version),
endpoint=orchestration_api_url,
token=kc.auth_token,
timeout=CONF.openstack_client_http_timeout,
insecure=self.endpoint.insecure,
cacert=self.endpoint.cacert,
**self._get_auth_info(project_name_key=None))
return client
@configure("cinder", default_version="1", default_service_type="volume",
supported_versions=["1", "2"])
class Cinder(OSClient):
def create_client(self, version=None, service_type=None):
"""Return cinder client."""
from cinderclient import client as cinder
client = cinder.Client(self.choose_version(version),
http_log_debug=logging.is_debug(),
timeout=CONF.openstack_client_http_timeout,
insecure=self.endpoint.insecure,
cacert=self.endpoint.cacert,
**self._get_auth_info(password_key="api_key"))
kc = self.keystone()
volume_api_url = kc.service_catalog.url_for(
service_type=self.choose_service_type(service_type),
endpoint_type=self.endpoint.endpoint_type,
region_name=self.endpoint.region_name)
client.client.management_url = volume_api_url
client.client.auth_token = kc.auth_token
return client
@configure("manila", default_version="1", default_service_type="share",
supported_versions=["1", "2"])
class Manila(OSClient):
def create_client(self, version=None, service_type=None):
"""Return manila client."""
from manilaclient import client as manila
manila_client = manila.Client(
self.choose_version(version),
region_name=self.endpoint.region_name,
http_log_debug=logging.is_debug(),
timeout=CONF.openstack_client_http_timeout,
insecure=self.endpoint.insecure,
cacert=self.endpoint.cacert,
**self._get_auth_info(password_key="api_key",
project_name_key="project_name"))
kc = self.keystone()
manila_client.client.management_url = kc.service_catalog.url_for(
service_type=self.choose_service_type(service_type),
endpoint_type=self.endpoint.endpoint_type,
region_name=self.endpoint.region_name)
manila_client.client.auth_token = kc.auth_token
return manila_client
@configure("ceilometer", default_version="2", default_service_type="metering",
supported_versions=["1", "2"])
class Ceilometer(OSClient):
def create_client(self, version=None, service_type=None):
"""Return ceilometer client."""
from ceilometerclient import client as ceilometer
kc = self.keystone()
metering_api_url = kc.service_catalog.url_for(
service_type=self.choose_service_type(service_type),
endpoint_type=self.endpoint.endpoint_type,
region_name=self.endpoint.region_name)
auth_token = kc.auth_token
if not hasattr(auth_token, "__call__"):
# python-ceilometerclient requires auth_token to be a callable
auth_token = lambda: kc.auth_token
client = ceilometer.get_client(
self.choose_version(version),
os_endpoint=metering_api_url,
token=auth_token,
timeout=CONF.openstack_client_http_timeout,
insecure=self.endpoint.insecure,
cacert=self.endpoint.cacert,
**self._get_auth_info(project_name_key="tenant_name"))
return client
@configure("ironic", default_version="1", default_service_type="baremetal",
supported_versions=["1"])
class Ironic(OSClient):
def create_client(self, version=None, service_type=None):
"""Return Ironic client."""
from ironicclient import client as ironic
kc = self.keystone()
baremetal_api_url = kc.service_catalog.url_for(
service_type=self.choose_service_type(service_type),
endpoint_type=self.endpoint.endpoint_type,
region_name=self.endpoint.region_name)
client = ironic.get_client(self.choose_version(version),
os_auth_token=kc.auth_token,
ironic_url=baremetal_api_url,
timeout=CONF.openstack_client_http_timeout,
insecure=self.endpoint.insecure,
cacert=self.endpoint.cacert)
return client
@configure("sahara", default_version="1.1", supported_versions=["1.0", "1.1"],
default_service_type="data-processing")
class Sahara(OSClient):
# NOTE(andreykurilin): saharaclient supports "1.0" version and doesn't
# support "1". `choose_version` and `validate_version` methods are written
# as a hack to covert 1 -> 1.0, which can simplify setting saharaclient
# for end-users.
def choose_version(self, version=None):
return float(super(Sahara, self).choose_version(version))
@classmethod
def validate_version(cls, version):
super(Sahara, cls).validate_version(float(version))
def create_client(self, version=None, service_type=None):
"""Return Sahara client."""
from saharaclient import client as sahara
client = sahara.Client(
self.choose_version(version),
service_type=self.choose_service_type(service_type),
**self._get_auth_info(password_key="api_key",
project_name_key="project_name"))
return client
@configure("zaqar", default_version="1.1", default_service_type="messaging",
supported_versions=["1", "1.1"])
class Zaqar(OSClient):
def choose_version(self, version=None):
# zaqarclient accepts only int or float obj as version
return float(super(Zaqar, self).choose_version(version))
def create_client(self, version=None, service_type=None):
"""Return Zaqar client."""
from zaqarclient.queues import client as zaqar
kc = self.keystone()
messaging_api_url = kc.service_catalog.url_for(
service_type=self.choose_service_type(service_type),
endpoint_type=self.endpoint.endpoint_type,
region_name=self.endpoint.region_name)
conf = {"auth_opts": {"backend": "keystone", "options": {
"os_username": self.endpoint.username,
"os_password": self.endpoint.password,
"os_project_name": self.endpoint.tenant_name,
"os_project_id": kc.auth_tenant_id,
"os_auth_url": self.endpoint.auth_url,
"insecure": self.endpoint.insecure,
}}}
client = zaqar.Client(url=messaging_api_url,
version=self.choose_version(version),
conf=conf)
return client
@configure("murano", default_version="1",
default_service_type="application_catalog",
supported_versions=["1"])
class Murano(OSClient):
def create_client(self, version=None, service_type=None):
"""Return Murano client."""
from muranoclient import client as murano
kc = self.keystone()
murano_url = kc.service_catalog.url_for(
service_type=self.choose_service_type(service_type),
endpoint_type=self.endpoint.endpoint_type,
region_name=self.endpoint.region_name
)
client = murano.Client(self.choose_version(version),
endpoint=murano_url,
token=kc.auth_token)
return client
@configure("designate", default_version="1", default_service_type="dns",
supported_versions=["1", "2"])
class Designate(OSClient):
def create_client(self, version=None, service_type=None):
"""Return designate client."""
from designateclient import client
version = self.choose_version(version)
api_url = self._get_endpoint(service_type)
api_url += "/v%s" % version
session = self._get_session(endpoint=api_url)
return client.Client(version, session=session)
@configure("trove", default_version="1.0", supported_versions=["1.0"])
class Trove(OSClient):
def create_client(self, version=None):
"""Returns trove client."""
from troveclient import client as trove
client = trove.Client(self.choose_version(version),
region_name=self.endpoint.region_name,
timeout=CONF.openstack_client_http_timeout,
insecure=self.endpoint.insecure,
cacert=self.endpoint.cacert,
**self._get_auth_info(password_key="api_key")
)
return client
@configure("mistral", default_service_type="workflowv2")
class Mistral(OSClient):
def create_client(self, service_type=None):
"""Return Mistral client."""
from mistralclient.api import client
kc = self.keystone()
mistral_url = kc.service_catalog.url_for(
service_type=self.choose_service_type(service_type),
endpoint_type=self.endpoint.endpoint_type,
region_name=self.endpoint.region_name)
client = client.client(
mistral_url=mistral_url,
service_type=self.choose_service_type(service_type),
auth_token=kc.auth_token)
return client
@configure("swift", default_service_type="object-store")
class Swift(OSClient):
def create_client(self, service_type=None):
"""Return swift client."""
from swiftclient import client as swift
kc = self.keystone()
object_api_url = kc.service_catalog.url_for(
service_type=self.choose_service_type(service_type),
endpoint_type=self.endpoint.endpoint_type,
region_name=self.endpoint.region_name)
client = swift.Connection(retries=1,
preauthurl=object_api_url,
preauthtoken=kc.auth_token,
insecure=self.endpoint.insecure,
cacert=self.endpoint.cacert,
**self._get_auth_info(
user_key="user",
password_key="key",
auth_url_key="authurl",
project_name_key="tenant_name")
)
return client
@configure("ec2")
class EC2(OSClient):
def create_client(self):
"""Return ec2 client."""
import boto
kc = self.keystone()
if kc.version != "v2.0":
raise exceptions.RallyException(
_("Rally EC2 benchmark currently supports only"
"Keystone version 2"))
ec2_credential = kc.ec2.create(user_id=kc.auth_user_id,
tenant_id=kc.auth_tenant_id)
ec2_api_url = kc.service_catalog.url_for(
service_type=consts.ServiceType.EC2,
endpoint_type=self.endpoint.endpoint_type,
region_name=self.endpoint.region_name)
client = boto.connect_ec2_endpoint(
url=ec2_api_url,
aws_access_key_id=ec2_credential.access,
aws_secret_access_key=ec2_credential.secret,
is_secure=self.endpoint.insecure)
return client
@configure("monasca", default_version="2_0",
default_service_type="monitoring", supported_versions=["2_0"])
class Monasca(OSClient):
def create_client(self, version=None, service_type=None):
"""Return monasca client."""
from monascaclient import client as monasca
kc = self.keystone()
monitoring_api_url = kc.service_catalog.url_for(
service_type=self.choose_service_type(service_type),
endpoint_type=self.endpoint.endpoint_type,
region_name=self.endpoint.region_name)
auth_token = kc.auth_token
client = monasca.Client(
self.choose_version(version),
monitoring_api_url,
token=auth_token,
timeout=CONF.openstack_client_http_timeout,
insecure=self.endpoint.insecure,
cacert=self.endpoint.cacert,
**self._get_auth_info(project_name_key="tenant_name"))
return client
class Clients(object):
"""This class simplify and unify work with OpenStack python clients."""
def __init__(self, endpoint, api_info=None):
self.endpoint = endpoint
self.api_info = api_info or {}
# NOTE(kun) Apply insecure/cacert settings from rally.conf if those are
# not set in deployment config. Remove it when invalid.
if self.endpoint.insecure is None:
self.endpoint.insecure = CONF.https_insecure
if self.endpoint.cacert is None:
self.endpoint.cacert = CONF.https_cacert
self.cache = {}
def __getattr__(self, client_name):
"""Lazy load of clients."""
return OSClient.get(client_name)(self.endpoint, self.api_info,
self.cache)
@classmethod
def create_from_env(cls):
return cls(
objects.Endpoint(
os.environ["OS_AUTH_URL"],
os.environ["OS_USERNAME"],
os.environ["OS_PASSWORD"],
os.environ.get("OS_TENANT_NAME"),
region_name=os.environ.get("OS_REGION_NAME")
))
def clear(self):
"""Remove all cached client handles."""
self.cache = {}
def verified_keystone(self):
"""Ensure keystone endpoints are valid and then authenticate
:returns: Keystone Client
"""
from keystoneclient import exceptions as keystone_exceptions
try:
# Ensure that user is admin
client = self.keystone()
if "admin" not in [role.lower() for role in
client.auth_ref.role_names]:
raise exceptions.InvalidAdminException(
username=self.endpoint.username)
except keystone_exceptions.Unauthorized:
raise exceptions.InvalidEndpointsException()
except keystone_exceptions.AuthorizationFailure:
raise exceptions.HostUnreachableException(
url=self.endpoint.auth_url)
return client
def services(self):
"""Return available services names and types.
:returns: dict, {"service_type": "service_name", ...}
"""
if "services_data" not in self.cache:
services_data = {}
ks = self.keystone()
available_services = ks.service_catalog.get_endpoints()
for stype in available_services.keys():
if stype in consts.ServiceType:
services_data[stype] = consts.ServiceType[stype]
self.cache["services_data"] = services_data
return self.cache["services_data"]
@classmethod
@logging.log_deprecated("Use rally.osclients.configure decorator instead.",
"0.1.2")
def register(cls, client_name):
"""DEPRECATED!Decorator that adds new OpenStack client dynamically.
Use rally.osclients.configure decorator instead!
:param client_name: str name how client will be named in Rally clients
Decorated class will be added to Clients in runtime, so its sole
argument is a Clients instance.
Decorated function will be added to Clients in runtime, so its sole
argument is a Clients instance.
Example:
>>> from rally import osclients
>>> @osclients.Clients.register("supernova")
... def another_nova_client(self):
... from novaclient import client as nova
... return nova.Client("2", auth_token=self.keystone().auth_token,
... **self._get_auth_info(password_key="key"))
...
>>> clients = osclients.Clients.create_from_env()
>>> clients.supernova().services.list()[:2]
[<Service: nova-conductor>, <Service: nova-cert>]
"""
def wrap(client_func):
try:
OSClient.get(client_name)
except exceptions.PluginNotFound:
# everything is ok
pass
else:
raise ValueError(
_("Can not register client: name already exists: %s")
% client_name)
@configure(client_name)
class NewClient(OSClient):
create_client = client_func
return NewClient
return wrap