initial version
Change-Id: I699e0ab082657880998d8618fe29eb7f56c6c661
This commit is contained in:
6
watcher/api/README.md
Normal file
6
watcher/api/README.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Watcher API
|
||||
|
||||
This component implements the REST API provided by the Watcher system to the external world. It enables a cluster administrator to control and monitor the Watcher system via any interaction mechanism connected to this API :
|
||||
* CLI
|
||||
* Horizon plugin
|
||||
* Python SDK
|
||||
0
watcher/api/__init__.py
Normal file
0
watcher/api/__init__.py
Normal file
49
watcher/api/acl.py
Normal file
49
watcher/api/acl.py
Normal file
@@ -0,0 +1,49 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Copyright © 2012 New Dream Network, LLC (DreamHost)
|
||||
#
|
||||
# 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.
|
||||
|
||||
|
||||
"""Access Control Lists (ACL's) control access the API server."""
|
||||
|
||||
from oslo_config import cfg
|
||||
from watcher.api.middleware import auth_token
|
||||
|
||||
|
||||
AUTH_OPTS = [
|
||||
cfg.BoolOpt('enable_authentication',
|
||||
default=True,
|
||||
help='This option enables or disables user authentication '
|
||||
'via keystone. Default value is True.'),
|
||||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(AUTH_OPTS)
|
||||
|
||||
|
||||
def install(app, conf, public_routes):
|
||||
"""Install ACL check on application.
|
||||
|
||||
:param app: A WSGI applicatin.
|
||||
:param conf: Settings. Dict'ified and passed to keystonemiddleware
|
||||
:param public_routes: The list of the routes which will be allowed to
|
||||
access without authentication.
|
||||
:return: The same WSGI application with ACL installed.
|
||||
|
||||
"""
|
||||
if not cfg.CONF.get('enable_authentication'):
|
||||
return app
|
||||
return auth_token.AuthTokenMiddleware(app,
|
||||
conf=dict(conf),
|
||||
public_api_routes=public_routes)
|
||||
70
watcher/api/app.py
Normal file
70
watcher/api/app.py
Normal file
@@ -0,0 +1,70 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
# Copyright © 2012 New Dream Network, LLC (DreamHost)
|
||||
# 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.
|
||||
|
||||
|
||||
from oslo_config import cfg
|
||||
import pecan
|
||||
|
||||
from watcher.api import acl
|
||||
from watcher.api import config as api_config
|
||||
from watcher.api import middleware
|
||||
from watcher.decision_engine.framework.strategy import strategy_selector
|
||||
|
||||
# Register options for the service
|
||||
API_SERVICE_OPTS = [
|
||||
cfg.IntOpt('port',
|
||||
default=9322,
|
||||
help='The port for the watcher API server'),
|
||||
cfg.StrOpt('host',
|
||||
default='0.0.0.0',
|
||||
help='The listen IP for the watcher API server'),
|
||||
cfg.IntOpt('max_limit',
|
||||
default=1000,
|
||||
help='The maximum number of items returned in a single '
|
||||
'response from a collection resource.')
|
||||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
opt_group = cfg.OptGroup(name='api',
|
||||
title='Options for the watcher-api service')
|
||||
|
||||
CONF.register_group(opt_group)
|
||||
CONF.register_opts(API_SERVICE_OPTS, opt_group)
|
||||
CONF.register_opts(strategy_selector.WATCHER_GOALS_OPTS)
|
||||
|
||||
|
||||
def get_pecan_config():
|
||||
# Set up the pecan configuration
|
||||
filename = api_config.__file__.replace('.pyc', '.py')
|
||||
return pecan.configuration.conf_from_file(filename)
|
||||
|
||||
|
||||
def setup_app(config=None):
|
||||
if not config:
|
||||
config = get_pecan_config()
|
||||
|
||||
app_conf = dict(config.app)
|
||||
|
||||
app = pecan.make_app(
|
||||
app_conf.pop('root'),
|
||||
logging=getattr(config, 'logging', {}),
|
||||
debug=CONF.debug,
|
||||
wrap_app=middleware.ParsableErrorMiddleware,
|
||||
**app_conf
|
||||
)
|
||||
|
||||
return acl.install(app, CONF, config.app.acl_public_routes)
|
||||
46
watcher/api/config.py
Normal file
46
watcher/api/config.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from oslo_config import cfg
|
||||
from watcher.api import hooks
|
||||
|
||||
# Server Specific Configurations
|
||||
# See https://pecan.readthedocs.org/en/latest/configuration.html#server-configuration # noqa
|
||||
server = {
|
||||
'port': '9322',
|
||||
'host': '0.0.0.0'
|
||||
}
|
||||
|
||||
# Pecan Application Configurations
|
||||
# See https://pecan.readthedocs.org/en/latest/configuration.html#application-configuration # noqa
|
||||
app = {
|
||||
'root': 'watcher.api.controllers.root.RootController',
|
||||
'modules': ['watcher.api'],
|
||||
'hooks': [
|
||||
hooks.ContextHook(),
|
||||
hooks.NoExceptionTracebackHook(),
|
||||
],
|
||||
'static_root': '%(confdir)s/public',
|
||||
'enable_acl': True,
|
||||
'acl_public_routes': [
|
||||
'/',
|
||||
],
|
||||
}
|
||||
|
||||
# WSME Configurations
|
||||
# See https://wsme.readthedocs.org/en/latest/integrate.html#configuration
|
||||
wsme = {
|
||||
'debug': cfg.CONF.debug,
|
||||
}
|
||||
0
watcher/api/controllers/__init__.py
Normal file
0
watcher/api/controllers/__init__.py
Normal file
51
watcher/api/controllers/base.py
Normal file
51
watcher/api/controllers/base.py
Normal file
@@ -0,0 +1,51 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import datetime
|
||||
|
||||
import wsme
|
||||
from wsme import types as wtypes
|
||||
|
||||
|
||||
class APIBase(wtypes.Base):
|
||||
|
||||
created_at = wsme.wsattr(datetime.datetime, readonly=True)
|
||||
"""The time in UTC at which the object is created"""
|
||||
|
||||
updated_at = wsme.wsattr(datetime.datetime, readonly=True)
|
||||
"""The time in UTC at which the object is updated"""
|
||||
|
||||
deleted_at = wsme.wsattr(datetime.datetime, readonly=True)
|
||||
"""The time in UTC at which the object is deleted"""
|
||||
|
||||
def as_dict(self):
|
||||
"""Render this object as a dict of its fields."""
|
||||
return dict((k, getattr(self, k))
|
||||
for k in self.fields
|
||||
if hasattr(self, k) and
|
||||
getattr(self, k) != wsme.Unset)
|
||||
|
||||
def unset_fields_except(self, except_list=None):
|
||||
"""Unset fields so they don't appear in the message body.
|
||||
|
||||
:param except_list: A list of fields that won't be touched.
|
||||
|
||||
"""
|
||||
if except_list is None:
|
||||
except_list = []
|
||||
|
||||
for k in self.as_dict():
|
||||
if k not in except_list:
|
||||
setattr(self, k, wsme.Unset)
|
||||
60
watcher/api/controllers/link.py
Normal file
60
watcher/api/controllers/link.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright 2013 Red Hat, Inc.
|
||||
# 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.
|
||||
|
||||
import pecan
|
||||
from wsme import types as wtypes
|
||||
|
||||
from watcher.api.controllers import base
|
||||
|
||||
|
||||
def build_url(resource, resource_args, bookmark=False, base_url=None):
|
||||
if base_url is None:
|
||||
base_url = pecan.request.host_url
|
||||
|
||||
template = '%(url)s/%(res)s' if bookmark else '%(url)s/v1/%(res)s'
|
||||
# FIXME(lucasagomes): I'm getting a 404 when doing a GET on
|
||||
# a nested resource that the URL ends with a '/'.
|
||||
# https://groups.google.com/forum/#!topic/pecan-dev/QfSeviLg5qs
|
||||
template += '%(args)s' if resource_args.startswith('?') else '/%(args)s'
|
||||
return template % {'url': base_url, 'res': resource, 'args': resource_args}
|
||||
|
||||
|
||||
class Link(base.APIBase):
|
||||
"""A link representation."""
|
||||
|
||||
href = wtypes.text
|
||||
"""The url of a link."""
|
||||
|
||||
rel = wtypes.text
|
||||
"""The name of a link."""
|
||||
|
||||
type = wtypes.text
|
||||
"""Indicates the type of document/link."""
|
||||
|
||||
@staticmethod
|
||||
def make_link(rel_name, url, resource, resource_args,
|
||||
bookmark=False, type=wtypes.Unset):
|
||||
href = build_url(resource, resource_args,
|
||||
bookmark=bookmark, base_url=url)
|
||||
return Link(href=href, rel=rel_name, type=type)
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
sample = cls(href="http://localhost:6385/chassis/"
|
||||
"eaaca217-e7d8-47b4-bb41-3f99f20eed89",
|
||||
rel="bookmark")
|
||||
return sample
|
||||
98
watcher/api/controllers/root.py
Normal file
98
watcher/api/controllers/root.py
Normal file
@@ -0,0 +1,98 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Copyright © 2012 New Dream Network, LLC (DreamHost)
|
||||
#
|
||||
# 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.
|
||||
|
||||
import pecan
|
||||
from pecan import rest
|
||||
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 import v1
|
||||
|
||||
|
||||
class Version(base.APIBase):
|
||||
"""An API version representation."""
|
||||
|
||||
id = wtypes.text
|
||||
"""The ID of the version, also acts as the release number"""
|
||||
|
||||
links = [link.Link]
|
||||
"""A Link that point to a specific version of the API"""
|
||||
|
||||
@staticmethod
|
||||
def convert(id):
|
||||
version = Version()
|
||||
version.id = id
|
||||
version.links = [link.Link.make_link('self', pecan.request.host_url,
|
||||
id, '', bookmark=True)]
|
||||
return version
|
||||
|
||||
|
||||
class Root(base.APIBase):
|
||||
|
||||
name = wtypes.text
|
||||
"""The name of the API"""
|
||||
|
||||
description = wtypes.text
|
||||
"""Some information about this API"""
|
||||
|
||||
versions = [Version]
|
||||
"""Links to all the versions available in this API"""
|
||||
|
||||
default_version = Version
|
||||
"""A link to the default version of the API"""
|
||||
|
||||
@staticmethod
|
||||
def convert():
|
||||
root = Root()
|
||||
root.name = "OpenStack Watcher API"
|
||||
root.description = ("Watcher is an OpenStack project which aims to "
|
||||
"to improve physical resources usage through "
|
||||
"better VM placement.")
|
||||
root.versions = [Version.convert('v1')]
|
||||
root.default_version = Version.convert('v1')
|
||||
return root
|
||||
|
||||
|
||||
class RootController(rest.RestController):
|
||||
|
||||
_versions = ['v1']
|
||||
"""All supported API versions"""
|
||||
|
||||
_default_version = 'v1'
|
||||
"""The default API version"""
|
||||
|
||||
v1 = v1.Controller()
|
||||
|
||||
@wsme_pecan.wsexpose(Root)
|
||||
def get(self):
|
||||
# NOTE: The reason why convert() it's being called for every
|
||||
# request is because we need to get the host url from
|
||||
# the request object to make the links.
|
||||
return Root.convert()
|
||||
|
||||
@pecan.expose()
|
||||
def _route(self, args):
|
||||
"""Overrides the default routing behavior.
|
||||
|
||||
It redirects the request to the default version of the watcher API
|
||||
if the version number is not specified in the url.
|
||||
"""
|
||||
|
||||
if args[0] and args[0] not in self._versions:
|
||||
args = [self._default_version] + args
|
||||
return super(RootController, self)._route(args)
|
||||
166
watcher/api/controllers/v1/__init__.py
Normal file
166
watcher/api/controllers/v1/__init__.py
Normal file
@@ -0,0 +1,166 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
"""
|
||||
Version 1 of the Watcher API
|
||||
|
||||
NOTE: IN PROGRESS AND NOT FULLY IMPLEMENTED.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
import pecan
|
||||
from pecan import rest
|
||||
import wsme
|
||||
from wsme import types as wtypes
|
||||
import wsmeext.pecan as wsme_pecan
|
||||
|
||||
from watcher.api.controllers import link
|
||||
from watcher.api.controllers.v1 import action
|
||||
from watcher.api.controllers.v1 import action_plan
|
||||
from watcher.api.controllers.v1 import audit
|
||||
from watcher.api.controllers.v1 import audit_template
|
||||
|
||||
|
||||
class APIBase(wtypes.Base):
|
||||
|
||||
created_at = wsme.wsattr(datetime.datetime, readonly=True)
|
||||
"""The time in UTC at which the object is created"""
|
||||
|
||||
updated_at = wsme.wsattr(datetime.datetime, readonly=True)
|
||||
"""The time in UTC at which the object is updated"""
|
||||
|
||||
deleted_at = wsme.wsattr(datetime.datetime, readonly=True)
|
||||
"""The time in UTC at which the object is deleted"""
|
||||
|
||||
def as_dict(self):
|
||||
"""Render this object as a dict of its fields."""
|
||||
return dict((k, getattr(self, k))
|
||||
for k in self.fields
|
||||
if hasattr(self, k) and
|
||||
getattr(self, k) != wsme.Unset)
|
||||
|
||||
def unset_fields_except(self, except_list=None):
|
||||
"""Unset fields so they don't appear in the message body.
|
||||
|
||||
:param except_list: A list of fields that won't be touched.
|
||||
|
||||
"""
|
||||
if except_list is None:
|
||||
except_list = []
|
||||
|
||||
for k in self.as_dict():
|
||||
if k not in except_list:
|
||||
setattr(self, k, wsme.Unset)
|
||||
|
||||
|
||||
class MediaType(APIBase):
|
||||
"""A media type representation."""
|
||||
|
||||
base = wtypes.text
|
||||
type = wtypes.text
|
||||
|
||||
def __init__(self, base, type):
|
||||
self.base = base
|
||||
self.type = type
|
||||
|
||||
|
||||
class V1(APIBase):
|
||||
"""The representation of the version 1 of the API."""
|
||||
|
||||
id = wtypes.text
|
||||
"""The ID of the version, also acts as the release number"""
|
||||
|
||||
media_types = [MediaType]
|
||||
"""An array of supcontainersed media types for this version"""
|
||||
|
||||
audit_templates = [link.Link]
|
||||
"""Links to the audit templates resource"""
|
||||
|
||||
audits = [link.Link]
|
||||
"""Links to the audits resource"""
|
||||
|
||||
actions = [link.Link]
|
||||
"""Links to the actions resource"""
|
||||
|
||||
action_plans = [link.Link]
|
||||
"""Links to the action plans resource"""
|
||||
|
||||
links = [link.Link]
|
||||
"""Links that point to a specific URL for this version and documentation"""
|
||||
|
||||
@staticmethod
|
||||
def convert():
|
||||
v1 = V1()
|
||||
v1.id = "v1"
|
||||
v1.links = [link.Link.make_link('self', pecan.request.host_url,
|
||||
'v1', '', bookmark=True),
|
||||
link.Link.make_link('describedby',
|
||||
'http://docs.openstack.org',
|
||||
'developer/watcher/dev',
|
||||
'api-spec-v1.html',
|
||||
bookmark=True, type='text/html')
|
||||
]
|
||||
v1.media_types = [MediaType('application/json',
|
||||
'application/vnd.openstack.watcher.v1+json')]
|
||||
v1.audit_templates = [link.Link.make_link('self',
|
||||
pecan.request.host_url,
|
||||
'audit_templates', ''),
|
||||
link.Link.make_link('bookmark',
|
||||
pecan.request.host_url,
|
||||
'audit_templates', '',
|
||||
bookmark=True)
|
||||
]
|
||||
v1.audits = [link.Link.make_link('self', pecan.request.host_url,
|
||||
'audits', ''),
|
||||
link.Link.make_link('bookmark',
|
||||
pecan.request.host_url,
|
||||
'audits', '',
|
||||
bookmark=True)
|
||||
]
|
||||
v1.actions = [link.Link.make_link('self', pecan.request.host_url,
|
||||
'actions', ''),
|
||||
link.Link.make_link('bookmark',
|
||||
pecan.request.host_url,
|
||||
'actions', '',
|
||||
bookmark=True)
|
||||
]
|
||||
v1.action_plans = [link.Link.make_link(
|
||||
'self', pecan.request.host_url, 'action_plans', ''),
|
||||
link.Link.make_link('bookmark',
|
||||
pecan.request.host_url,
|
||||
'action_plans', '',
|
||||
bookmark=True)
|
||||
]
|
||||
return v1
|
||||
|
||||
|
||||
class Controller(rest.RestController):
|
||||
"""Version 1 API controller root."""
|
||||
|
||||
audits = audit.AuditsController()
|
||||
audit_templates = audit_template.AuditTemplatesController()
|
||||
actions = action.ActionsController()
|
||||
action_plans = action_plan.ActionPlansController()
|
||||
|
||||
@wsme_pecan.wsexpose(V1)
|
||||
def get(self):
|
||||
# NOTE: The reason why convert() it's being called for every
|
||||
# request is because we need to get the host url from
|
||||
# the request object to make the links.
|
||||
return V1.convert()
|
||||
|
||||
__all__ = (Controller)
|
||||
397
watcher/api/controllers/v1/action.py
Normal file
397
watcher/api/controllers/v1/action.py
Normal file
@@ -0,0 +1,397 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright 2013 Red Hat, Inc.
|
||||
# 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.
|
||||
|
||||
import datetime
|
||||
|
||||
import pecan
|
||||
from pecan import rest
|
||||
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 collection
|
||||
from watcher.api.controllers.v1 import types
|
||||
from watcher.api.controllers.v1 import utils as api_utils
|
||||
from watcher.common import exception
|
||||
from watcher import objects
|
||||
|
||||
|
||||
class ActionPatchType(types.JsonPatchType):
|
||||
|
||||
@staticmethod
|
||||
def mandatory_attrs():
|
||||
return []
|
||||
|
||||
|
||||
class Action(base.APIBase):
|
||||
"""API representation of a action.
|
||||
|
||||
This class enforces type checking and value constraints, and converts
|
||||
between the internal object model and the API representation of a action.
|
||||
"""
|
||||
_action_plan_uuid = None
|
||||
_next_uuid = None
|
||||
|
||||
def _get_action_plan_uuid(self):
|
||||
return self._action_plan_uuid
|
||||
|
||||
def _set_action_plan_uuid(self, value):
|
||||
if value == wtypes.Unset:
|
||||
self._action_plan_uuid = wtypes.Unset
|
||||
elif value and self._action_plan_uuid != value:
|
||||
try:
|
||||
action_plan = objects.ActionPlan.get(
|
||||
pecan.request.context, value)
|
||||
self._action_plan_uuid = action_plan.uuid
|
||||
self.action_plan_id = action_plan.id
|
||||
except exception.ActionPlanNotFound:
|
||||
self._action_plan_uuid = None
|
||||
|
||||
def _get_next_uuid(self):
|
||||
return self._next_uuid
|
||||
|
||||
def _set_next_uuid(self, value):
|
||||
if value == wtypes.Unset:
|
||||
self._next_uuid = wtypes.Unset
|
||||
elif value and self._next_uuid != value:
|
||||
try:
|
||||
action_next = objects.Action.get(
|
||||
pecan.request.context, value)
|
||||
self._next_uuid = action_next.uuid
|
||||
self.next = action_next.id
|
||||
except exception.ActionNotFound:
|
||||
self.action_next_uuid = None
|
||||
# raise e
|
||||
|
||||
uuid = types.uuid
|
||||
"""Unique UUID for this action"""
|
||||
|
||||
action_plan_uuid = wsme.wsproperty(types.uuid, _get_action_plan_uuid,
|
||||
_set_action_plan_uuid,
|
||||
mandatory=True)
|
||||
"""The action plan this action belongs to """
|
||||
|
||||
description = wtypes.text
|
||||
"""Description of this action"""
|
||||
|
||||
state = wtypes.text
|
||||
"""This audit state"""
|
||||
|
||||
alarm = types.uuid
|
||||
"""An alarm UUID related to this action"""
|
||||
|
||||
applies_to = wtypes.text
|
||||
"""Applies to"""
|
||||
|
||||
src = wtypes.text
|
||||
"""Hypervisor source"""
|
||||
|
||||
dst = wtypes.text
|
||||
"""Hypervisor source"""
|
||||
|
||||
action_type = wtypes.text
|
||||
"""Action type"""
|
||||
|
||||
parameter = wtypes.text
|
||||
"""Additionnal parameter"""
|
||||
|
||||
next_uuid = wsme.wsproperty(types.uuid, _get_next_uuid,
|
||||
_set_next_uuid,
|
||||
mandatory=True)
|
||||
"""This next action UUID"""
|
||||
|
||||
links = wsme.wsattr([link.Link], readonly=True)
|
||||
"""A list containing a self link and associated action links"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(Action, self).__init__()
|
||||
|
||||
self.fields = []
|
||||
fields = list(objects.Action.fields)
|
||||
# audit_template_uuid is not part of objects.Audit.fields
|
||||
# because it's an API-only attribute.
|
||||
fields.append('action_plan_uuid')
|
||||
fields.append('next_uuid')
|
||||
for field in fields:
|
||||
# Skip fields we do not expose.
|
||||
if not hasattr(self, field):
|
||||
continue
|
||||
self.fields.append(field)
|
||||
setattr(self, field, kwargs.get(field, wtypes.Unset))
|
||||
|
||||
self.fields.append('action_plan_id')
|
||||
setattr(self, 'action_plan_uuid', kwargs.get('action_plan_id',
|
||||
wtypes.Unset))
|
||||
setattr(self, 'next_uuid', kwargs.get('next',
|
||||
wtypes.Unset))
|
||||
|
||||
@staticmethod
|
||||
def _convert_with_links(action, url, expand=True):
|
||||
if not expand:
|
||||
action.unset_fields_except(['uuid', 'state', 'next', 'next_uuid',
|
||||
'action_plan_uuid', 'action_plan_id',
|
||||
'action_type'])
|
||||
|
||||
action.links = [link.Link.make_link('self', url,
|
||||
'actions', action.uuid),
|
||||
link.Link.make_link('bookmark', url,
|
||||
'actions', action.uuid,
|
||||
bookmark=True)
|
||||
]
|
||||
return action
|
||||
|
||||
@classmethod
|
||||
def convert_with_links(cls, action, expand=True):
|
||||
action = Action(**action.as_dict())
|
||||
return cls._convert_with_links(action, pecan.request.host_url, expand)
|
||||
|
||||
@classmethod
|
||||
def sample(cls, expand=True):
|
||||
sample = cls(uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c',
|
||||
description='action description',
|
||||
state='PENDING',
|
||||
alarm=None,
|
||||
created_at=datetime.datetime.utcnow(),
|
||||
deleted_at=None,
|
||||
updated_at=datetime.datetime.utcnow())
|
||||
sample._action_plan_uuid = '7ae81bb3-dec3-4289-8d6c-da80bd8001ae'
|
||||
sample._next_uuid = '7ae81bb3-dec3-4289-8d6c-da80bd8001ae'
|
||||
return cls._convert_with_links(sample, 'http://localhost:9322', expand)
|
||||
|
||||
|
||||
class ActionCollection(collection.Collection):
|
||||
"""API representation of a collection of actions."""
|
||||
|
||||
actions = [Action]
|
||||
"""A list containing actions objects"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._type = 'actions'
|
||||
|
||||
@staticmethod
|
||||
def convert_with_links(actions, limit, url=None, expand=False,
|
||||
**kwargs):
|
||||
|
||||
collection = ActionCollection()
|
||||
collection.actions = [Action.convert_with_links(p, expand)
|
||||
for p in actions]
|
||||
|
||||
if 'sort_key' in kwargs:
|
||||
reverse = False
|
||||
if kwargs['sort_key'] == 'next_uuid':
|
||||
if 'sort_dir' in kwargs:
|
||||
reverse = True if kwargs['sort_dir'] == 'desc' else False
|
||||
collection.actions = sorted(
|
||||
collection.actions,
|
||||
key=lambda action: action.next_uuid,
|
||||
reverse=reverse)
|
||||
|
||||
collection.next = collection.get_next(limit, url=url, **kwargs)
|
||||
return collection
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
sample = cls()
|
||||
sample.actions = [Action.sample(expand=False)]
|
||||
return sample
|
||||
|
||||
|
||||
class ActionsController(rest.RestController):
|
||||
"""REST controller for Actions."""
|
||||
def __init__(self):
|
||||
super(ActionsController, self).__init__()
|
||||
|
||||
from_actions = False
|
||||
"""A flag to indicate if the requests to this controller are coming
|
||||
from the top-level resource Actions."""
|
||||
|
||||
_custom_actions = {
|
||||
'detail': ['GET'],
|
||||
}
|
||||
|
||||
def _get_actions_collection(self, marker, limit,
|
||||
sort_key, sort_dir, expand=False,
|
||||
resource_url=None,
|
||||
action_plan_uuid=None, audit_uuid=None):
|
||||
|
||||
limit = api_utils.validate_limit(limit)
|
||||
sort_dir = api_utils.validate_sort_dir(sort_dir)
|
||||
|
||||
marker_obj = None
|
||||
if marker:
|
||||
marker_obj = objects.Action.get_by_uuid(pecan.request.context,
|
||||
marker)
|
||||
|
||||
filters = {}
|
||||
if action_plan_uuid:
|
||||
filters['action_plan_uuid'] = action_plan_uuid
|
||||
|
||||
if audit_uuid:
|
||||
filters['audit_uuid'] = audit_uuid
|
||||
|
||||
if sort_key == 'next_uuid':
|
||||
sort_db_key = None
|
||||
else:
|
||||
sort_db_key = sort_key
|
||||
|
||||
actions = objects.Action.list(pecan.request.context,
|
||||
limit,
|
||||
marker_obj, sort_key=sort_db_key,
|
||||
sort_dir=sort_dir,
|
||||
filters=filters)
|
||||
|
||||
return ActionCollection.convert_with_links(actions, limit,
|
||||
url=resource_url,
|
||||
expand=expand,
|
||||
sort_key=sort_key,
|
||||
sort_dir=sort_dir)
|
||||
|
||||
@wsme_pecan.wsexpose(ActionCollection, types.uuid, types.uuid,
|
||||
int, wtypes.text, wtypes.text, types.uuid,
|
||||
types.uuid)
|
||||
def get_all(self, action_uuid=None, marker=None, limit=None,
|
||||
sort_key='id', sort_dir='asc', action_plan_uuid=None,
|
||||
audit_uuid=None):
|
||||
"""Retrieve a list of actions.
|
||||
|
||||
:param marker: pagination marker for large data sets.
|
||||
:param limit: maximum number of resources to return in a single result.
|
||||
:param sort_key: column to sort results by. Default: id.
|
||||
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
|
||||
:param action_plan_uuid: Optional UUID of an action plan,
|
||||
to get only actions for that action plan.
|
||||
:param audit_uuid: Optional UUID of an audit,
|
||||
to get only actions for that audit.
|
||||
"""
|
||||
if action_plan_uuid and audit_uuid:
|
||||
raise exception.ActionFilterCombinationProhibited
|
||||
|
||||
return self._get_actions_collection(
|
||||
marker, limit, sort_key, sort_dir,
|
||||
action_plan_uuid=action_plan_uuid, audit_uuid=audit_uuid)
|
||||
|
||||
@wsme_pecan.wsexpose(ActionCollection, types.uuid,
|
||||
types.uuid, int, wtypes.text, wtypes.text,
|
||||
types.uuid, types.uuid)
|
||||
def detail(self, action_uuid=None, marker=None, limit=None,
|
||||
sort_key='id', sort_dir='asc', action_plan_uuid=None,
|
||||
audit_uuid=None):
|
||||
"""Retrieve a list of actions with detail.
|
||||
|
||||
:param action_uuid: UUID of a action, to get only actions for that
|
||||
action.
|
||||
:param marker: pagination marker for large data sets.
|
||||
:param limit: maximum number of resources to return in a single result.
|
||||
:param sort_key: column to sort results by. Default: id.
|
||||
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
|
||||
:param action_plan_uuid: Optional UUID of an action plan,
|
||||
to get only actions for that action plan.
|
||||
:param audit_uuid: Optional UUID of an audit,
|
||||
to get only actions for that audit.
|
||||
"""
|
||||
# NOTE(lucasagomes): /detail should only work agaist collections
|
||||
parent = pecan.request.path.split('/')[:-1][-1]
|
||||
if parent != "actions":
|
||||
raise exception.HTTPNotFound
|
||||
|
||||
if action_plan_uuid and audit_uuid:
|
||||
raise exception.ActionFilterCombinationProhibited
|
||||
|
||||
expand = True
|
||||
resource_url = '/'.join(['actions', 'detail'])
|
||||
return self._get_actions_collection(
|
||||
marker, limit, sort_key, sort_dir, expand, resource_url,
|
||||
action_plan_uuid=action_plan_uuid, audit_uuid=audit_uuid)
|
||||
|
||||
@wsme_pecan.wsexpose(Action, types.uuid)
|
||||
def get_one(self, action_uuid):
|
||||
"""Retrieve information about the given action.
|
||||
|
||||
:param action_uuid: UUID of a action.
|
||||
"""
|
||||
if self.from_actions:
|
||||
raise exception.OperationNotPermitted
|
||||
|
||||
action = objects.Action.get_by_uuid(pecan.request.context,
|
||||
action_uuid)
|
||||
return Action.convert_with_links(action)
|
||||
|
||||
@wsme_pecan.wsexpose(Action, body=Action, status_code=201)
|
||||
def post(self, action):
|
||||
"""Create a new action.
|
||||
|
||||
:param action: a action within the request body.
|
||||
"""
|
||||
if self.from_actions:
|
||||
raise exception.OperationNotPermitted
|
||||
|
||||
action_dict = action.as_dict()
|
||||
context = pecan.request.context
|
||||
new_action = objects.Action(context, **action_dict)
|
||||
new_action.create(context)
|
||||
|
||||
# Set the HTTP Location Header
|
||||
pecan.response.location = link.build_url('actions', new_action.uuid)
|
||||
return Action.convert_with_links(new_action)
|
||||
|
||||
@wsme.validate(types.uuid, [ActionPatchType])
|
||||
@wsme_pecan.wsexpose(Action, types.uuid, body=[ActionPatchType])
|
||||
def patch(self, action_uuid, patch):
|
||||
"""Update an existing action.
|
||||
|
||||
:param action_uuid: UUID of a action.
|
||||
:param patch: a json PATCH document to apply to this action.
|
||||
"""
|
||||
if self.from_actions:
|
||||
raise exception.OperationNotPermitted
|
||||
|
||||
action_to_update = objects.Action.get_by_uuid(pecan.request.context,
|
||||
action_uuid)
|
||||
try:
|
||||
action_dict = action_to_update.as_dict()
|
||||
action = Action(**api_utils.apply_jsonpatch(action_dict, patch))
|
||||
except api_utils.JSONPATCH_EXCEPTIONS as e:
|
||||
raise exception.PatchError(patch=patch, reason=e)
|
||||
|
||||
# Update only the fields that have changed
|
||||
for field in objects.Action.fields:
|
||||
try:
|
||||
patch_val = getattr(action, field)
|
||||
except AttributeError:
|
||||
# Ignore fields that aren't exposed in the API
|
||||
continue
|
||||
if patch_val == wtypes.Unset:
|
||||
patch_val = None
|
||||
if action_to_update[field] != patch_val:
|
||||
action_to_update[field] = patch_val
|
||||
|
||||
action_to_update.save()
|
||||
return Action.convert_with_links(action_to_update)
|
||||
|
||||
@wsme_pecan.wsexpose(None, types.uuid, status_code=204)
|
||||
def delete(self, action_uuid):
|
||||
"""Delete a action.
|
||||
|
||||
:param action_uuid: UUID of a action.
|
||||
"""
|
||||
|
||||
action_to_delete = objects.Action.get_by_uuid(
|
||||
pecan.request.context,
|
||||
action_uuid)
|
||||
action_to_delete.soft_delete()
|
||||
350
watcher/api/controllers/v1/action_plan.py
Normal file
350
watcher/api/controllers/v1/action_plan.py
Normal file
@@ -0,0 +1,350 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright 2013 Red Hat, Inc.
|
||||
# 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.
|
||||
|
||||
import datetime
|
||||
|
||||
import pecan
|
||||
from pecan import rest
|
||||
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 collection
|
||||
from watcher.api.controllers.v1 import types
|
||||
from watcher.api.controllers.v1 import utils as api_utils
|
||||
from watcher.applier.framework.rpcapi import ApplierAPI
|
||||
from watcher.common import exception
|
||||
from watcher import objects
|
||||
|
||||
|
||||
class ActionPlanPatchType(types.JsonPatchType):
|
||||
|
||||
@staticmethod
|
||||
def mandatory_attrs():
|
||||
return []
|
||||
|
||||
|
||||
class ActionPlan(base.APIBase):
|
||||
"""API representation of a action plan.
|
||||
|
||||
This class enforces type checking and value constraints, and converts
|
||||
between the internal object model and the API representation of an
|
||||
action plan.
|
||||
"""
|
||||
|
||||
_audit_uuid = None
|
||||
_first_action_uuid = None
|
||||
|
||||
def _get_audit_uuid(self):
|
||||
return self._audit_uuid
|
||||
|
||||
def _set_audit_uuid(self, value):
|
||||
if value == wtypes.Unset:
|
||||
self._audit_uuid = wtypes.Unset
|
||||
elif value and self._audit_uuid != value:
|
||||
try:
|
||||
audit = objects.Audit.get(pecan.request.context, value)
|
||||
self._audit_uuid = audit.uuid
|
||||
self.audit_id = audit.id
|
||||
except exception.AuditNotFound:
|
||||
self._audit_uuid = None
|
||||
|
||||
def _get_first_action_uuid(self):
|
||||
return self._first_action_uuid
|
||||
|
||||
def _set_first_action_uuid(self, value):
|
||||
if value == wtypes.Unset:
|
||||
self._first_action_uuid = wtypes.Unset
|
||||
elif value and self._first_action_uuid != value:
|
||||
try:
|
||||
first_action = objects.Action.get(pecan.request.context,
|
||||
value)
|
||||
self._first_action_uuid = first_action.uuid
|
||||
self.first_action_id = first_action.id
|
||||
except exception.ActionNotFound:
|
||||
self._first_action_uuid = None
|
||||
|
||||
uuid = types.uuid
|
||||
"""Unique UUID for this action plan"""
|
||||
|
||||
first_action_uuid = wsme.wsproperty(
|
||||
types.uuid, _get_first_action_uuid, _set_first_action_uuid,
|
||||
mandatory=True)
|
||||
"""The UUID of the first action this action plans links to"""
|
||||
|
||||
audit_uuid = wsme.wsproperty(types.uuid, _get_audit_uuid, _set_audit_uuid,
|
||||
mandatory=True)
|
||||
"""The UUID of the audit this port belongs to"""
|
||||
|
||||
state = wtypes.text
|
||||
"""This action plan state"""
|
||||
|
||||
links = wsme.wsattr([link.Link], readonly=True)
|
||||
"""A list containing a self link and associated action links"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(ActionPlan, self).__init__()
|
||||
|
||||
self.fields = []
|
||||
fields = list(objects.ActionPlan.fields)
|
||||
fields.append('audit_uuid')
|
||||
for field in fields:
|
||||
# Skip fields we do not expose.
|
||||
if not hasattr(self, field):
|
||||
continue
|
||||
self.fields.append(field)
|
||||
setattr(self, field, kwargs.get(field, wtypes.Unset))
|
||||
|
||||
self.fields.append('audit_id')
|
||||
setattr(self, 'audit_uuid', kwargs.get('audit_id', wtypes.Unset))
|
||||
|
||||
@staticmethod
|
||||
def _convert_with_links(action_plan, url, expand=True):
|
||||
if not expand:
|
||||
action_plan.unset_fields_except(['uuid', 'state', 'updated_at',
|
||||
'audit_uuid'])
|
||||
|
||||
action_plan.links = [link.Link.make_link(
|
||||
'self', url,
|
||||
'action_plans', action_plan.uuid),
|
||||
link.Link.make_link(
|
||||
'bookmark', url,
|
||||
'action_plans', action_plan.uuid,
|
||||
bookmark=True)]
|
||||
return action_plan
|
||||
|
||||
@classmethod
|
||||
def convert_with_links(cls, rpc_action_plan, expand=True):
|
||||
action_plan = ActionPlan(**rpc_action_plan.as_dict())
|
||||
return cls._convert_with_links(action_plan, pecan.request.host_url,
|
||||
expand)
|
||||
|
||||
@classmethod
|
||||
def sample(cls, expand=True):
|
||||
sample = cls(uuid='9ef4d84c-41e8-4418-9220-ce55be0436af',
|
||||
state='ONGOING',
|
||||
created_at=datetime.datetime.utcnow(),
|
||||
deleted_at=None,
|
||||
updated_at=datetime.datetime.utcnow())
|
||||
sample._first_action_uuid = '57eaf9ab-5aaa-4f7e-bdf7-9a140ac7a720'
|
||||
sample._audit_uuid = 'abcee106-14d3-4515-b744-5a26885cf6f6'
|
||||
return cls._convert_with_links(sample, 'http://localhost:9322', expand)
|
||||
|
||||
|
||||
class ActionPlanCollection(collection.Collection):
|
||||
"""API representation of a collection of action_plans."""
|
||||
|
||||
action_plans = [ActionPlan]
|
||||
"""A list containing action_plans objects"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._type = 'action_plans'
|
||||
|
||||
@staticmethod
|
||||
def convert_with_links(rpc_action_plans, limit, url=None, expand=False,
|
||||
**kwargs):
|
||||
collection = ActionPlanCollection()
|
||||
collection.action_plans = [ActionPlan.convert_with_links(
|
||||
p, expand) for p in rpc_action_plans]
|
||||
|
||||
if 'sort_key' in kwargs:
|
||||
reverse = False
|
||||
if kwargs['sort_key'] == 'audit_uuid':
|
||||
if 'sort_dir' in kwargs:
|
||||
reverse = True if kwargs['sort_dir'] == 'desc' else False
|
||||
collection.action_plans = sorted(
|
||||
collection.action_plans,
|
||||
key=lambda action_plan: action_plan.audit_uuid,
|
||||
reverse=reverse)
|
||||
|
||||
collection.next = collection.get_next(limit, url=url, **kwargs)
|
||||
return collection
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
sample = cls()
|
||||
sample.action_plans = [ActionPlan.sample(expand=False)]
|
||||
return sample
|
||||
|
||||
|
||||
class ActionPlansController(rest.RestController):
|
||||
"""REST controller for Actions."""
|
||||
def __init__(self):
|
||||
super(ActionPlansController, self).__init__()
|
||||
|
||||
from_actionsPlans = False
|
||||
"""A flag to indicate if the requests to this controller are coming
|
||||
from the top-level resource ActionPlan."""
|
||||
|
||||
_custom_actions = {
|
||||
'detail': ['GET'],
|
||||
}
|
||||
|
||||
def _get_action_plans_collection(self, marker, limit,
|
||||
sort_key, sort_dir, expand=False,
|
||||
resource_url=None, audit_uuid=None):
|
||||
|
||||
limit = api_utils.validate_limit(limit)
|
||||
sort_dir = api_utils.validate_sort_dir(sort_dir)
|
||||
|
||||
marker_obj = None
|
||||
if marker:
|
||||
marker_obj = objects.ActionPlan.get_by_uuid(
|
||||
pecan.request.context, marker)
|
||||
|
||||
filters = {}
|
||||
if audit_uuid:
|
||||
filters['audit_uuid'] = audit_uuid
|
||||
|
||||
if sort_key == 'audit_uuid':
|
||||
sort_db_key = None
|
||||
else:
|
||||
sort_db_key = sort_key
|
||||
|
||||
action_plans = objects.ActionPlan.list(
|
||||
pecan.request.context,
|
||||
limit,
|
||||
marker_obj, sort_key=sort_db_key,
|
||||
sort_dir=sort_dir, filters=filters)
|
||||
|
||||
return ActionPlanCollection.convert_with_links(
|
||||
action_plans, limit,
|
||||
url=resource_url,
|
||||
expand=expand,
|
||||
sort_key=sort_key,
|
||||
sort_dir=sort_dir)
|
||||
|
||||
@wsme_pecan.wsexpose(ActionPlanCollection, types.uuid, types.uuid,
|
||||
int, wtypes.text, wtypes.text, types.uuid)
|
||||
def get_all(self, action_plan_uuid=None, marker=None, limit=None,
|
||||
sort_key='id', sort_dir='asc', audit_uuid=None):
|
||||
"""Retrieve a list of action plans.
|
||||
|
||||
:param marker: pagination marker for large data sets.
|
||||
:param limit: maximum number of resources to return in a single result.
|
||||
:param sort_key: column to sort results by. Default: id.
|
||||
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
|
||||
:param audit_uuid: Optional UUID of an audit, to get only actions
|
||||
for that audit.
|
||||
"""
|
||||
return self._get_action_plans_collection(
|
||||
marker, limit, sort_key, sort_dir, audit_uuid=audit_uuid)
|
||||
|
||||
@wsme_pecan.wsexpose(ActionPlanCollection, types.uuid, types.uuid,
|
||||
int, wtypes.text, wtypes.text, types.uuid)
|
||||
def detail(self, action_plan_uuid=None, marker=None, limit=None,
|
||||
sort_key='id', sort_dir='asc', audit_uuid=None):
|
||||
"""Retrieve a list of action_plans with detail.
|
||||
|
||||
:param action_plan_uuid: UUID of a action plan, to get only
|
||||
:action_plans for that action.
|
||||
:param marker: pagination marker for large data sets.
|
||||
:param limit: maximum number of resources to return in a single result.
|
||||
:param sort_key: column to sort results by. Default: id.
|
||||
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
|
||||
:param audit_uuid: Optional UUID of an audit, to get only actions
|
||||
for that audit.
|
||||
"""
|
||||
# NOTE(lucasagomes): /detail should only work agaist collections
|
||||
parent = pecan.request.path.split('/')[:-1][-1]
|
||||
if parent != "action_plans":
|
||||
raise exception.HTTPNotFound
|
||||
|
||||
expand = True
|
||||
resource_url = '/'.join(['action_plans', 'detail'])
|
||||
return self._get_action_plans_collection(
|
||||
marker, limit,
|
||||
sort_key, sort_dir, expand,
|
||||
resource_url, audit_uuid=audit_uuid)
|
||||
|
||||
@wsme_pecan.wsexpose(ActionPlan, types.uuid)
|
||||
def get_one(self, action_plan_uuid):
|
||||
"""Retrieve information about the given action plan.
|
||||
|
||||
:param action_plan_uuid: UUID of a action plan.
|
||||
"""
|
||||
if self.from_actionsPlans:
|
||||
raise exception.OperationNotPermitted
|
||||
|
||||
action_plan = objects.ActionPlan.get_by_uuid(
|
||||
pecan.request.context, action_plan_uuid)
|
||||
return ActionPlan.convert_with_links(action_plan)
|
||||
|
||||
@wsme_pecan.wsexpose(None, types.uuid, status_code=204)
|
||||
def delete(self, action_plan_uuid):
|
||||
"""Delete an action plan.
|
||||
|
||||
:param action_plan_uuid: UUID of a action.
|
||||
"""
|
||||
|
||||
action_plan_to_delete = objects.ActionPlan.get_by_uuid(
|
||||
pecan.request.context,
|
||||
action_plan_uuid)
|
||||
action_plan_to_delete.soft_delete()
|
||||
|
||||
@wsme.validate(types.uuid, [ActionPlanPatchType])
|
||||
@wsme_pecan.wsexpose(ActionPlan, types.uuid,
|
||||
body=[ActionPlanPatchType])
|
||||
def patch(self, action_plan_uuid, patch):
|
||||
"""Update an existing audit template.
|
||||
|
||||
:param audit template_uuid: UUID of a audit template.
|
||||
:param patch: a json PATCH document to apply to this audit template.
|
||||
"""
|
||||
launch_action_plan = True
|
||||
if self.from_actionsPlans:
|
||||
raise exception.OperationNotPermitted
|
||||
|
||||
action_plan_to_update = objects.ActionPlan.get_by_uuid(
|
||||
pecan.request.context,
|
||||
action_plan_uuid)
|
||||
try:
|
||||
action_plan_dict = action_plan_to_update.as_dict()
|
||||
action_plan = ActionPlan(**api_utils.apply_jsonpatch(
|
||||
action_plan_dict, patch))
|
||||
except api_utils.JSONPATCH_EXCEPTIONS as e:
|
||||
raise exception.PatchError(patch=patch, reason=e)
|
||||
|
||||
launch_action_plan = False
|
||||
# Update only the fields that have changed
|
||||
for field in objects.ActionPlan.fields:
|
||||
try:
|
||||
patch_val = getattr(action_plan, field)
|
||||
except AttributeError:
|
||||
# Ignore fields that aren't exposed in the API
|
||||
continue
|
||||
if patch_val == wtypes.Unset:
|
||||
patch_val = None
|
||||
if action_plan_to_update[field] != patch_val:
|
||||
action_plan_to_update[field] = patch_val
|
||||
|
||||
if field == 'state' and patch_val == 'STARTING':
|
||||
launch_action_plan = True
|
||||
|
||||
action_plan_to_update.save()
|
||||
|
||||
if launch_action_plan:
|
||||
applier_client = ApplierAPI()
|
||||
applier_client.launch_action_plan(pecan.request.context,
|
||||
action_plan.uuid)
|
||||
|
||||
action_plan_to_update = objects.ActionPlan.get_by_uuid(
|
||||
pecan.request.context,
|
||||
action_plan_uuid)
|
||||
return ActionPlan.convert_with_links(action_plan_to_update)
|
||||
351
watcher/api/controllers/v1/audit.py
Normal file
351
watcher/api/controllers/v1/audit.py
Normal file
@@ -0,0 +1,351 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright 2013 Red Hat, Inc.
|
||||
# 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.
|
||||
|
||||
import datetime
|
||||
|
||||
import pecan
|
||||
from pecan import rest
|
||||
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 collection
|
||||
from watcher.api.controllers.v1 import types
|
||||
from watcher.api.controllers.v1 import utils as api_utils
|
||||
from watcher.common import exception
|
||||
from watcher.common import utils
|
||||
from watcher.decision_engine.framework.rpcapi import DecisionEngineAPI
|
||||
from watcher import objects
|
||||
|
||||
|
||||
class AuditPatchType(types.JsonPatchType):
|
||||
|
||||
@staticmethod
|
||||
def mandatory_attrs():
|
||||
return ['/audit_template_uuid']
|
||||
|
||||
|
||||
class Audit(base.APIBase):
|
||||
"""API representation of a audit.
|
||||
|
||||
This class enforces type checking and value constraints, and converts
|
||||
between the internal object model and the API representation of a audit.
|
||||
"""
|
||||
_audit_template_uuid = None
|
||||
|
||||
def _get_audit_template_uuid(self):
|
||||
return self._audit_template_uuid
|
||||
|
||||
def _set_audit_template_uuid(self, value):
|
||||
if value == wtypes.Unset:
|
||||
self._audit_template_uuid = wtypes.Unset
|
||||
elif value and self._audit_template_uuid != value:
|
||||
try:
|
||||
if utils.is_uuid_like(value) or utils.is_int_like(value):
|
||||
audit_template = objects.AuditTemplate.get(
|
||||
pecan.request.context, value)
|
||||
else:
|
||||
audit_template = objects.AuditTemplate.get_by_name(
|
||||
pecan.request.context, value)
|
||||
self._audit_template_uuid = audit_template.uuid
|
||||
self.audit_template_id = audit_template.id
|
||||
except exception.AuditTemplateNotFound:
|
||||
self._audit_template_uuid = None
|
||||
|
||||
uuid = types.uuid
|
||||
"""Unique UUID for this audit"""
|
||||
|
||||
type = wtypes.text
|
||||
"""Type of this audit"""
|
||||
|
||||
deadline = datetime.datetime
|
||||
"""deadline of the audit"""
|
||||
|
||||
state = wtypes.text
|
||||
"""This audit state"""
|
||||
|
||||
audit_template_uuid = wsme.wsproperty(wtypes.text,
|
||||
_get_audit_template_uuid,
|
||||
_set_audit_template_uuid,
|
||||
mandatory=True)
|
||||
"""The UUID of the node this port belongs to"""
|
||||
|
||||
links = wsme.wsattr([link.Link], readonly=True)
|
||||
"""A list containing a self link and associated audit links"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.fields = []
|
||||
fields = list(objects.Audit.fields)
|
||||
# audit_template_uuid is not part of objects.Audit.fields
|
||||
# because it's an API-only attribute.
|
||||
fields.append('audit_template_uuid')
|
||||
for k in fields:
|
||||
# Skip fields we do not expose.
|
||||
if not hasattr(self, k):
|
||||
continue
|
||||
self.fields.append(k)
|
||||
setattr(self, k, kwargs.get(k, wtypes.Unset))
|
||||
|
||||
self.fields.append('audit_template_id')
|
||||
setattr(self, 'audit_template_uuid', kwargs.get('audit_template_id',
|
||||
wtypes.Unset))
|
||||
|
||||
@staticmethod
|
||||
def _convert_with_links(audit, url, expand=True):
|
||||
if not expand:
|
||||
audit.unset_fields_except(['uuid', 'type', 'deadline',
|
||||
'state', 'audit_template_uuid'])
|
||||
|
||||
# The numeric ID should not be exposed to
|
||||
# the user, it's internal only.
|
||||
audit.audit_template_id = wtypes.Unset
|
||||
|
||||
audit.links = [link.Link.make_link('self', url,
|
||||
'audits', audit.uuid),
|
||||
link.Link.make_link('bookmark', url,
|
||||
'audits', audit.uuid,
|
||||
bookmark=True)
|
||||
]
|
||||
|
||||
return audit
|
||||
|
||||
@classmethod
|
||||
def convert_with_links(cls, rpc_audit, expand=True):
|
||||
audit = Audit(**rpc_audit.as_dict())
|
||||
return cls._convert_with_links(audit, pecan.request.host_url, expand)
|
||||
|
||||
@classmethod
|
||||
def sample(cls, expand=True):
|
||||
sample = cls(uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c',
|
||||
type='ONESHOT',
|
||||
state='PENDING',
|
||||
deadline=None,
|
||||
created_at=datetime.datetime.utcnow(),
|
||||
deleted_at=None,
|
||||
updated_at=datetime.datetime.utcnow())
|
||||
sample._audit_template_uuid = '7ae81bb3-dec3-4289-8d6c-da80bd8001ae'
|
||||
return cls._convert_with_links(sample, 'http://localhost:9322', expand)
|
||||
|
||||
|
||||
class AuditCollection(collection.Collection):
|
||||
"""API representation of a collection of audits."""
|
||||
|
||||
audits = [Audit]
|
||||
"""A list containing audits objects"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._type = 'audits'
|
||||
|
||||
@staticmethod
|
||||
def convert_with_links(rpc_audits, limit, url=None, expand=False,
|
||||
**kwargs):
|
||||
collection = AuditCollection()
|
||||
collection.audits = [Audit.convert_with_links(p, expand)
|
||||
for p in rpc_audits]
|
||||
|
||||
if 'sort_key' in kwargs:
|
||||
reverse = False
|
||||
if kwargs['sort_key'] == 'audit_template_uuid':
|
||||
if 'sort_dir' in kwargs:
|
||||
reverse = True if kwargs['sort_dir'] == 'desc' else False
|
||||
collection.audits = sorted(
|
||||
collection.audits,
|
||||
key=lambda audit: audit.audit_template_uuid,
|
||||
reverse=reverse)
|
||||
|
||||
collection.next = collection.get_next(limit, url=url, **kwargs)
|
||||
return collection
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
sample = cls()
|
||||
sample.audits = [Audit.sample(expand=False)]
|
||||
return sample
|
||||
|
||||
|
||||
class AuditsController(rest.RestController):
|
||||
"""REST controller for Audits."""
|
||||
def __init__(self):
|
||||
super(AuditsController, self).__init__()
|
||||
|
||||
from_audits = False
|
||||
"""A flag to indicate if the requests to this controller are coming
|
||||
from the top-level resource Audits."""
|
||||
|
||||
_custom_actions = {
|
||||
'detail': ['GET'],
|
||||
}
|
||||
|
||||
def _get_audits_collection(self, marker, limit,
|
||||
sort_key, sort_dir, expand=False,
|
||||
resource_url=None, audit_template=None):
|
||||
limit = api_utils.validate_limit(limit)
|
||||
sort_dir = api_utils.validate_sort_dir(sort_dir)
|
||||
|
||||
marker_obj = None
|
||||
if marker:
|
||||
marker_obj = objects.Audit.get_by_uuid(pecan.request.context,
|
||||
marker)
|
||||
|
||||
filters = {}
|
||||
if audit_template:
|
||||
if utils.is_uuid_like(audit_template):
|
||||
filters['audit_template_uuid'] = audit_template
|
||||
else:
|
||||
filters['audit_template_name'] = audit_template
|
||||
|
||||
if sort_key == 'audit_template_uuid':
|
||||
sort_db_key = None
|
||||
else:
|
||||
sort_db_key = sort_key
|
||||
|
||||
audits = objects.Audit.list(pecan.request.context,
|
||||
limit,
|
||||
marker_obj, sort_key=sort_db_key,
|
||||
sort_dir=sort_dir, filters=filters)
|
||||
|
||||
return AuditCollection.convert_with_links(audits, limit,
|
||||
url=resource_url,
|
||||
expand=expand,
|
||||
sort_key=sort_key,
|
||||
sort_dir=sort_dir)
|
||||
|
||||
@wsme_pecan.wsexpose(AuditCollection, types.uuid,
|
||||
types.uuid, int, wtypes.text,
|
||||
wtypes.text, wtypes.text)
|
||||
def get_all(self, audit_uuid=None, marker=None, limit=None,
|
||||
sort_key='id', sort_dir='asc', audit_template=None):
|
||||
"""Retrieve a list of audits.
|
||||
|
||||
:param marker: pagination marker for large data sets.
|
||||
:param limit: maximum number of resources to return in a single result.
|
||||
:param sort_key: column to sort results by. Default: id.
|
||||
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
|
||||
:param audit_template: Optional UUID or description of an audit
|
||||
template, to get only audits for that audit template.
|
||||
"""
|
||||
return self._get_audits_collection(marker, limit, sort_key,
|
||||
sort_dir,
|
||||
audit_template=audit_template)
|
||||
|
||||
@wsme_pecan.wsexpose(AuditCollection, types.uuid,
|
||||
types.uuid, int, wtypes.text, wtypes.text)
|
||||
def detail(self, audit_uuid=None, marker=None, limit=None,
|
||||
sort_key='id', sort_dir='asc'):
|
||||
"""Retrieve a list of audits with detail.
|
||||
|
||||
:param audit_uuid: UUID of a audit, to get only audits for that audit.
|
||||
:param marker: pagination marker for large data sets.
|
||||
:param limit: maximum number of resources to return in a single result.
|
||||
:param sort_key: column to sort results by. Default: id.
|
||||
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
|
||||
"""
|
||||
# NOTE(lucasagomes): /detail should only work agaist collections
|
||||
parent = pecan.request.path.split('/')[:-1][-1]
|
||||
if parent != "audits":
|
||||
raise exception.HTTPNotFound
|
||||
|
||||
expand = True
|
||||
resource_url = '/'.join(['audits', 'detail'])
|
||||
return self._get_audits_collection(marker, limit,
|
||||
sort_key, sort_dir, expand,
|
||||
resource_url)
|
||||
|
||||
@wsme_pecan.wsexpose(Audit, types.uuid)
|
||||
def get_one(self, audit_uuid):
|
||||
"""Retrieve information about the given audit.
|
||||
|
||||
:param audit_uuid: UUID of a audit.
|
||||
"""
|
||||
if self.from_audits:
|
||||
raise exception.OperationNotPermitted
|
||||
|
||||
rpc_audit = objects.Audit.get_by_uuid(pecan.request.context,
|
||||
audit_uuid)
|
||||
return Audit.convert_with_links(rpc_audit)
|
||||
|
||||
@wsme_pecan.wsexpose(Audit, body=Audit, status_code=201)
|
||||
def post(self, audit):
|
||||
"""Create a new audit.
|
||||
|
||||
:param audit: a audit within the request body.
|
||||
"""
|
||||
if self.from_audits:
|
||||
raise exception.OperationNotPermitted
|
||||
|
||||
audit_dict = audit.as_dict()
|
||||
context = pecan.request.context
|
||||
new_audit = objects.Audit(context, **audit_dict)
|
||||
new_audit.create(context)
|
||||
|
||||
# Set the HTTP Location Header
|
||||
pecan.response.location = link.build_url('audits', new_audit.uuid)
|
||||
|
||||
# trigger decision-engine to run the audit
|
||||
|
||||
dc_client = DecisionEngineAPI()
|
||||
dc_client.trigger_audit(context, new_audit.uuid)
|
||||
|
||||
return Audit.convert_with_links(new_audit)
|
||||
|
||||
@wsme.validate(types.uuid, [AuditPatchType])
|
||||
@wsme_pecan.wsexpose(Audit, types.uuid, body=[AuditPatchType])
|
||||
def patch(self, audit_uuid, patch):
|
||||
"""Update an existing audit.
|
||||
|
||||
:param audit_uuid: UUID of a audit.
|
||||
:param patch: a json PATCH document to apply to this audit.
|
||||
"""
|
||||
if self.from_audits:
|
||||
raise exception.OperationNotPermitted
|
||||
|
||||
audit_to_update = objects.Audit.get_by_uuid(pecan.request.context,
|
||||
audit_uuid)
|
||||
try:
|
||||
audit_dict = audit_to_update.as_dict()
|
||||
audit = Audit(**api_utils.apply_jsonpatch(audit_dict, patch))
|
||||
except api_utils.JSONPATCH_EXCEPTIONS as e:
|
||||
raise exception.PatchError(patch=patch, reason=e)
|
||||
|
||||
# Update only the fields that have changed
|
||||
for field in objects.Audit.fields:
|
||||
try:
|
||||
patch_val = getattr(audit, field)
|
||||
except AttributeError:
|
||||
# Ignore fields that aren't exposed in the API
|
||||
continue
|
||||
if patch_val == wtypes.Unset:
|
||||
patch_val = None
|
||||
if audit_to_update[field] != patch_val:
|
||||
audit_to_update[field] = patch_val
|
||||
|
||||
audit_to_update.save()
|
||||
return Audit.convert_with_links(audit_to_update)
|
||||
|
||||
@wsme_pecan.wsexpose(None, types.uuid, status_code=204)
|
||||
def delete(self, audit_uuid):
|
||||
"""Delete a audit.
|
||||
|
||||
:param audit_uuid: UUID of a audit.
|
||||
"""
|
||||
|
||||
audit_to_delete = objects.Audit.get_by_uuid(
|
||||
pecan.request.context,
|
||||
audit_uuid)
|
||||
audit_to_delete.soft_delete()
|
||||
327
watcher/api/controllers/v1/audit_template.py
Normal file
327
watcher/api/controllers/v1/audit_template.py
Normal file
@@ -0,0 +1,327 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright 2013 Red Hat, Inc.
|
||||
# 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.
|
||||
|
||||
import datetime
|
||||
|
||||
import pecan
|
||||
from pecan import rest
|
||||
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 collection
|
||||
from watcher.api.controllers.v1 import types
|
||||
from watcher.api.controllers.v1 import utils as api_utils
|
||||
from watcher.common import exception
|
||||
from watcher.common import utils as common_utils
|
||||
from watcher import objects
|
||||
|
||||
|
||||
class AuditTemplatePatchType(types.JsonPatchType):
|
||||
|
||||
@staticmethod
|
||||
def mandatory_attrs():
|
||||
return []
|
||||
|
||||
|
||||
class AuditTemplate(base.APIBase):
|
||||
"""API representation of a audit template.
|
||||
|
||||
This class enforces type checking and value constraints, and converts
|
||||
between the internal object model and the API representation of an
|
||||
audit template.
|
||||
"""
|
||||
uuid = types.uuid
|
||||
"""Unique UUID for this audit template"""
|
||||
|
||||
name = wtypes.text
|
||||
"""Name of this audit template"""
|
||||
|
||||
description = wtypes.text
|
||||
"""Short description of this audit template"""
|
||||
|
||||
deadline = datetime.datetime
|
||||
"""deadline of the audit template"""
|
||||
|
||||
host_aggregate = wtypes.IntegerType(minimum=1)
|
||||
"""ID of the Nova host aggregate targeted by the audit template"""
|
||||
|
||||
extra = {wtypes.text: types.jsontype}
|
||||
"""The metadata of the audit template"""
|
||||
|
||||
goal = wtypes.text
|
||||
"""Goal type of the audit template"""
|
||||
|
||||
version = wtypes.text
|
||||
"""Internal version of the audit template"""
|
||||
|
||||
audits = wsme.wsattr([link.Link], readonly=True)
|
||||
"""Links to the collection of audits contained in this audit template"""
|
||||
|
||||
links = wsme.wsattr([link.Link], readonly=True)
|
||||
"""A list containing a self link and associated audit template links"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(AuditTemplate, self).__init__()
|
||||
|
||||
self.fields = []
|
||||
for field in objects.AuditTemplate.fields:
|
||||
# Skip fields we do not expose.
|
||||
if not hasattr(self, field):
|
||||
continue
|
||||
self.fields.append(field)
|
||||
setattr(self, field, kwargs.get(field, wtypes.Unset))
|
||||
|
||||
@staticmethod
|
||||
def _convert_with_links(audit_template, url, expand=True):
|
||||
if not expand:
|
||||
audit_template.unset_fields_except(['uuid', 'name',
|
||||
'host_aggregate', 'goal'])
|
||||
|
||||
audit_template.links = [link.Link.make_link('self', url,
|
||||
'audit_templates',
|
||||
audit_template.uuid),
|
||||
link.Link.make_link('bookmark', url,
|
||||
'audit_templates',
|
||||
audit_template.uuid,
|
||||
bookmark=True)
|
||||
]
|
||||
return audit_template
|
||||
|
||||
@classmethod
|
||||
def convert_with_links(cls, rpc_audit_template, expand=True):
|
||||
audit_template = AuditTemplate(**rpc_audit_template.as_dict())
|
||||
return cls._convert_with_links(audit_template, pecan.request.host_url,
|
||||
expand)
|
||||
|
||||
@classmethod
|
||||
def sample(cls, expand=True):
|
||||
sample = cls(uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c',
|
||||
name='My Audit Template',
|
||||
description='Description of my audit template',
|
||||
host_aggregate=5,
|
||||
goal='SERVERS_CONSOLIDATION',
|
||||
extra={'automatic': True},
|
||||
created_at=datetime.datetime.utcnow(),
|
||||
deleted_at=None,
|
||||
updated_at=datetime.datetime.utcnow())
|
||||
return cls._convert_with_links(sample, 'http://localhost:9322', expand)
|
||||
|
||||
|
||||
class AuditTemplateCollection(collection.Collection):
|
||||
"""API representation of a collection of audit templates."""
|
||||
|
||||
audit_templates = [AuditTemplate]
|
||||
"""A list containing audit templates objects"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._type = 'audit_templates'
|
||||
|
||||
@staticmethod
|
||||
def convert_with_links(rpc_audit_templates, limit, url=None, expand=False,
|
||||
**kwargs):
|
||||
collection = AuditTemplateCollection()
|
||||
collection.audit_templates = \
|
||||
[AuditTemplate.convert_with_links(p, expand)
|
||||
for p in rpc_audit_templates]
|
||||
collection.next = collection.get_next(limit, url=url, **kwargs)
|
||||
return collection
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
sample = cls()
|
||||
sample.audit_templates = [AuditTemplate.sample(expand=False)]
|
||||
return sample
|
||||
|
||||
|
||||
class AuditTemplatesController(rest.RestController):
|
||||
"""REST controller for AuditTemplates."""
|
||||
def __init__(self):
|
||||
super(AuditTemplatesController, self).__init__()
|
||||
|
||||
from_audit_templates = False
|
||||
"""A flag to indicate if the requests to this controller are coming
|
||||
from the top-level resource AuditTemplates."""
|
||||
|
||||
_custom_actions = {
|
||||
'detail': ['GET'],
|
||||
}
|
||||
|
||||
def _get_audit_templates_collection(self, marker, limit,
|
||||
sort_key, sort_dir, expand=False,
|
||||
resource_url=None):
|
||||
|
||||
limit = api_utils.validate_limit(limit)
|
||||
sort_dir = api_utils.validate_sort_dir(sort_dir)
|
||||
|
||||
marker_obj = None
|
||||
if marker:
|
||||
marker_obj = objects.AuditTemplate.get_by_uuid(
|
||||
pecan.request.context,
|
||||
marker)
|
||||
|
||||
audit_templates = objects.AuditTemplate.list(
|
||||
pecan.request.context,
|
||||
limit,
|
||||
marker_obj, sort_key=sort_key,
|
||||
sort_dir=sort_dir)
|
||||
|
||||
return AuditTemplateCollection.convert_with_links(audit_templates,
|
||||
limit,
|
||||
url=resource_url,
|
||||
expand=expand,
|
||||
sort_key=sort_key,
|
||||
sort_dir=sort_dir)
|
||||
|
||||
@wsme_pecan.wsexpose(AuditTemplateCollection, types.uuid, int,
|
||||
wtypes.text, wtypes.text)
|
||||
def get_all(self, marker=None, limit=None,
|
||||
sort_key='id', sort_dir='asc'):
|
||||
"""Retrieve a list of audit templates.
|
||||
|
||||
:param marker: pagination marker for large data sets.
|
||||
:param limit: maximum number of resources to return in a single result.
|
||||
:param sort_key: column to sort results by. Default: id.
|
||||
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
|
||||
"""
|
||||
return self._get_audit_templates_collection(marker, limit, sort_key,
|
||||
sort_dir)
|
||||
|
||||
@wsme_pecan.wsexpose(AuditTemplateCollection, types.uuid, int,
|
||||
wtypes.text, wtypes.text)
|
||||
def detail(self, marker=None, limit=None,
|
||||
sort_key='id', sort_dir='asc'):
|
||||
"""Retrieve a list of audit templates with detail.
|
||||
|
||||
:param marker: pagination marker for large data sets.
|
||||
:param limit: maximum number of resources to return in a single result.
|
||||
:param sort_key: column to sort results by. Default: id.
|
||||
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
|
||||
"""
|
||||
# NOTE(lucasagomes): /detail should only work agaist collections
|
||||
parent = pecan.request.path.split('/')[:-1][-1]
|
||||
if parent != "audit_templates":
|
||||
raise exception.HTTPNotFound
|
||||
|
||||
expand = True
|
||||
resource_url = '/'.join(['audit_templates', 'detail'])
|
||||
return self._get_audit_templates_collection(marker, limit,
|
||||
sort_key, sort_dir, expand,
|
||||
resource_url)
|
||||
|
||||
@wsme_pecan.wsexpose(AuditTemplate, wtypes.text)
|
||||
def get_one(self, audit_template):
|
||||
"""Retrieve information about the given audit template.
|
||||
|
||||
:param audit template_uuid: UUID or name of an audit template.
|
||||
"""
|
||||
if self.from_audit_templates:
|
||||
raise exception.OperationNotPermitted
|
||||
|
||||
if common_utils.is_uuid_like(audit_template):
|
||||
rpc_audit_template = objects.AuditTemplate.get_by_uuid(
|
||||
pecan.request.context,
|
||||
audit_template)
|
||||
else:
|
||||
rpc_audit_template = objects.AuditTemplate.get_by_name(
|
||||
pecan.request.context,
|
||||
audit_template)
|
||||
|
||||
return AuditTemplate.convert_with_links(rpc_audit_template)
|
||||
|
||||
@wsme_pecan.wsexpose(AuditTemplate, body=AuditTemplate, status_code=201)
|
||||
def post(self, audit_template):
|
||||
"""Create a new audit template.
|
||||
|
||||
:param audit template: a audit template within the request body.
|
||||
"""
|
||||
if self.from_audit_templates:
|
||||
raise exception.OperationNotPermitted
|
||||
|
||||
audit_template_dict = audit_template.as_dict()
|
||||
context = pecan.request.context
|
||||
new_audit_template = objects.AuditTemplate(context,
|
||||
**audit_template_dict)
|
||||
new_audit_template.create(context)
|
||||
|
||||
# Set the HTTP Location Header
|
||||
pecan.response.location = link.build_url('audit_templates',
|
||||
new_audit_template.uuid)
|
||||
return AuditTemplate.convert_with_links(new_audit_template)
|
||||
|
||||
@wsme.validate(types.uuid, [AuditTemplatePatchType])
|
||||
@wsme_pecan.wsexpose(AuditTemplate, wtypes.text,
|
||||
body=[AuditTemplatePatchType])
|
||||
def patch(self, audit_template, patch):
|
||||
"""Update an existing audit template.
|
||||
|
||||
:param audit template_uuid: UUID of a audit template.
|
||||
:param patch: a json PATCH document to apply to this audit template.
|
||||
"""
|
||||
if self.from_audit_templates:
|
||||
raise exception.OperationNotPermitted
|
||||
|
||||
if common_utils.is_uuid_like(audit_template):
|
||||
audit_template_to_update = objects.AuditTemplate.get_by_uuid(
|
||||
pecan.request.context,
|
||||
audit_template)
|
||||
else:
|
||||
audit_template_to_update = objects.AuditTemplate.get_by_name(
|
||||
pecan.request.context,
|
||||
audit_template)
|
||||
|
||||
try:
|
||||
audit_template_dict = audit_template_to_update.as_dict()
|
||||
audit_template = AuditTemplate(**api_utils.apply_jsonpatch(
|
||||
audit_template_dict, patch))
|
||||
except api_utils.JSONPATCH_EXCEPTIONS as e:
|
||||
raise exception.PatchError(patch=patch, reason=e)
|
||||
|
||||
# Update only the fields that have changed
|
||||
for field in objects.AuditTemplate.fields:
|
||||
try:
|
||||
patch_val = getattr(audit_template, field)
|
||||
except AttributeError:
|
||||
# Ignore fields that aren't exposed in the API
|
||||
continue
|
||||
if patch_val == wtypes.Unset:
|
||||
patch_val = None
|
||||
if audit_template_to_update[field] != patch_val:
|
||||
audit_template_to_update[field] = patch_val
|
||||
|
||||
audit_template_to_update.save()
|
||||
return AuditTemplate.convert_with_links(audit_template_to_update)
|
||||
|
||||
@wsme_pecan.wsexpose(None, wtypes.text, status_code=204)
|
||||
def delete(self, audit_template):
|
||||
"""Delete a audit template.
|
||||
|
||||
:param audit template_uuid: UUID or name of an audit template.
|
||||
"""
|
||||
|
||||
if common_utils.is_uuid_like(audit_template):
|
||||
audit_template_to_delete = objects.AuditTemplate.get_by_uuid(
|
||||
pecan.request.context,
|
||||
audit_template)
|
||||
else:
|
||||
audit_template_to_delete = objects.AuditTemplate.get_by_name(
|
||||
pecan.request.context,
|
||||
audit_template)
|
||||
|
||||
audit_template_to_delete.soft_delete()
|
||||
50
watcher/api/controllers/v1/collection.py
Normal file
50
watcher/api/controllers/v1/collection.py
Normal file
@@ -0,0 +1,50 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright 2013 Red Hat, Inc.
|
||||
# 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.
|
||||
|
||||
import pecan
|
||||
from wsme import types as wtypes
|
||||
|
||||
from watcher.api.controllers import base
|
||||
from watcher.api.controllers import link
|
||||
|
||||
|
||||
class Collection(base.APIBase):
|
||||
|
||||
next = wtypes.text
|
||||
"""A link to retrieve the next subset of the collection"""
|
||||
|
||||
@property
|
||||
def collection(self):
|
||||
return getattr(self, self._type)
|
||||
|
||||
def has_next(self, limit):
|
||||
"""Return whether collection has more items."""
|
||||
return len(self.collection) and len(self.collection) == limit
|
||||
|
||||
def get_next(self, limit, url=None, **kwargs):
|
||||
"""Return a link to the next subset of the collection."""
|
||||
if not self.has_next(limit):
|
||||
return wtypes.Unset
|
||||
|
||||
resource_url = url or self._type
|
||||
q_args = ''.join(['%s=%s&' % (key, kwargs[key]) for key in kwargs])
|
||||
next_args = '?%(args)slimit=%(limit)d&marker=%(marker)s' % {
|
||||
'args': q_args, 'limit': limit,
|
||||
'marker': self.collection[-1].uuid}
|
||||
|
||||
return link.Link.make_link('next', pecan.request.host_url,
|
||||
resource_url, next_args).href
|
||||
237
watcher/api/controllers/v1/types.py
Normal file
237
watcher/api/controllers/v1/types.py
Normal file
@@ -0,0 +1,237 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
# Copyright 2013 Red Hat, Inc.
|
||||
# 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.
|
||||
|
||||
import json
|
||||
from oslo_utils import strutils
|
||||
import six
|
||||
import wsme
|
||||
from wsme import types as wtypes
|
||||
|
||||
from watcher.common import exception
|
||||
from watcher.common.i18n import _
|
||||
from watcher.common import utils
|
||||
|
||||
|
||||
class UuidOrNameType(wtypes.UserType):
|
||||
"""A simple UUID or logical name type."""
|
||||
|
||||
basetype = wtypes.text
|
||||
name = 'uuid_or_name'
|
||||
# FIXME(lucasagomes): When used with wsexpose decorator WSME will try
|
||||
# to get the name of the type by accessing it's __name__ attribute.
|
||||
# Remove this __name__ attribute once it's fixed in WSME.
|
||||
# https://bugs.launchpad.net/wsme/+bug/1265590
|
||||
__name__ = name
|
||||
|
||||
@staticmethod
|
||||
def validate(value):
|
||||
if not (utils.is_uuid_like(value) or utils.is_hostname_safe(value)):
|
||||
raise exception.InvalidUuidOrName(name=value)
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def frombasetype(value):
|
||||
if value is None:
|
||||
return None
|
||||
return UuidOrNameType.validate(value)
|
||||
|
||||
|
||||
class NameType(wtypes.UserType):
|
||||
"""A simple logical name type."""
|
||||
|
||||
basetype = wtypes.text
|
||||
name = 'name'
|
||||
# FIXME(lucasagomes): When used with wsexpose decorator WSME will try
|
||||
# to get the name of the type by accessing it's __name__ attribute.
|
||||
# Remove this __name__ attribute once it's fixed in WSME.
|
||||
# https://bugs.launchpad.net/wsme/+bug/1265590
|
||||
__name__ = name
|
||||
|
||||
@staticmethod
|
||||
def validate(value):
|
||||
if not utils.is_hostname_safe(value):
|
||||
raise exception.InvalidName(name=value)
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def frombasetype(value):
|
||||
if value is None:
|
||||
return None
|
||||
return NameType.validate(value)
|
||||
|
||||
|
||||
class UuidType(wtypes.UserType):
|
||||
"""A simple UUID type."""
|
||||
|
||||
basetype = wtypes.text
|
||||
name = 'uuid'
|
||||
# FIXME(lucasagomes): When used with wsexpose decorator WSME will try
|
||||
# to get the name of the type by accessing it's __name__ attribute.
|
||||
# Remove this __name__ attribute once it's fixed in WSME.
|
||||
# https://bugs.launchpad.net/wsme/+bug/1265590
|
||||
__name__ = name
|
||||
|
||||
@staticmethod
|
||||
def validate(value):
|
||||
if not utils.is_uuid_like(value):
|
||||
raise exception.InvalidUUID(uuid=value)
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def frombasetype(value):
|
||||
if value is None:
|
||||
return None
|
||||
return UuidType.validate(value)
|
||||
|
||||
|
||||
class BooleanType(wtypes.UserType):
|
||||
"""A simple boolean type."""
|
||||
|
||||
basetype = wtypes.text
|
||||
name = 'boolean'
|
||||
# FIXME(lucasagomes): When used with wsexpose decorator WSME will try
|
||||
# to get the name of the type by accessing it's __name__ attribute.
|
||||
# Remove this __name__ attribute once it's fixed in WSME.
|
||||
# https://bugs.launchpad.net/wsme/+bug/1265590
|
||||
__name__ = name
|
||||
|
||||
@staticmethod
|
||||
def validate(value):
|
||||
try:
|
||||
return strutils.bool_from_string(value, strict=True)
|
||||
except ValueError as e:
|
||||
# raise Invalid to return 400 (BadRequest) in the API
|
||||
raise exception.Invalid(e)
|
||||
|
||||
@staticmethod
|
||||
def frombasetype(value):
|
||||
if value is None:
|
||||
return None
|
||||
return BooleanType.validate(value)
|
||||
|
||||
|
||||
class JsonType(wtypes.UserType):
|
||||
"""A simple JSON type."""
|
||||
|
||||
basetype = wtypes.text
|
||||
name = 'json'
|
||||
# FIXME(lucasagomes): When used with wsexpose decorator WSME will try
|
||||
# to get the name of the type by accessing it's __name__ attribute.
|
||||
# Remove this __name__ attribute once it's fixed in WSME.
|
||||
# https://bugs.launchpad.net/wsme/+bug/1265590
|
||||
__name__ = name
|
||||
|
||||
def __str__(self):
|
||||
# These are the json serializable native types
|
||||
return ' | '.join(map(str, (wtypes.text, six.integer_types, float,
|
||||
BooleanType, list, dict, None)))
|
||||
|
||||
@staticmethod
|
||||
def validate(value):
|
||||
try:
|
||||
json.dumps(value)
|
||||
except TypeError:
|
||||
raise exception.Invalid(_('%s is not JSON serializable') % value)
|
||||
else:
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def frombasetype(value):
|
||||
return JsonType.validate(value)
|
||||
|
||||
|
||||
uuid = UuidType()
|
||||
boolean = BooleanType()
|
||||
jsontype = JsonType()
|
||||
|
||||
|
||||
class MultiType(wtypes.UserType):
|
||||
"""A complex type that represents one or more types.
|
||||
|
||||
Used for validating that a value is an instance of one of the types.
|
||||
|
||||
:param types: Variable-length list of types.
|
||||
|
||||
"""
|
||||
def __init__(self, *types):
|
||||
self.types = types
|
||||
|
||||
def __str__(self):
|
||||
return ' | '.join(map(str, self.types))
|
||||
|
||||
def validate(self, value):
|
||||
for t in self.types:
|
||||
if t is wsme.types.text and isinstance(value, wsme.types.bytes):
|
||||
value = value.decode()
|
||||
if isinstance(value, t):
|
||||
return value
|
||||
else:
|
||||
raise ValueError(
|
||||
_("Wrong type. Expected '%(type)s', got '%(value)s'")
|
||||
% {'type': self.types, 'value': type(value)})
|
||||
|
||||
|
||||
class JsonPatchType(wtypes.Base):
|
||||
"""A complex type that represents a single json-patch operation."""
|
||||
|
||||
path = wtypes.wsattr(wtypes.StringType(pattern='^(/[\w-]+)+$'),
|
||||
mandatory=True)
|
||||
op = wtypes.wsattr(wtypes.Enum(str, 'add', 'replace', 'remove'),
|
||||
mandatory=True)
|
||||
value = wsme.wsattr(jsontype, default=wtypes.Unset)
|
||||
|
||||
@staticmethod
|
||||
def internal_attrs():
|
||||
"""Returns a list of internal attributes.
|
||||
|
||||
Internal attributes can't be added, replaced or removed. This
|
||||
method may be overwritten by derived class.
|
||||
|
||||
"""
|
||||
return ['/created_at', '/id', '/links', '/updated_at',
|
||||
'/deleted_at', '/uuid']
|
||||
|
||||
@staticmethod
|
||||
def mandatory_attrs():
|
||||
"""Retruns a list of mandatory attributes.
|
||||
|
||||
Mandatory attributes can't be removed from the document. This
|
||||
method should be overwritten by derived class.
|
||||
|
||||
"""
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def validate(patch):
|
||||
_path = '/' + patch.path.split('/')[1]
|
||||
if _path in patch.internal_attrs():
|
||||
msg = _("'%s' is an internal attribute and can not be updated")
|
||||
raise wsme.exc.ClientSideError(msg % patch.path)
|
||||
|
||||
if patch.path in patch.mandatory_attrs() and patch.op == 'remove':
|
||||
msg = _("'%s' is a mandatory attribute and can not be removed")
|
||||
raise wsme.exc.ClientSideError(msg % patch.path)
|
||||
|
||||
if patch.op != 'remove':
|
||||
if patch.value is wsme.Unset:
|
||||
msg = _("'add' and 'replace' operations needs value")
|
||||
raise wsme.exc.ClientSideError(msg)
|
||||
|
||||
ret = {'path': patch.path, 'op': patch.op}
|
||||
if patch.value is not wsme.Unset:
|
||||
ret['value'] = patch.value
|
||||
return ret
|
||||
52
watcher/api/controllers/v1/utils.py
Normal file
52
watcher/api/controllers/v1/utils.py
Normal file
@@ -0,0 +1,52 @@
|
||||
# Copyright 2013 Red Hat, Inc.
|
||||
# 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.
|
||||
|
||||
import jsonpatch
|
||||
from oslo_config import cfg
|
||||
import wsme
|
||||
|
||||
from watcher.common.i18n import _
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
JSONPATCH_EXCEPTIONS = (jsonpatch.JsonPatchException,
|
||||
jsonpatch.JsonPointerException,
|
||||
KeyError)
|
||||
|
||||
|
||||
def validate_limit(limit):
|
||||
if limit is not None and limit <= 0:
|
||||
raise wsme.exc.ClientSideError(_("Limit must be positive"))
|
||||
|
||||
return min(CONF.api.max_limit, limit) or CONF.api.max_limit
|
||||
|
||||
|
||||
def validate_sort_dir(sort_dir):
|
||||
if sort_dir not in ['asc', 'desc']:
|
||||
raise wsme.exc.ClientSideError(_("Invalid sort direction: %s. "
|
||||
"Acceptable values are "
|
||||
"'asc' or 'desc'") % sort_dir)
|
||||
return sort_dir
|
||||
|
||||
|
||||
def apply_jsonpatch(doc, patch):
|
||||
for p in patch:
|
||||
if p['op'] == 'add' and p['path'].count('/') == 1:
|
||||
if p['path'].lstrip('/') not in doc:
|
||||
msg = _('Adding a new attribute (%s) to the root of '
|
||||
' the resource is not allowed')
|
||||
raise wsme.exc.ClientSideError(msg % p['path'])
|
||||
return jsonpatch.apply_patch(doc, jsonpatch.JsonPatch(patch))
|
||||
113
watcher/api/hooks.py
Normal file
113
watcher/api/hooks.py
Normal file
@@ -0,0 +1,113 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Copyright © 2012 New Dream Network, LLC (DreamHost)
|
||||
#
|
||||
# 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 oslo_config import cfg
|
||||
from oslo_utils import importutils
|
||||
from pecan import hooks
|
||||
|
||||
from watcher.common import context
|
||||
|
||||
|
||||
class ContextHook(hooks.PecanHook):
|
||||
"""Configures a request context and attaches it to the request.
|
||||
|
||||
The following HTTP request headers are used:
|
||||
|
||||
X-User:
|
||||
Used for context.user.
|
||||
|
||||
X-User-Id:
|
||||
Used for context.user_id.
|
||||
|
||||
X-Project-Name:
|
||||
Used for context.project.
|
||||
|
||||
X-Project-Id:
|
||||
Used for context.project_id.
|
||||
|
||||
X-Auth-Token:
|
||||
Used for context.auth_token.
|
||||
|
||||
"""
|
||||
|
||||
def before(self, state):
|
||||
headers = state.request.headers
|
||||
user = headers.get('X-User')
|
||||
user_id = headers.get('X-User-Id')
|
||||
project = headers.get('X-Project-Name')
|
||||
project_id = headers.get('X-Project-Id')
|
||||
domain_id = headers.get('X-User-Domain-Id')
|
||||
domain_name = headers.get('X-User-Domain-Name')
|
||||
auth_token = headers.get('X-Storage-Token')
|
||||
auth_token = headers.get('X-Auth-Token', auth_token)
|
||||
show_deleted = headers.get('X-Show-Deleted')
|
||||
auth_token_info = state.request.environ.get('keystone.token_info')
|
||||
|
||||
auth_url = headers.get('X-Auth-Url')
|
||||
if auth_url is None:
|
||||
importutils.import_module('keystonemiddleware.auth_token')
|
||||
auth_url = cfg.CONF.keystone_authtoken.auth_uri
|
||||
|
||||
state.request.context = context.make_context(
|
||||
auth_token=auth_token,
|
||||
auth_url=auth_url,
|
||||
auth_token_info=auth_token_info,
|
||||
user=user,
|
||||
user_id=user_id,
|
||||
project=project,
|
||||
project_id=project_id,
|
||||
domain_id=domain_id,
|
||||
domain_name=domain_name,
|
||||
show_deleted=show_deleted)
|
||||
|
||||
|
||||
class NoExceptionTracebackHook(hooks.PecanHook):
|
||||
"""Workaround rpc.common: deserialize_remote_exception.
|
||||
|
||||
deserialize_remote_exception builds rpc exception traceback into error
|
||||
message which is then sent to the client. Such behavior is a security
|
||||
concern so this hook is aimed to cut-off traceback from the error message.
|
||||
"""
|
||||
# NOTE(max_lobur): 'after' hook used instead of 'on_error' because
|
||||
# 'on_error' never fired for wsme+pecan pair. wsme @wsexpose decorator
|
||||
# catches and handles all the errors, so 'on_error' dedicated for unhandled
|
||||
# exceptions never fired.
|
||||
def after(self, state):
|
||||
# Omit empty body. Some errors may not have body at this level yet.
|
||||
if not state.response.body:
|
||||
return
|
||||
|
||||
# Do nothing if there is no error.
|
||||
if 200 <= state.response.status_int < 400:
|
||||
return
|
||||
|
||||
json_body = state.response.json
|
||||
# Do not remove traceback when server in debug mode (except 'Server'
|
||||
# errors when 'debuginfo' will be used for traces).
|
||||
if cfg.CONF.debug and json_body.get('faultcode') != 'Server':
|
||||
return
|
||||
|
||||
faultstring = json_body.get('faultstring')
|
||||
traceback_marker = 'Traceback (most recent call last):'
|
||||
if faultstring and (traceback_marker in faultstring):
|
||||
# Cut-off traceback.
|
||||
faultstring = faultstring.split(traceback_marker, 1)[0]
|
||||
# Remove trailing newlines and spaces if any.
|
||||
json_body['faultstring'] = faultstring.rstrip()
|
||||
# Replace the whole json. Cannot change original one beacause it's
|
||||
# generated on the fly.
|
||||
state.response.json = json_body
|
||||
25
watcher/api/middleware/__init__.py
Normal file
25
watcher/api/middleware/__init__.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
from watcher.api.middleware import auth_token
|
||||
from watcher.api.middleware import parsable_error
|
||||
|
||||
|
||||
ParsableErrorMiddleware = parsable_error.ParsableErrorMiddleware
|
||||
AuthTokenMiddleware = auth_token.AuthTokenMiddleware
|
||||
|
||||
__all__ = (ParsableErrorMiddleware,
|
||||
AuthTokenMiddleware)
|
||||
61
watcher/api/middleware/auth_token.py
Normal file
61
watcher/api/middleware/auth_token.py
Normal file
@@ -0,0 +1,61 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import re
|
||||
|
||||
from keystonemiddleware import auth_token
|
||||
|
||||
from watcher.common import exception
|
||||
from watcher.common.i18n import _
|
||||
from watcher.common import utils
|
||||
from watcher.openstack.common import log
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class AuthTokenMiddleware(auth_token.AuthProtocol):
|
||||
"""A wrapper on Keystone auth_token middleware.
|
||||
|
||||
Does not perform verification of authentication tokens
|
||||
for public routes in the API.
|
||||
|
||||
"""
|
||||
def __init__(self, app, conf, public_api_routes=[]):
|
||||
route_pattern_tpl = '%s(\.json|\.xml)?$'
|
||||
|
||||
try:
|
||||
self.public_api_routes = [re.compile(route_pattern_tpl % route_tpl)
|
||||
for route_tpl in public_api_routes]
|
||||
except re.error as e:
|
||||
msg = _('Cannot compile public API routes: %s') % e
|
||||
|
||||
LOG.error(msg)
|
||||
raise exception.ConfigInvalid(error_msg=msg)
|
||||
|
||||
super(AuthTokenMiddleware, self).__init__(app, conf)
|
||||
|
||||
def __call__(self, env, start_response):
|
||||
path = utils.safe_rstrip(env.get('PATH_INFO'), '/')
|
||||
|
||||
# The information whether the API call is being performed against the
|
||||
# public API is required for some other components. Saving it to the
|
||||
# WSGI environment is reasonable thereby.
|
||||
env['is_public_api'] = any(map(lambda pattern: re.match(pattern, path),
|
||||
self.public_api_routes))
|
||||
|
||||
if env['is_public_api']:
|
||||
return self._app(env, start_response)
|
||||
|
||||
return super(AuthTokenMiddleware, self).__call__(env, start_response)
|
||||
90
watcher/api/middleware/parsable_error.py
Normal file
90
watcher/api/middleware/parsable_error.py
Normal file
@@ -0,0 +1,90 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Copyright © 2012 New Dream Network, LLC (DreamHost)
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Middleware to replace the plain text message body of an error
|
||||
response with one formatted so the client can parse it.
|
||||
|
||||
Based on pecan.middleware.errordocument
|
||||
"""
|
||||
|
||||
import json
|
||||
from xml import etree as et
|
||||
|
||||
import webob
|
||||
|
||||
from watcher.common.i18n import _
|
||||
from watcher.common.i18n import _LE
|
||||
from watcher.openstack.common import log
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class ParsableErrorMiddleware(object):
|
||||
"""Replace error body with something the client can parse."""
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
# Request for this state, modified by replace_start_response()
|
||||
# and used when an error is being reported.
|
||||
state = {}
|
||||
|
||||
def replacement_start_response(status, headers, exc_info=None):
|
||||
"""Overrides the default response to make errors parsable."""
|
||||
try:
|
||||
status_code = int(status.split(' ')[0])
|
||||
state['status_code'] = status_code
|
||||
except (ValueError, TypeError): # pragma: nocover
|
||||
raise Exception(_(
|
||||
'ErrorDocumentMiddleware received an invalid '
|
||||
'status %s') % status)
|
||||
else:
|
||||
if (state['status_code'] // 100) not in (2, 3):
|
||||
# Remove some headers so we can replace them later
|
||||
# when we have the full error message and can
|
||||
# compute the length.
|
||||
headers = [(h, v)
|
||||
for (h, v) in headers
|
||||
if h not in ('Content-Length', 'Content-Type')
|
||||
]
|
||||
# Save the headers in case we need to modify them.
|
||||
state['headers'] = headers
|
||||
return start_response(status, headers, exc_info)
|
||||
|
||||
app_iter = self.app(environ, replacement_start_response)
|
||||
if (state['status_code'] // 100) not in (2, 3):
|
||||
req = webob.Request(environ)
|
||||
if (req.accept.best_match(['application/json', 'application/xml'])
|
||||
== 'application/xml'):
|
||||
try:
|
||||
# simple check xml is valid
|
||||
body = [et.ElementTree.tostring(
|
||||
et.ElementTree.fromstring('<error_message>'
|
||||
+ '\n'.join(app_iter)
|
||||
+ '</error_message>'))]
|
||||
except et.ElementTree.ParseError as err:
|
||||
LOG.error(_LE('Error parsing HTTP response: %s'), err)
|
||||
body = ['<error_message>%s' % state['status_code']
|
||||
+ '</error_message>']
|
||||
state['headers'].append(('Content-Type', 'application/xml'))
|
||||
else:
|
||||
body = [json.dumps({'error_message': '\n'.join(app_iter)})]
|
||||
state['headers'].append(('Content-Type', 'application/json'))
|
||||
state['headers'].append(('Content-Length', len(body[0])))
|
||||
else:
|
||||
body = app_iter
|
||||
return body
|
||||
Reference in New Issue
Block a user