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:
parent
c2550e534e
commit
c4a30153f1
3
.gitignore
vendored
3
.gitignore
vendored
@ -75,3 +75,6 @@ releasenotes/build
|
||||
|
||||
# Autogenerated sample config file
|
||||
etc/watcher/watcher.conf.sample
|
||||
|
||||
# Atom
|
||||
.remote-sync.json
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
@ -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"
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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:**
|
||||
|
||||
|
@ -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
|
||||
|
@ -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.
|
@ -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
|
||||
|
@ -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
|
@ -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.")
|
||||
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
||||
|
104
watcher/tests/api/v1/test_microversions.py
Normal file
104
watcher/tests/api/v1/test_microversions.py
Normal 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'])
|
Loading…
Reference in New Issue
Block a user