Rally provides a framework for performance analysis and benchmarking of individual OpenStack components as well as full production OpenStack cloud deployments
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

387 lines
15 KiB

  1. #
  2. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  3. # not use this file except in compliance with the License. You may obtain
  4. # a copy of the License at
  5. #
  6. # http://www.apache.org/licenses/LICENSE-2.0
  7. #
  8. # Unless required by applicable law or agreed to in writing, software
  9. # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  10. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  11. # License for the specific language governing permissions and limitations
  12. # under the License.
  13. import functools
  14. import inspect
  15. import six
  16. from rally.common.plugin import discover
  17. from rally.common.plugin import meta
  18. from rally import exceptions
  19. from rally.task import atomic
  20. def service(service_name, service_type, version, client_name=None):
  21. """Mark class as an implementation of partial service APIs.
  22. :param service_name: name of the service (e.g. Nova)
  23. :type service_name: str
  24. :param service_type: type of the service (e.g. Compute)
  25. :type service_type: str
  26. :param version: version of service (e.g. 2.1)
  27. :type version: str
  28. :param client_name: name of client for service. If None, service_name will
  29. be used instead.
  30. :type client_name: str
  31. """
  32. def wrapper(cls):
  33. cls._meta_init()
  34. cls._meta_set("name", service_name.lower())
  35. cls._meta_set("type", service_type.lower())
  36. cls._meta_set("version", str(version))
  37. cls._meta_set("client_name", client_name or service_name)
  38. return cls
  39. return wrapper
  40. def compat_layer(original_impl):
  41. """Set class which should be unified to common interface
  42. :param original_impl: implementation of specific service API
  43. :type original_impl: cls
  44. """
  45. def wrapper(cls):
  46. cls._meta_init()
  47. cls._meta_set("impl", original_impl)
  48. return cls
  49. return wrapper
  50. def should_be_overridden(func):
  51. """Mark method which should be overridden by subclasses."""
  52. func.require_impl = True
  53. return func
  54. def method_wrapper(func):
  55. """Wraps service's methods with some magic
  56. 1) Each service method should not be called with positional arguments,
  57. since it can lead mistakes in wrong order while writing version
  58. compatible code. We had such situation in KeystoneWrapper
  59. (see https://review.opendev.org/#/c/309470/ ):
  60. .. code-block:: python
  61. class IdentityService(Service):
  62. def add_role(self, role_id, user_id, project_id):
  63. self._impl(role_id, user_id, project_id)
  64. class KeystoneServiceV2(Service):
  65. def add_role(self, user_id, role_id, project_id):
  66. pass
  67. class KeystoneServiceV3(Service):
  68. def add_role(self, role_id, user_id, project_id):
  69. pass
  70. Explanation of example: The signature of add_role method is
  71. different in KeystoneServiceV2 and KeystoneServiceV3. Since
  72. IdentityService uses positional arguments to make call to
  73. self._impl.add_role, we have swapped values of role_id and user_id in
  74. case of KeystoneServiceV2.
  75. Original code and idea are taken from `positional` library.
  76. 2) We do not need keep atomics for some actions, for example for inner
  77. actions (until we start to support them). Previously, we used
  78. "atomic_action" argument with `if atomic_action` checks inside each
  79. method. To reduce number of similar if blocks, let's write them in one
  80. place, make the code cleaner and support such feature for all service
  81. methods.
  82. """
  83. @functools.wraps(func)
  84. def wrapper(instance, *args, **kwargs):
  85. args_len = len(args)
  86. if args_len > 1:
  87. message = ("%(name)s takes at most 1 positional argument "
  88. "(%(given)d given)" % {"name": func.__name__,
  89. "given": args_len})
  90. raise TypeError(message)
  91. return func(instance, *args, **kwargs)
  92. return wrapper
  93. class ServiceMeta(type):
  94. """Alternative implementation of abstract classes for Services.
  95. Common class of specific Service should not be hardcoded for any version of
  96. API. We expect that all public methods of specific common class are
  97. overridden in all versioned implementation.
  98. """
  99. def __new__(mcs, name, parents, dct):
  100. for field in dct:
  101. if not field.startswith("_") and callable(dct[field]):
  102. dct[field] = method_wrapper(dct[field])
  103. return super(ServiceMeta, mcs).__new__(mcs, name, parents, dct)
  104. def __init__(cls, name, bases, namespaces):
  105. super(ServiceMeta, cls).__init__(name, bases, namespaces)
  106. bases = [c for c in cls.__bases__ if type(c) == ServiceMeta]
  107. if not bases:
  108. # nothing to check
  109. return
  110. # obtain all properties of cls, since namespace doesn't include
  111. # properties of parents
  112. not_implemented_apis = set()
  113. for name, obj in inspect.getmembers(cls):
  114. if (getattr(obj, "require_impl", False)
  115. # name in namespace means that object was introduced in cls
  116. and name not in namespaces):
  117. # it is not overridden...
  118. not_implemented_apis.add(name)
  119. if not_implemented_apis:
  120. raise exceptions.RallyException(
  121. "%s has wrong implementation. Implementation of specific "
  122. "version of API should override all required methods of "
  123. "base service class. Missed method(s): %s." %
  124. (cls.__name__, ", ".join(not_implemented_apis)))
  125. @six.add_metaclass(ServiceMeta)
  126. class Service(meta.MetaMixin):
  127. """Base help class for Cloud Services(for example OpenStack services).
  128. A simple example of implementation:
  129. .. code-block::
  130. # Implementation of Keystone V2 service
  131. @service("keystone", service_type="identity", version="2")
  132. class KeystoneV2Service(Service):
  133. @atomic.action_timer("keystone_v2.create_tenant")
  134. def create_tenant(self, tenant_name):
  135. return self.client.tenants.create(project_name)
  136. # Implementation of Keystone V3 service
  137. @service("keystone", service_type="identity", version="3")
  138. class KeystoneV3Service(Service):
  139. @atomic.action_timer("keystone_v3.create_project")
  140. def create_project(self, project_name):
  141. return self.client.project.create(project_name)
  142. """
  143. def __init__(self, clients, name_generator=None, atomic_inst=None):
  144. """Initialize service class
  145. :param clients: instance of rally.plugins.openstack.osclients.Clients
  146. :param name_generator: a method for generating random names. Usually
  147. it is generate_random_name method of RandomNameGeneratorMixin
  148. instance.
  149. :param atomic_inst: an object to store atomic actions. Usually, it is
  150. `_atomic_actions` property of ActionTimerMixin instance
  151. """
  152. self._clients = clients
  153. self._name_generator = name_generator
  154. if atomic_inst is None:
  155. self._atomic_actions = atomic.ActionTimerMixin().atomic_actions()
  156. else:
  157. self._atomic_actions = atomic_inst
  158. self.version = None
  159. if self._meta_is_inited(raise_exc=False):
  160. self.version = self._meta_get("version")
  161. def generate_random_name(self):
  162. if not self._name_generator:
  163. raise exceptions.RallyException(
  164. "You cannot use `generate_random_name` method, until you "
  165. "initialize class with `name_generator` argument.")
  166. return self._name_generator()
  167. class UnifiedService(Service):
  168. """Base help class for unified layer for Cloud Services
  169. A simple example of Identity service implementation:
  170. .. code-block::
  171. import collections
  172. Project = collections.namedtuple("Project", ["id", "name"])
  173. # Unified entry-point for Identity OpenStack service
  174. class Identity(UnifiedService):
  175. # this method is equal in UnifiedKeystoneV2 and UnifiedKeystoneV3.
  176. # Since there is no other implementation except Keystone, there
  177. # are no needs to copy-paste it.
  178. @classmethod
  179. def _is_applicable(cls, clients):
  180. cloud_version = clients.keystone().version.split(".")[0][1:]
  181. return cloud_version == impl._meta_get("version")
  182. def create_project(self, project_name, domain_name="Default"):
  183. return self._impl.create_project(project_name,
  184. domain_name=domain_name)
  185. # Class which unifies raw keystone v2 data to common form
  186. @compat_layer(KeystoneV2Service)
  187. class UnifiedKeystoneV2(Identity):
  188. def create_project(self, project_name, domain_name="Default"):
  189. if domain_name.lower() != "default":
  190. raise NotImplementedError(
  191. "Domain functionality not implemented in Keystone v2")
  192. tenant = self._impl.create_tenant(project_name)
  193. return Project(id=tenant.id, name=tenant.name)
  194. # Class which unifies raw keystone v3 data to common form
  195. @compat_layer(KeystoneV3Service)
  196. class UnifiedKeystoneV3(Identity):
  197. def create_project(self, project_name, domain_name="Default"):
  198. project = self._impl.create_project(project_name,
  199. domain_name=domain_name)
  200. return Project(id=project.id, name=project.name)
  201. """
  202. def __init__(self, clients, name_generator=None, atomic_inst=None):
  203. """Initialize service class
  204. :param clients: instance of rally.plugins.openstack.osclients.Clients
  205. :param name_generator: a method for generating random names. Usually
  206. it is generate_random_name method of RandomNameGeneratorMixin
  207. instance.
  208. :param atomic_inst: an object to store atomic actions. Usually, it is
  209. `_atomic_actions` property of ActionTimerMixin instance
  210. """
  211. super(UnifiedService, self).__init__(clients, name_generator,
  212. atomic_inst)
  213. if self._meta_is_inited(raise_exc=False):
  214. # it is an instance of compatibility layer for specific Service
  215. impl_cls = self._meta_get("impl")
  216. self._impl = impl_cls(self._clients, self._name_generator,
  217. self._atomic_actions)
  218. self.version = impl_cls._meta_get("version")
  219. else:
  220. # it is a base class of service
  221. impl_cls, _all_impls = self.discover_impl()
  222. if not impl_cls:
  223. raise exceptions.RallyException(
  224. "There is no proper implementation for %s."
  225. % self.__class__.__name__)
  226. self._impl = impl_cls(self._clients, self._name_generator,
  227. self._atomic_actions)
  228. self.version = self._impl.version
  229. def discover_impl(self):
  230. """Discover implementation for service
  231. One Service can have different implementations(not only in terms of
  232. versioning, for example Network service of OpenStack has Nova-network
  233. and Neutron implementation. they are quite different). Each of such
  234. implementations can support several versions. This method is designed
  235. to choose the proper helper class based on available services in the
  236. cloud and based on expected version.
  237. Returns a tuple with implementation class as first element, a set of
  238. all implementations as a second element
  239. """
  240. # find all classes with unified implementation
  241. impls = {cls: cls._meta_get("impl")
  242. for cls in discover.itersubclasses(self.__class__)
  243. if (cls._meta_is_inited(raise_exc=False)
  244. and cls._meta_get("impl"))}
  245. service_names = {o._meta_get("name") for o in impls.values()}
  246. enabled_services = None
  247. # let's make additional calls to cloud only when we need to make a
  248. # decision based on available services
  249. if len(service_names) > 1:
  250. enabled_services = list(self._clients.services().values())
  251. for cls, impl in impls.items():
  252. if (enabled_services is not None
  253. and impl._meta_get("name") not in enabled_services):
  254. continue
  255. if cls.is_applicable(self._clients):
  256. return cls, impls
  257. return None, impls
  258. @classmethod
  259. def is_applicable(cls, clients):
  260. """Check that implementation can be used in cloud."""
  261. if cls._meta_is_inited(raise_exc=False):
  262. impl = cls._meta_get("impl", cls)
  263. client = getattr(clients, impl._meta_get("client_name"))
  264. return client.choose_version() == impl._meta_get("version")
  265. return False
  266. class _Resource(object):
  267. __slots__ = []
  268. _id_property = None
  269. def __init__(self, **kwargs):
  270. for k, v in kwargs.items():
  271. setattr(self, k, v)
  272. def __getitem__(self, item, default=None):
  273. return getattr(self, item, default)
  274. def __repr__(self):
  275. return "<%s id=%s>" % (self.__class__.__name__,
  276. getattr(self, self._id_property, "n/a"))
  277. def __eq__(self, other):
  278. self_id = getattr(self, self._id_property)
  279. return (isinstance(other, self.__class__)
  280. and self_id == getattr(other, self._id_property))
  281. def _as_dict(self):
  282. return dict((k, self[k]) for k in self.__slots__)
  283. def make_resource_cls(name, properties, id_property="id"):
  284. """Construct a resource class with limited number of properties.
  285. Unlike collections.namedtuple, a created class has user-friendly getitem
  286. method for obtaining properties.
  287. :param name: The name of resource (i.e image, container..)
  288. :param properties: The list of allowed properties
  289. :param id_property: The name of property which should be used as id of
  290. resource. By defaults, it is "id" field if such property presents in
  291. "properties" or first element of "properties" in other cases.
  292. """
  293. id_property = id_property if id_property in properties else properties[0]
  294. # NOTE(andreykurilin): call a `type` method instead of returning just raw
  295. # class (create Resource class inside the method make_resource_cls and
  296. # return it) allows to setup a custom name of a new class, which will be
  297. # used in case of errors and etc
  298. return type(name.title(), (_Resource,), {"__slots__": properties,
  299. "_id_property": id_property})