API Microversioning

This patch set adds API microversion support along
with the first API microversion: start/end time for
CONTINUOUS audits.

APIImpact

Implements: blueprint api-microversioning
Depends-On: I6bb838d777b2c7aa799a70485980e5dc87838456
Change-Id: I17309d80b637f02bc5e6d33294472e02add88f86
This commit is contained in:
Alexander Chadin 2018-10-04 12:33:28 +03:00
parent c2550e534e
commit c4a30153f1
29 changed files with 535 additions and 18 deletions

3
.gitignore vendored
View File

@ -75,3 +75,6 @@ releasenotes/build
# Autogenerated sample config file
etc/watcher/watcher.conf.sample
# Atom
.remote-sync.json

View File

@ -191,6 +191,13 @@ audit_autotrigger:
in: body
required: false
type: boolean
audit_endtime:
description: |
The time after which audit can't be executed.
in: body
required: false
type: string
min_version: 1.1
audit_goal:
description: |
The UUID or name of the Goal.
@ -229,6 +236,13 @@ audit_parameters:
in: body
required: false
type: JSON
audit_starttime:
description: |
The time after which audit can be executed in accordance with interval.
in: body
required: false
type: string
min_version: 1.1
audit_state:
description: |
State of this audit. To get more information about states and

View File

@ -48,5 +48,7 @@
"strategy_name": "workload_stabilization",
"next_run_time": "2018-04-06T11:56:00",
"updated_at": "2018-04-06T11:54:01.266447+00:00",
"hostname": "controller"
}
"hostname": "controller",
"start_time": null,
"end_time": null
}

View File

@ -8,5 +8,7 @@
]
},
"audit_type": "CONTINUOUS",
"interval": "*/2 * * * *"
}
"interval": "*/2 * * * *",
"start_time":"2018-04-02 20:30:00",
"end_time": "2018-04-04 20:30:00"
}

View File

@ -48,5 +48,7 @@
"strategy_name": "workload_stabilization",
"next_run_time": null,
"updated_at": null,
"hostname": null
}
"hostname": null,
"start_time": null,
"end_time": null
}

View File

@ -50,7 +50,9 @@
"strategy_name": "workload_stabilization",
"next_run_time": "2018-04-06T09:46:00",
"updated_at": "2018-04-06T09:44:01.604146+00:00",
"hostname": "controller"
"hostname": "controller",
"start_time": null,
"end_time": null
}
]
}
}

View File

@ -48,5 +48,7 @@
"strategy_name": "workload_stabilization",
"next_run_time": "2018-04-06T11:56:00",
"updated_at": "2018-04-06T11:54:01.266447+00:00",
"hostname": "controller"
}
"hostname": "controller",
"start_time": null,
"end_time": null
}

View File

@ -48,5 +48,7 @@
"strategy_name": "workload_stabilization",
"next_run_time": "2018-04-06T11:56:00",
"updated_at": "2018-04-06T11:54:01.266447+00:00",
"hostname": "controller"
}
"hostname": "controller",
"start_time": null,
"end_time": null
}

View File

@ -46,6 +46,8 @@ Request
- interval: audit_interval
- scope: audittemplate_scope
- auto_trigger: audit_autotrigger
- start_time: audit_starttime
- end_time: audit_endtime
**Example ONESHOT Audit creation request:**
@ -80,6 +82,8 @@ version 1:
- scope: audittemplate_scope
- links: links
- hostname: audit_hostname
- start_time: audit_starttime
- end_time: audit_endtime
**Example JSON representation of an Audit:**
@ -176,6 +180,8 @@ Response
- scope: audittemplate_scope
- links: links
- hostname: audit_hostname
- start_time: audit_starttime
- end_time: audit_endtime
**Example JSON representation of an Audit:**
@ -220,6 +226,8 @@ Response
- scope: audittemplate_scope
- links: links
- hostname: audit_hostname
- start_time: audit_starttime
- end_time: audit_endtime
**Example JSON representation of an Audit:**
@ -272,6 +280,8 @@ version 1:
- scope: audittemplate_scope
- links: links
- hostname: audit_hostname
- start_time: audit_starttime
- end_time: audit_endtime
**Example JSON representation of an Audit:**
@ -324,6 +334,8 @@ Response
- scope: audittemplate_scope
- links: links
- hostname: audit_hostname
- start_time: audit_starttime
- end_time: audit_endtime
**Example JSON representation of an Audit:**
@ -347,4 +359,4 @@ Request
.. rest_parameters:: parameters.yaml
- audit_ident: audit_ident
- audit_ident: audit_ident

View File

@ -57,6 +57,7 @@ lxml==4.1.1
Mako==1.0.7
MarkupSafe==1.0
mccabe==0.2.1
microversion_parse==0.2.1
mock==2.0.0
monotonic==1.4
mox3==0.25.0

View File

@ -0,0 +1,8 @@
---
features:
Watcher starts to support API microversions since Stein cycle. From now
onwards all API changes should be made with saving backward compatibility.
To specify API version operator should use OpenStack-API-Version
HTTP header. If operator wants to know the mininum and maximum supported
versions by API, he/she can access /v1 resource and Watcher API will
return appropriate headers in response.

View File

@ -45,4 +45,4 @@ taskflow>=3.1.0 # Apache-2.0
WebOb>=1.7.4 # MIT
WSME>=0.9.2 # MIT
networkx>=1.11 # BSD
microversion_parse>=0.2.1 # Apache-2.0

View File

@ -14,7 +14,10 @@
# limitations under the License.
import datetime
import functools
import microversion_parse
from webob import exc
import wsme
from wsme import types as wtypes
@ -49,3 +52,84 @@ class APIBase(wtypes.Base):
for k in self.as_dict():
if k not in except_list:
setattr(self, k, wsme.Unset)
@functools.total_ordering
class Version(object):
"""API Version object."""
string = 'OpenStack-API-Version'
"""HTTP Header string carrying the requested version"""
min_string = 'OpenStack-API-Minimum-Version'
"""HTTP response header"""
max_string = 'OpenStack-API-Maximum-Version'
"""HTTP response header"""
def __init__(self, headers, default_version, latest_version):
"""Create an API Version object from the supplied headers.
:param headers: webob headers
:param default_version: version to use if not specified in headers
:param latest_version: version to use if latest is requested
:raises: webob.HTTPNotAcceptable
"""
(self.major, self.minor) = Version.parse_headers(
headers, default_version, latest_version)
def __repr__(self):
return '%s.%s' % (self.major, self.minor)
@staticmethod
def parse_headers(headers, default_version, latest_version):
"""Determine the API version requested based on the headers supplied.
:param headers: webob headers
:param default_version: version to use if not specified in headers
:param latest_version: version to use if latest is requested
:returns: a tuple of (major, minor) version numbers
:raises: webob.HTTPNotAcceptable
"""
version_str = microversion_parse.get_version(
headers,
service_type='infra-optim')
minimal_version = (1, 0)
if version_str is None:
# If requested header is wrong, Watcher answers with the minimal
# supported version.
return minimal_version
if version_str.lower() == 'latest':
parse_str = latest_version
else:
parse_str = version_str
try:
version = tuple(int(i) for i in parse_str.split('.'))
except ValueError:
version = minimal_version
# NOTE (alexchadin): Old python-watcherclient sends requests with
# value of version header is "1". It should be transformed to 1.0 as
# it was supposed to be.
if len(version) == 1 and version[0] == 1:
version = minimal_version
if len(version) != 2:
raise exc.HTTPNotAcceptable(
"Invalid value for %s header" % Version.string)
return version
def __gt__(self, other):
return (self.major, self.minor) > (other.major, other.minor)
def __eq__(self, other):
return (self.major, self.minor) == (other.major, other.minor)
def __ne__(self, other):
return not self.__eq__(other)

View File

@ -14,6 +14,8 @@
# License for the specific language governing permissions and limitations
# under the License.
import importlib
import pecan
from pecan import rest
from wsme import types as wtypes
@ -24,19 +26,39 @@ from watcher.api.controllers import link
from watcher.api.controllers import v1
class APIStatus(object):
CURRENT = "CURRENT"
SUPPORTED = "SUPPORTED"
DEPRECATED = "DEPRECATED"
EXPERIMENTAL = "EXPERIMENTAL"
class Version(base.APIBase):
"""An API version representation."""
id = wtypes.text
"""The ID of the version, also acts as the release number"""
status = wtypes.text
"""The state of this API version"""
max_version = wtypes.text
"""The maximum version supported"""
min_version = wtypes.text
"""The minimum version supported"""
links = [link.Link]
"""A Link that point to a specific version of the API"""
@staticmethod
def convert(id):
def convert(id, status=APIStatus.CURRENT):
v = importlib.import_module('watcher.api.controllers.%s.versions' % id)
version = Version()
version.id = id
version.status = status
version.max_version = v.max_version_string()
version.min_version = v.min_version_string()
version.links = [link.Link.make_link('self', pecan.request.host_url,
id, '', bookmark=True)]
return version

View File

@ -24,10 +24,12 @@ import datetime
import pecan
from pecan import rest
from webob import exc
import wsme
from wsme import types as wtypes
import wsmeext.pecan as wsme_pecan
from watcher.api.controllers import base
from watcher.api.controllers import link
from watcher.api.controllers.v1 import action
from watcher.api.controllers.v1 import action_plan
@ -37,6 +39,21 @@ from watcher.api.controllers.v1 import goal
from watcher.api.controllers.v1 import scoring_engine
from watcher.api.controllers.v1 import service
from watcher.api.controllers.v1 import strategy
from watcher.api.controllers.v1 import versions
def min_version():
return base.Version(
{base.Version.string: ' '.join([versions.service_type_string(),
versions.min_version_string()])},
versions.min_version_string(), versions.max_version_string())
def max_version():
return base.Version(
{base.Version.string: ' '.join([versions.service_type_string(),
versions.max_version_string()])},
versions.min_version_string(), versions.max_version_string())
class APIBase(wtypes.Base):
@ -193,5 +210,50 @@ class Controller(rest.RestController):
# the request object to make the links.
return V1.convert()
def _check_version(self, version, headers=None):
if headers is None:
headers = {}
# ensure that major version in the URL matches the header
if version.major != versions.BASE_VERSION:
raise exc.HTTPNotAcceptable(
"Mutually exclusive versions requested. Version %(ver)s "
"requested but not supported by this service. The supported "
"version range is: [%(min)s, %(max)s]." %
{'ver': version, 'min': versions.min_version_string(),
'max': versions.max_version_string()},
headers=headers)
# ensure the minor version is within the supported range
if version < min_version() or version > max_version():
raise exc.HTTPNotAcceptable(
"Version %(ver)s was requested but the minor version is not "
"supported by this service. The supported version range is: "
"[%(min)s, %(max)s]." %
{'ver': version, 'min': versions.min_version_string(),
'max': versions.max_version_string()},
headers=headers)
@pecan.expose()
def _route(self, args, request=None):
v = base.Version(pecan.request.headers, versions.min_version_string(),
versions.max_version_string())
# The Vary header is used as a hint to caching proxies and user agents
# that the response is also dependent on the OpenStack-API-Version and
# not just the body and query parameters. See RFC 7231 for details.
pecan.response.headers['Vary'] = base.Version.string
# Always set the min and max headers
pecan.response.headers[base.Version.min_string] = (
versions.min_version_string())
pecan.response.headers[base.Version.max_string] = (
versions.max_version_string())
# assert that requested version is supported
self._check_version(v, pecan.response.headers)
pecan.response.headers[base.Version.string] = str(v)
pecan.request.version = v
return super(Controller, self)._route(args, request)
__all__ = ("Controller", )

View File

@ -74,6 +74,16 @@ from watcher.common import policy
from watcher import objects
def hide_fields_in_newer_versions(obj):
"""This method hides fields that were added in newer API versions.
Certain node fields were introduced at certain API versions.
These fields are only made available when the request's API version
matches or exceeds the versions when these fields were introduced.
"""
pass
class ActionPatchType(types.JsonPatchType):
@staticmethod
@ -174,6 +184,8 @@ class Action(base.APIBase):
description = ""
setattr(action, 'description', description)
hide_fields_in_newer_versions(action)
return cls._convert_with_links(action, pecan.request.host_url, expand)
@classmethod

View File

@ -80,6 +80,16 @@ from watcher.objects import action_plan as ap_objects
LOG = log.getLogger(__name__)
def hide_fields_in_newer_versions(obj):
"""This method hides fields that were added in newer API versions.
Certain node fields were introduced at certain API versions.
These fields are only made available when the request's API version
matches or exceeds the versions when these fields were introduced.
"""
pass
class ActionPlanPatchType(types.JsonPatchType):
@staticmethod
@ -273,6 +283,7 @@ class ActionPlan(base.APIBase):
@classmethod
def convert_with_links(cls, rpc_action_plan, expand=True):
action_plan = ActionPlan(**rpc_action_plan.as_dict())
hide_fields_in_newer_versions(action_plan)
return cls._convert_with_links(action_plan, pecan.request.host_url,
expand)

View File

@ -63,6 +63,18 @@ def _get_object_by_value(context, class_name, value):
return class_name.get_by_name(context, value)
def hide_fields_in_newer_versions(obj):
"""This method hides fields that were added in newer API versions.
Certain node fields were introduced at certain API versions.
These fields are only made available when the request's API version
matches or exceeds the versions when these fields were introduced.
"""
if not api_utils.allow_start_end_audit_time():
obj.start_time = wsme.Unset
obj.end_time = wsme.Unset
class AuditPostType(wtypes.Base):
name = wtypes.wsattr(wtypes.text, mandatory=False)
@ -116,6 +128,11 @@ class AuditPostType(wtypes.Base):
raise exception.AuditStartEndTimeNotAllowed(
audit_type=self.audit_type)
if not api_utils.allow_start_end_audit_time():
for field in ('start_time', 'end_time'):
if getattr(self, field) not in (wsme.Unset, None):
raise exception.NotAcceptable()
# If audit_template_uuid was provided, we will provide any
# variables not included in the request, but not override
# those variables that were included.
@ -388,6 +405,7 @@ class Audit(base.APIBase):
@classmethod
def convert_with_links(cls, rpc_audit, expand=True):
audit = Audit(**rpc_audit.as_dict())
hide_fields_in_newer_versions(audit)
return cls._convert_with_links(audit, pecan.request.host_url, expand)
@classmethod

View File

@ -65,6 +65,16 @@ from watcher.decision_engine.loading import default as default_loading
from watcher import objects
def hide_fields_in_newer_versions(obj):
"""This method hides fields that were added in newer API versions.
Certain node fields were introduced at certain API versions.
These fields are only made available when the request's API version
matches or exceeds the versions when these fields were introduced.
"""
pass
class AuditTemplatePostType(wtypes.Base):
_ctx = context_utils.make_context()
@ -410,6 +420,7 @@ class AuditTemplate(base.APIBase):
@classmethod
def convert_with_links(cls, rpc_audit_template, expand=True):
audit_template = AuditTemplate(**rpc_audit_template.as_dict())
hide_fields_in_newer_versions(audit_template)
return cls._convert_with_links(audit_template, pecan.request.host_url,
expand)

View File

@ -48,6 +48,16 @@ from watcher.common import policy
from watcher import objects
def hide_fields_in_newer_versions(obj):
"""This method hides fields that were added in newer API versions.
Certain node fields were introduced at certain API versions.
These fields are only made available when the request's API version
matches or exceeds the versions when these fields were introduced.
"""
pass
class Goal(base.APIBase):
"""API representation of a goal.
@ -97,6 +107,7 @@ class Goal(base.APIBase):
@classmethod
def convert_with_links(cls, goal, expand=True):
goal = Goal(**goal.as_dict())
hide_fields_in_newer_versions(goal)
return cls._convert_with_links(goal, pecan.request.host_url, expand)
@classmethod

View File

@ -43,6 +43,16 @@ from watcher.common import policy
from watcher import objects
def hide_fields_in_newer_versions(obj):
"""This method hides fields that were added in newer API versions.
Certain node fields were introduced at certain API versions.
These fields are only made available when the request's API version
matches or exceeds the versions when these fields were introduced.
"""
pass
class ScoringEngine(base.APIBase):
"""API representation of a scoring engine.
@ -95,6 +105,7 @@ class ScoringEngine(base.APIBase):
@classmethod
def convert_with_links(cls, scoring_engine, expand=True):
scoring_engine = ScoringEngine(**scoring_engine.as_dict())
hide_fields_in_newer_versions(scoring_engine)
return cls._convert_with_links(
scoring_engine, pecan.request.host_url, expand)

View File

@ -44,6 +44,16 @@ CONF = cfg.CONF
LOG = log.getLogger(__name__)
def hide_fields_in_newer_versions(obj):
"""This method hides fields that were added in newer API versions.
Certain node fields were introduced at certain API versions.
These fields are only made available when the request's API version
matches or exceeds the versions when these fields were introduced.
"""
pass
class Service(base.APIBase):
"""API representation of a service.
@ -126,6 +136,7 @@ class Service(base.APIBase):
@classmethod
def convert_with_links(cls, service, expand=True):
service = Service(**service.as_dict())
hide_fields_in_newer_versions(service)
return cls._convert_with_links(
service, pecan.request.host_url, expand)

View File

@ -45,6 +45,16 @@ from watcher.decision_engine import rpcapi
from watcher import objects
def hide_fields_in_newer_versions(obj):
"""This method hides fields that were added in newer API versions.
Certain node fields were introduced at certain API versions.
These fields are only made available when the request's API version
matches or exceeds the versions when these fields were introduced.
"""
pass
class Strategy(base.APIBase):
"""API representation of a strategy.
@ -146,6 +156,7 @@ class Strategy(base.APIBase):
@classmethod
def convert_with_links(cls, strategy, expand=True):
strategy = Strategy(**strategy.as_dict())
hide_fields_in_newer_versions(strategy)
return cls._convert_with_links(
strategy, pecan.request.host_url, expand)

View File

@ -23,6 +23,7 @@ import pecan
import wsme
from watcher._i18n import _
from watcher.api.controllers.v1 import versions
from watcher.common import utils
from watcher import objects
@ -155,3 +156,12 @@ def get_resource(resource, resource_id, eager=False):
return _get(pecan.request.context, resource_id, eager=eager)
return _get(pecan.request.context, resource_id)
def allow_start_end_audit_time():
"""Check if we should support optional start/end attributes for Audit.
Version 1.1 of the API added support for start and end time of continuous
audits.
"""
return pecan.request.version.minor >= versions.MINOR_1_START_END_TIMING

View File

@ -0,0 +1,52 @@
# Copyright (c) 2015 Intel Corporation
# Copyright (c) 2018 SBCloud
# 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.
# This is the version 1 API
BASE_VERSION = 1
# Here goes a short log of changes in every version.
#
# v1.0: corresponds to Rocky API
# v1.1: Add start/end time for continuous audit
MINOR_0_ROCKY = 0
MINOR_1_START_END_TIMING = 1
MINOR_MAX_VERSION = MINOR_1_START_END_TIMING
# String representations of the minor and maximum versions
_MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_0_ROCKY)
_MAX_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_MAX_VERSION)
def service_type_string():
return 'infra-optim'
def min_version_string():
"""Returns the minimum supported API version (as a string)"""
return _MIN_VERSION_STRING
def max_version_string():
"""Returns the maximum supported API version (as a string).
If the service is pinned, the maximum API version is the pinned
version. Otherwise, it is the maximum supported API version.
"""
return _MAX_VERSION_STRING

View File

@ -122,6 +122,11 @@ class NotAuthorized(WatcherException):
code = 403
class NotAcceptable(WatcherException):
msg_fmt = _("Request not acceptable.")
code = 406
class PolicyNotAuthorized(NotAuthorized):
msg_fmt = _("Policy doesn't allow %(action)s to be performed.")

View File

@ -203,7 +203,8 @@ class FunctionalTest(base.DbTestCase):
return response
def get_json(self, path, expect_errors=False, headers=None,
extra_environ=None, q=[], path_prefix=PATH_PREFIX, **params):
extra_environ=None, q=[], path_prefix=PATH_PREFIX,
return_json=True, **params):
"""Sends simulated HTTP GET request to Pecan test app.
:param path: url path of target service
@ -236,7 +237,7 @@ class FunctionalTest(base.DbTestCase):
headers=headers,
extra_environ=extra_environ,
expect_errors=expect_errors)
if not expect_errors:
if return_json and not expect_errors:
response = response.json
print('GOT:%s' % response)
return response

View File

@ -900,7 +900,10 @@ class TestPost(api_base.FunctionalTest):
audit_dict['start_time'] = str(start_time)
audit_dict['end_time'] = str(end_time)
response = self.post_json('/audits', audit_dict)
response = self.post_json(
'/audits',
audit_dict,
headers={'OpenStack-API-Version': 'infra-optim 1.1'})
self.assertEqual('application/json', response.content_type)
self.assertEqual(201, response.status_int)
self.assertEqual(objects.audit.State.PENDING,
@ -919,6 +922,34 @@ class TestPost(api_base.FunctionalTest):
self.assertEqual(iso_start_time, return_start_time)
self.assertEqual(iso_end_time, return_end_time)
@mock.patch.object(deapi.DecisionEngineAPI, 'trigger_audit')
def test_create_continuous_audit_with_start_end_time_incompatible_version(
self, mock_trigger_audit):
mock_trigger_audit.return_value = mock.ANY
start_time = datetime.datetime(2018, 3, 1, 0, 0)
end_time = datetime.datetime(2018, 4, 1, 0, 0)
audit_dict = post_get_test_audit(
params_to_exclude=['uuid', 'state', 'scope',
'next_run_time', 'hostname', 'goal']
)
audit_dict['audit_type'] = objects.audit.AuditType.CONTINUOUS.value
audit_dict['interval'] = '1200'
audit_dict['start_time'] = str(start_time)
audit_dict['end_time'] = str(end_time)
response = self.post_json(
'/audits',
audit_dict,
headers={'X-OpenStack-Watcher-API-Version': '1.0'},
expect_errors=True)
self.assertEqual('application/json', response.content_type)
self.assertEqual(406, response.status_int)
expected_error_msg = 'Request not acceptable.'
self.assertTrue(response.json['error_message'])
self.assertIn(expected_error_msg, response.json['error_message'])
assert not mock_trigger_audit.called
class TestDelete(api_base.FunctionalTest):

View File

@ -0,0 +1,104 @@
# 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.
from watcher.api.controllers.v1 import versions
from watcher.tests.api import base as api_base
SERVICE_TYPE = 'infra-optim'
H_MIN_VER = 'openstack-api-minimum-version'
H_MAX_VER = 'openstack-api-maximum-version'
H_RESP_VER = 'openstack-api-version'
MIN_VER = versions.min_version_string()
MAX_VER = versions.max_version_string()
class TestMicroversions(api_base.FunctionalTest):
controller_list_response = [
'scoring_engines', 'audit_templates', 'audits', 'actions',
'action_plans', 'services']
def setUp(self):
super(TestMicroversions, self).setUp()
def test_wrong_major_version(self):
response = self.get_json(
'/',
headers={'OpenStack-API-Version': ' '.join([SERVICE_TYPE,
'10'])},
expect_errors=True, return_json=False)
self.assertEqual('application/json', response.content_type)
self.assertEqual(406, response.status_int)
expected_error_msg = ('Invalid value for'
' OpenStack-API-Version header')
self.assertTrue(response.json['error_message'])
self.assertIn(expected_error_msg, response.json['error_message'])
def test_extend_initial_version_with_micro(self):
response = self.get_json(
'/',
headers={'OpenStack-API-Version': ' '.join([SERVICE_TYPE,
'1'])},
return_json=False)
self.assertEqual(response.headers[H_MIN_VER], MIN_VER)
self.assertEqual(response.headers[H_MAX_VER], MAX_VER)
self.assertEqual(response.headers[H_RESP_VER], MIN_VER)
self.assertTrue(all(x in response.json.keys() for x in
self.controller_list_response))
def test_without_microversion(self):
response = self.get_json('/', return_json=False)
self.assertEqual(response.headers[H_MIN_VER], MIN_VER)
self.assertEqual(response.headers[H_MAX_VER], MAX_VER)
self.assertEqual(response.headers[H_RESP_VER], MIN_VER)
self.assertTrue(all(x in response.json.keys() for x in
self.controller_list_response))
def test_new_client_new_api(self):
response = self.get_json(
'/',
headers={'OpenStack-API-Version': ' '.join([SERVICE_TYPE,
'1.1'])},
return_json=False)
self.assertEqual(response.headers[H_MIN_VER], MIN_VER)
self.assertEqual(response.headers[H_MAX_VER], MAX_VER)
self.assertEqual(response.headers[H_RESP_VER], '1.1')
self.assertTrue(all(x in response.json.keys() for x in
self.controller_list_response))
def test_latest_microversion(self):
response = self.get_json(
'/',
headers={'OpenStack-API-Version': ' '.join([SERVICE_TYPE,
'latest'])},
return_json=False)
self.assertEqual(response.headers[H_MIN_VER], MIN_VER)
self.assertEqual(response.headers[H_MAX_VER], MAX_VER)
self.assertEqual(response.headers[H_RESP_VER], MAX_VER)
self.assertTrue(all(x in response.json.keys() for x in
self.controller_list_response))
def test_unsupported_version(self):
response = self.get_json(
'/',
headers={'OpenStack-API-Version': ' '.join([SERVICE_TYPE,
'1.999'])},
expect_errors=True)
self.assertEqual(406, response.status_int)
self.assertEqual(response.headers[H_MIN_VER], MIN_VER)
self.assertEqual(response.headers[H_MAX_VER], MAX_VER)
expected_error_msg = ('Version 1.999 was requested but the minor '
'version is not supported by this service. '
'The supported version range is')
self.assertTrue(response.json['error_message'])
self.assertIn(expected_error_msg, response.json['error_message'])