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

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