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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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", )
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
52
watcher/api/controllers/v1/versions.py
Normal file
52
watcher/api/controllers/v1/versions.py
Normal 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
|
||||
Reference in New Issue
Block a user