Implement fetching and accessor methods

The ServiceTypes class is the main entry point for python programmers.
It should allow them to answer the questions they have about the data
without necessarily walking the structure. It can also return the raw
structure if people want to get at it.

We need to know the project codename for services for doing API doc
publication validation and also for being able to send legacy
microversion headers. The information is in the data, use it.

In order to construct and send microversion headers for projects with
aliases, we need to know ALL of the aliases plus the official name,
because we don't know which one of the values will be the correct name
the service is looking for for a given version. The microversion spec,
however, requires that consumers be able to handle a header that
contains a list of services and to ignore the ones it isn't looking for.

By providing a a list of all the possible values we make it easy to
construct a header for a service given a service_type that should work
for all known identifiers of the service and that is future compatible
with the service-type and microversion specifyier aligning.

Co-Authored-By: Doug Hellmann <doug@doughellmann.com>
Change-Id: I57641c9e3c27688b6d7709b1bb07292740d26659
This commit is contained in:
Monty Taylor 2017-07-14 14:02:08 -05:00
parent 1865fa56f8
commit 48a94e00a7
No known key found for this signature in database
GPG Key ID: 7BAE94BC7141A594
13 changed files with 912 additions and 14 deletions

View File

@ -15,11 +15,10 @@ Contents:
install/index
library/index
contributor/index
reference/index
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

View File

@ -1,7 +1,26 @@
========
=====
Usage
========
=====
To use os-service-types in a project::
The most basic use of `os-service-types` in a project:
.. code-block:: python
import os_service_types
service_types = os_service_types.ServiceTypes()
However, `os-service-types` expects to be able to fetch remote data, so it's
better to pass in a ``Session`` object. Both
:class:`requests.sessions.Session` and
:class:`keystoneauth1.session.Session` objects are supported. A
:class:`keystoneauth1.session.Session` object does not need auth information
attached, although it will not break anything if it does.
.. code-block:: python
import keystoneauth1.session
import os_service_types
session = keystoneauth1.session.Session()
service_types = os_service_types.ServiceTypes(session=session)

View File

@ -0,0 +1,10 @@
=============
API Reference
=============
.. module:: os_service_types
:synopsis: OpenStack Service Types Data
.. autoclass:: os_service_types.ServiceTypes
:members:
:inherited-members:

View File

@ -11,8 +11,10 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
__all__ = ['__version__', 'ServiceTypes']
import pbr.version
from os_service_types.service_types import ServiceTypes # flake8: noqa
__version__ = pbr.version.VersionInfo('os-service-types').version_string()

View File

@ -0,0 +1,25 @@
# Copyright 2017 Red Hat, Inc
#
# 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.
__all__ = ['read_data']
import json
import os
DATA_DIR = os.path.dirname(__file__)
def read_data(filename):
"""Return data that is shipped inside the python package."""
return json.load(open(os.path.join(DATA_DIR, filename), 'r'))

View File

@ -0,0 +1,305 @@
{
"services": [
{
"service_type": "identity",
"project": "openstack/keystone",
"api_reference": "http://developer.openstack.org/api-ref/identity/"
},
{
"service_type": "compute",
"project": "openstack/nova",
"api_reference": "http://developer.openstack.org/api-ref/compute/"
},
{
"service_type": "image",
"project": "openstack/glance",
"api_reference": "http://developer.openstack.org/api-ref/image/"
},
{
"service_type": "load-balancer",
"project": "openstack/octavia",
"api_reference": "http://developer.openstack.org/api-ref/load-balancer/"
},
{
"service_type": "object-store",
"project": "openstack/swift",
"api_reference": "http://developer.openstack.org/api-ref/object-storage/"
},
{
"aliases": [
"clustering"
],
"service_type": "resource-cluster",
"project": "openstack/senlin",
"api_reference": "http://developer.openstack.org/api-ref/clustering/"
},
{
"service_type": "data-processing",
"project": "openstack/sahara",
"api_reference": "http://developer.openstack.org/api-ref/data-processing/"
},
{
"service_type": "baremetal",
"project": "openstack/ironic",
"api_reference": "http://developer.openstack.org/api-ref/baremetal/"
},
{
"service_type": "baremetal-introspection",
"project": "openstack/ironic-inspector",
"api_reference": "http://docs.openstack.org/developer/ironic-inspector/http-api.html"
},
{
"service_type": "key-manager",
"project": "openstack/barbican",
"api_reference": "http://developer.openstack.org/api-ref/key-manager/"
},
{
"service_type": "ec2-api",
"project": "openstack/ec2-api",
"api_reference": "http://developer.openstack.org/api-ref/ec2-api/"
},
{
"aliases": [
"infra-optim"
],
"service_type": "resource-optimization",
"project": "openstack/watcher",
"api_reference": "https://docs.openstack.org/developer/watcher/webapi/v1.html"
},
{
"aliases": [
"messaging"
],
"service_type": "message",
"project": "openstack/zaqar",
"api_reference": "http://developer.openstack.org/api-ref/messaging/"
},
{
"service_type": "application-catalog",
"project": "openstack/murano",
"api_reference": "http://developer.openstack.org/api-ref/application-catalog/"
},
{
"aliases": [
"container-infrastructure"
],
"service_type": "container-infrastructure-management",
"project": "openstack/magnum",
"api_reference": "http://developer.openstack.org/api-ref/container-infrastructure-management/"
},
{
"service_type": "search",
"project": "openstack/searchlight",
"api_reference": "http://developer.openstack.org/api-ref/search/"
},
{
"service_type": "dns",
"project": "openstack/designate",
"api_reference": "http://developer.openstack.org/api-ref/dns/"
},
{
"aliases": [
"workflowv2"
],
"service_type": "workflow",
"project": "openstack/mistral",
"api_reference": "http://docs.openstack.org/developer/mistral/developer/webapi/index.html"
},
{
"service_type": "rating",
"project": "openstack/cloudkitty",
"api_reference": "http://docs.openstack.org/developer/cloudkitty/webapi/root.html"
},
{
"aliases": [
"policy"
],
"service_type": "operator-policy",
"project": "openstack/congress",
"api_reference": "http://docs.openstack.org/developer/congress/api.html"
},
{
"aliases": [
"sharev2",
"share"
],
"service_type": "shared-file-system",
"project": "openstack/manila",
"api_reference": "http://developer.openstack.org/api-ref/shared-file-systems/"
},
{
"service_type": "data-protection-orchestration",
"project": "openstack/karbor",
"api_reference": "https://developer.openstack.org/api-ref/data-protection-orchestration/"
},
{
"service_type": "orchestration",
"project": "openstack/heat",
"api_reference": "http://developer.openstack.org/api-ref/orchestration/"
},
{
"aliases": [
"volume",
"volumev2",
"volumev3"
],
"service_type": "block-storage",
"project": "openstack/cinder",
"api_reference": "http://developer.openstack.org/api-ref/block-storage/"
},
{
"aliases": [
"alarming"
],
"service_type": "alarm",
"project": "openstack/aodh",
"api_reference": "https://docs.openstack.org/developer/aodh/webapi/index.html"
},
{
"aliases": [
"metering"
],
"service_type": "meter",
"project": "openstack/ceilometer",
"api_reference": "https://docs.openstack.org/developer/ceilometer/webapi/index.html"
},
{
"aliases": [
"events"
],
"service_type": "event",
"project": "openstack/panko",
"api_reference": "http://docs.openstack.org/developer/panko/webapi/index.html"
},
{
"aliases": [
"application_deployment"
],
"service_type": "application-deployment",
"project": "openstack/solum",
"api_reference": "http://docs.openstack.org/developer/solum/develop_applications/webapi/index.html"
},
{
"aliases": [
"tricircle"
],
"service_type": "multi-region-network-automation",
"project": "openstack/tricircle",
"api_reference": "http://docs.openstack.org/developer/tricircle/api_v1.html"
},
{
"service_type": "database",
"project": "openstack/trove",
"api_reference": "http://developer.openstack.org/api-ref/database/"
},
{
"aliases": [
"container"
],
"service_type": "application-container",
"project": "openstack/zun",
"api_reference": "https://git.openstack.org/cgit/openstack/zun/tree/api-ref/source"
},
{
"aliases": [
"rca"
],
"service_type": "root-cause-analysis",
"project": "openstack/vitrage",
"api_reference": "http://docs.openstack.org/developer/vitrage/vitrage-api.html"
},
{
"service_type": "nfv-orchestration",
"project": "openstack/tacker",
"api_reference": "http://developer.openstack.org/api-ref/nfv-orchestration/"
},
{
"service_type": "network",
"project": "openstack/neutron",
"api_reference": "http://developer.openstack.org/api-ref/networking/",
"api_reference_project": "openstack/neutron-lib"
},
{
"service_type": "backup",
"project": "openstack/freezer-api",
"api_reference": "http://developer.openstack.org/api-ref/backup/"
},
{
"service_type": "monitoring-log-api",
"project": "openstack/monasca-log-api",
"api_reference": "http://developer.openstack.org/api-ref/monitoring-log-api/"
}
],
"forward": {
"block-storage": [
"volume",
"volumev2",
"volumev3"
],
"resource-cluster": [
"clustering"
],
"message": [
"messaging"
],
"root-cause-analysis": [
"rca"
],
"resource-optimization": [
"infra-optim"
],
"multi-region-network-automation": [
"tricircle"
],
"event": [
"events"
],
"operator-policy": [
"policy"
],
"application-container": [
"container"
],
"alarm": [
"alarming"
],
"workflow": [
"workflowv2"
],
"application-deployment": [
"application_deployment"
],
"meter": [
"metering"
],
"shared-file-system": [
"sharev2",
"share"
],
"container-infrastructure-management": [
"container-infrastructure"
]
},
"version": "2017-07-10T13:19:11.690814",
"reverse": {
"events": "event",
"messaging": "message",
"metering": "meter",
"rca": "root-cause-analysis",
"container": "application-container",
"application_deployment": "application-deployment",
"volumev3": "block-storage",
"volume": "block-storage",
"workflowv2": "workflow",
"clustering": "resource-cluster",
"infra-optim": "resource-optimization",
"policy": "operator-policy",
"tricircle": "multi-region-network-automation",
"container-infrastructure": "container-infrastructure-management",
"volumev2": "block-storage",
"alarming": "alarm",
"sharev2": "shared-file-system",
"share": "shared-file-system"
},
"sha": "329e37b2fe03085875a7f65cd31eb9f1d8182176"
}

View File

@ -0,0 +1,228 @@
# Copyright 2017 Red Hat, Inc
#
# 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.
__all__ = ['ServiceTypes']
import copy
import os_service_types.data
BUILTIN_DATA = os_service_types.data.read_data('service-types.json')
SERVICE_TYPES_URL = "https://service-types.openstack.org/service-types.json"
class ServiceTypes(object):
"""Encapsulation of the OpenStack Service Types Authority data.
The Service Types Authority data will be either pulled from its remote
location or from local files as is appropriate.
If the user passes a Session, remote data will be fetched. If the user
does not do that, local builtin data will be used.
:param session: An object that behaves like a `requests.sessions.Session`
or a `keystoneauth1.session.Session` that provides a get method
and returns an object that behaves like a `requests.models.Response`.
Optional. If session is omitted, no remote actions will be performed.
:param bool only_remote: By default if there is a problem fetching data
remotely the builtin data will be returned as a fallback. only_remote
will cause remote failures to raise an error instead of falling back.
Optional, defaults to False.
"""
def __init__(self, session=None, only_remote=False):
self._service_types_data = BUILTIN_DATA
if session:
try:
response = session.get(SERVICE_TYPES_URL)
response.raise_for_status()
self._service_types_data = response.json()
except IOError:
# If we can't fetch, fall backto BUILTIN
if only_remote:
raise
by_project = {}
for s in self._service_types_data['services']:
for key in ['project', 'api_reference_project']:
name = s.get(key)
if name:
by_project[self._canonical_project_name(name)] = s
self._service_types_data['by_project'] = by_project
def _canonical_project_name(self, name):
"Convert repo name to project name."
if name is None:
raise ValueError("Empty project name is not allowed")
if name.startswith('openstack/'):
# Handle openstack/ prefix going away from STA data
return name.rpartition('/')[-1]
return name
@property
def url(self):
"The URL from which the data was retrieved."
return SERVICE_TYPES_URL
@property
def version(self):
"The version of the data."
return self._service_types_data['version']
@property
def forward(self):
"Mapping service-type names to their aliases."
return copy.deepcopy(self._service_types_data['forward'])
@property
def reverse(self):
"Mapping aliases to their service-type names."
return copy.deepcopy(self._service_types_data['reverse'])
@property
def services(self):
"Full service-type data listing."
return copy.deepcopy(self._service_types_data['services'])
def get_official_service_data(self, service_type):
"""Get the service data for an official service_type.
:param str service_type: The official service-type to get data for.
:returns dict: Service data for the service or None if not found.
"""
for service in self._service_types_data['services']:
if service_type == service['service_type']:
return service
return None
def get_service_data(self, service_type):
"""Get the service data for a given service_type.
:param str service_type: The service-type or alias to get data for.
:returns dict: Service data for the service or None if not found.
"""
service_type = self.get_service_type(service_type)
if not service_type:
return None
return self.get_official_service_data(service_type)
def is_official(self, service_type):
"""Is the given service-type an official service-type?
:param str service_type: The service-type to test.
:returns bool: True if it's an official type, False otherwise.
"""
return self.get_official_service_data(service_type) is not None
def is_alias(self, service_type):
"""Is the given service-type an alias?
:param str service_type: The service-type to test.
:returns bool: True if it's an alias type, False otherwise.
"""
return service_type in self._service_types_data['reverse']
def is_known(self, service_type):
"""Is the given service-type an official type or an alias?
:param str service_type: The service-type to test.
:returns bool: True if it's a known type, False otherwise.
"""
return self.is_official(service_type) or self.is_alias(service_type)
def is_match(self, requested, found):
"""Does a requested service-type match one found in the catalog?
A requested service-type matches a service-type in the catalog if
it is either a direct match, if the service-type in the catalog is
an official type and the requested type is one of its aliases, or
if the requested type is an official type and the type in the catalog
is one of its aliases.
A requested alias cannot match a different alias because there are
historical implications related to versioning to some historical
aliases that cannot be safely reasoned about in an automatic fashion.
:param str requested: A service-type that someone is looking for.
:param str found: A service-type found in a catalog
:returns bool: True if the service-type being requested matches the
entry in the catalog. False if it does not.
"""
# Exact match
if requested == found:
return True
# Found is official type, requested is one of its aliases
if requested in self.get_aliases(found):
return True
# Found is an alias, requested is an official type
if requested == self.get_service_type(found):
return True
return False
def get_aliases(self, service_type):
"""Returns the list of aliases for a given official service-type.
:param str service_type: An official service-type.
:returns list: List of aliases, or empty list if there are none.
"""
return self._service_types_data['forward'].get(service_type, [])
def get_service_type(self, service_type):
"""Given a possible service_type, return the official type.
:param str service_type: A potential service-type.
:returns str: The official service-type, or None if there is no match.
"""
if self.is_official(service_type):
return service_type
return self._service_types_data['reverse'].get(service_type)
def get_all_types(self, service_type):
"""Get a list of official type and all known aliases.
:param str service_type: The service-type or alias to get data for.
:returns dict: Service data for the service or None if not found.
"""
if not self.is_known(service_type):
return [service_type]
service_type = self.get_service_type(service_type)
ret = [service_type]
ret.extend(self.get_aliases(service_type))
return ret
def get_project_name(self, service_type):
"""Return the OpenStack project name for a given service_type.
:param str service_type: An official service-type or alias.
:returns str: The OpenStack project name or None if there is no match.
"""
service = self.get_service_data(service_type)
if service:
return self._canonical_project_name(service['project'])
return None
def get_service_data_for_project(self, project_name):
"""Return the service information associated with a project.
:param name: A repository or project name in the form
``'openstack/{project}'`` or just ``'{project}'``.
:type name: str
:returns: dict or None if not found
"""
key = self._canonical_project_name(project_name)
return self._service_types_data['by_project'].get(key)

View File

@ -15,9 +15,196 @@
# License for the specific language governing permissions and limitations
# under the License.
import copy
import datetime
import keystoneauth1.session
from oslotest import base
from requests_mock.contrib import fixture as rm_fixture
import os_service_types.service_types
class TestCase(base.BaseTestCase):
"""Base test case class before singleton protection is added."""
"""Test case base class for all unit tests."""
def setUp(self):
super(TestCase, self).setUp()
# use keystoneauth1 to get a Sessiom with no auth information
self.session = keystoneauth1.session.Session()
self.set_adapter()
self.builtin_content = os_service_types.service_types.BUILTIN_DATA
self.builtin_version = self.builtin_content['version']
# Set up copies of the data so that we can verify that we got the
# copy of it we think we should.
self.remote_version = datetime.datetime.utcnow().isoformat()
self.remote_content = copy.deepcopy(self.builtin_content)
self.remote_content['version'] = self.remote_version
def set_adapter(self):
# Set up a requests_mock fixture for all HTTP traffic
self.adapter = self.useFixture(rm_fixture.Fixture())
class ServiceDataMixin(object):
scenarios = [
('compute', dict(
service_type='compute', official='compute', aliases=[],
all_types=['compute'],
api_reference='compute', api_reference_project=None,
is_known=True, is_alias=False, is_official=True, project='nova')),
('volumev2', dict(
service_type='volumev2', official='block-storage', aliases=[],
all_types=['block-storage', 'volume', 'volumev2', 'volumev3'],
api_reference='block-storage', api_reference_project=None,
is_known=True, is_alias=True, is_official=False,
project='cinder')),
('volumev3', dict(
service_type='volumev3', official='block-storage', aliases=[],
all_types=['block-storage', 'volume', 'volumev2', 'volumev3'],
api_reference='block-storage', api_reference_project=None,
is_known=True, is_alias=True, is_official=False,
project='cinder')),
('block-storage', dict(
service_type='block-storage', official='block-storage',
all_types=['block-storage', 'volume', 'volumev2', 'volumev3'],
api_reference='block-storage', api_reference_project=None,
aliases=['volume', 'volumev2', 'volumev3'],
is_known=True, is_alias=False, is_official=True,
project='cinder')),
('network', dict(
service_type='network', official='network', aliases=[],
all_types=['network'],
api_reference='networking', api_reference_project='neutron-lib',
is_known=True, is_alias=False, is_official=True,
project='neutron')),
('missing', dict(
service_type='missing', official=None,
aliases=[],
all_types=['missing'],
api_reference=None, api_reference_project=None,
is_known=False, is_alias=False, is_official=False,
project=None)),
]
def test_get_service_type(self):
if self.official:
self.assertEqual(
self.official,
self.service_types.get_service_type(self.service_type))
else:
self.assertIsNone(
self.service_types.get_service_type(self.service_type))
def test_get_aliases(self):
self.assertEqual(
self.aliases,
self.service_types.get_aliases(self.service_type))
def test_is_known(self):
self.assertEqual(
self.is_known,
self.service_types.is_known(self.service_type))
def test_is_alias(self):
self.assertEqual(
self.is_alias,
self.service_types.is_alias(self.service_type))
def test_is_official(self):
self.assertEqual(
self.is_official,
self.service_types.is_official(self.service_type))
def test_get_project_name(self):
if self.project:
self.assertEqual(
self.project,
self.service_types.get_project_name(self.service_type))
else:
self.assertIsNone(
self.service_types.get_project_name(self.service_type))
def test_get_service_data(self):
service_data = self.service_types.get_service_data(self.service_type)
# TODO(mordred) Once all the docs have been aligned, remove
# self.api_reference and replace with self.service_type
api_url = 'http://developer.openstack.org/api-ref/{api_reference}/'
# Tests self.official here, since we expect to get data back for all
# official projects, regardless of service_type being an alias or not
if not self.official:
self.assertIsNone(service_data)
else:
self.assertIsNotNone(service_data)
self.assertEqual(
'openstack/{project}'.format(project=self.project),
service_data['project'])
self.assertEqual(self.official, service_data['service_type'])
self.assertEqual(
api_url.format(api_reference=self.api_reference),
service_data['api_reference'])
def test_get_official_service_data(self):
service_data = self.service_types.get_official_service_data(
self.service_type)
# TODO(mordred) Once all the docs have been aligned, remove
# self.api_reference and replace with self.service_type
api_url = 'http://developer.openstack.org/api-ref/{api_reference}/'
# Tests self.is_official here, since we expect only get data back for
# official projects.
if not self.is_official:
self.assertIsNone(service_data)
else:
self.assertIsNotNone(service_data)
self.assertEqual(
'openstack/{project}'.format(project=self.project),
service_data['project'])
self.assertEqual(self.official, service_data['service_type'])
self.assertEqual(
api_url.format(api_reference=self.api_reference),
service_data['api_reference'])
def test_empty_project_error(self):
if not self.project:
self.assertRaises(
ValueError,
self.service_types.get_service_data_for_project,
self.project)
def test_get_service_data_for_project(self):
if not self.project:
self.skipTest("Empty project is invalid but tested elsewhere.")
return
service_data = self.service_types.get_service_data_for_project(
self.project)
# TODO(mordred) Once all the docs have been aligned, remove
# self.api_reference and replace with self.service_type
api_url = 'http://developer.openstack.org/api-ref/{api_reference}/'
self.assertIsNotNone(service_data)
if self.api_reference_project:
self.assertEqual(
'openstack/{api_reference_project}'.format(
api_reference_project=self.api_reference_project),
service_data['api_reference_project'])
else:
self.assertEqual(
'openstack/{project}'.format(project=self.project),
service_data['project'])
self.assertEqual(self.official, service_data['service_type'])
self.assertEqual(
api_url.format(api_reference=self.api_reference),
service_data['api_reference'])
def test_get_all_types(self):
self.assertEqual(
self.all_types,
self.service_types.get_all_types(self.service_type))

View File

@ -13,16 +13,26 @@
# under the License.
"""
test_os_service_types
----------------------------------
test_builtin
------------
Tests for `os_service_types` module.
Tests for `ServiceTypes` class builtin data.
oslotest sets up a TempHomeDir for us, so there should be no ~/.cache files
available in these tests.
"""
from testscenarios import load_tests_apply_scenarios as load_tests # noqa
import os_service_types
from os_service_types.tests import base
class TestOs_service_types(base.TestCase):
class TestBuiltin(base.TestCase, base.ServiceDataMixin):
def test_something(self):
pass
def setUp(self):
super(TestBuiltin, self).setUp()
# Make an object with no network access
self.service_types = os_service_types.ServiceTypes()
def test_builtin_version(self):
self.assertEqual(self.builtin_version, self.service_types.version)

View File

@ -0,0 +1,62 @@
# -*- coding: utf-8 -*-
# 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.
"""
test_match
----------
Tests for is_match logic
oslotest sets up a TempHomeDir for us, so there should be no ~/.cache files
available in these tests.
"""
from testscenarios import load_tests_apply_scenarios as load_tests # noqa
import os_service_types
from os_service_types.tests import base
class TestMatch(base.TestCase):
scenarios = [
('match-official', dict(
requested='compute', found='compute', is_match=True)),
('direct-match-unknown', dict(
requested='unknown', found='unknown', is_match=True)),
('volumev2-finds-block', dict(
requested='volumev2', found='block-storage', is_match=True)),
('volumev3-finds-block', dict(
requested='volumev3', found='block-storage', is_match=True)),
('block-finds-volumev2', dict(
requested='block-storage', found='volumev2', is_match=True)),
('block-finds-volumev3', dict(
requested='block-storage', found='volumev3', is_match=True)),
('volumev2-not-volumev3', dict(
requested='volumev2', found='volumev3', is_match=False)),
('non-match', dict(
requested='unknown', found='compute', is_match=False)),
]
def setUp(self):
super(TestMatch, self).setUp()
# Make an object with no network access
self.service_types = os_service_types.ServiceTypes()
def test_is_match(self):
if self.is_match:
self.assertTrue(
self.service_types.is_match(self.requested, self.found))
else:
self.assertFalse(
self.service_types.is_match(self.requested, self.found))

View File

@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
# 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.
"""
test_remote
-----------
Tests for `ServiceTypes` class remote data.
oslotest sets up a TempHomeDir for us, so there should be no ~/.cache files
available in these tests.
"""
from testscenarios import load_tests_apply_scenarios as load_tests # noqa
import os_service_types
import os_service_types.service_types
from os_service_types.tests import base
class TestRemote(base.TestCase, base.ServiceDataMixin):
def setUp(self):
super(TestRemote, self).setUp()
self.adapter.register_uri(
'GET', os_service_types.service_types.SERVICE_TYPES_URL,
json=self.remote_content,
headers={'etag': self.getUniqueString('etag')})
# Make an object that fetches from the network
self.service_types = os_service_types.ServiceTypes(
session=self.session)
self.assertEqual(1, len(self.adapter.request_history))
def test_remote_version(self):
self.assertEqual(self.remote_version, self.service_types.version)

View File

@ -0,0 +1,5 @@
---
features:
- Added ServiceTypes class, which is the primary entry
point for Python developers who need access to the
OpenStack Service Types Authority data.

View File

@ -8,8 +8,9 @@ coverage!=4.4,>=4.0 # Apache-2.0
python-subunit>=0.0.18 # Apache-2.0/BSD
sphinx>=1.6.2 # BSD
oslotest>=1.10.0 # Apache-2.0
testrepository>=0.0.18 # Apache-2.0/BSD
testtools>=1.4.0 # MIT
testscenarios>=0.4 # Apache-2.0/BSD
requests-mock>=1.1 # Apache-2.0
openstackdocstheme>=1.11.0 # Apache-2.0
keystoneauth1>=2.21.0 # Apache-2.0
# releasenotes
reno!=2.3.1,>=1.8.0 # Apache-2.0