initial version
Change-Id: I699e0ab082657880998d8618fe29eb7f56c6c661
This commit is contained in:
19
watcher/__init__.py
Normal file
19
watcher/__init__.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# -*- coding: 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 pbr.version
|
||||
|
||||
|
||||
__version__ = pbr.version.VersionInfo(
|
||||
'watcher').version_string()
|
||||
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
|
||||
11
watcher/applier/README.md
Normal file
11
watcher/applier/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Watcher Actions Applier
|
||||
|
||||
This component is in charge of executing the plan of actions built by the Watcher Actions Planner.
|
||||
|
||||
For each action of the workflow, this component may call directly the component responsible for this kind of action (Example : Nova API for an instance migration) or via some publish/subscribe pattern on the message bus.
|
||||
|
||||
It notifies continuously of the current progress of the Action Plan (and atomic Actions), sending status messages on the bus. Those events may be used by the CEP to trigger new actions.
|
||||
|
||||
This component is also connected to the Watcher MySQL database in order to:
|
||||
* get the description of the action plan to execute
|
||||
* persist its current state so that if it is restarted, it can restore each Action plan context and restart from the last known safe point of each ongoing workflow.
|
||||
0
watcher/applier/__init__.py
Normal file
0
watcher/applier/__init__.py
Normal file
0
watcher/applier/api/__init__.py
Normal file
0
watcher/applier/api/__init__.py
Normal file
21
watcher/applier/api/applier.py
Normal file
21
watcher/applier/api/applier.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
||||
class Applier(object):
|
||||
def execute(self, action_plan_uuid):
|
||||
raise NotImplementedError("Should have implemented this")
|
||||
21
watcher/applier/api/command_mapper.py
Normal file
21
watcher/applier/api/command_mapper.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
||||
class CommandMapper(object):
|
||||
def build_primitive_command(self, action):
|
||||
raise NotImplementedError("Should have implemented this")
|
||||
0
watcher/applier/api/messaging/__init__.py
Normal file
0
watcher/applier/api/messaging/__init__.py
Normal file
21
watcher/applier/api/messaging/applier_command.py
Normal file
21
watcher/applier/api/messaging/applier_command.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
||||
class ApplierCommand(object):
|
||||
def execute(self):
|
||||
raise NotImplementedError("Should have implemented this")
|
||||
26
watcher/applier/api/primitive_command.py
Normal file
26
watcher/applier/api/primitive_command.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
#
|
||||
# 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.applier.api.promise import Promise
|
||||
|
||||
|
||||
class PrimitiveCommand(object):
|
||||
@Promise
|
||||
def execute(self):
|
||||
raise NotImplementedError("Should have implemented this")
|
||||
|
||||
@Promise
|
||||
def undo(self):
|
||||
raise NotImplementedError("Should have implemented this")
|
||||
48
watcher/applier/api/promise.py
Normal file
48
watcher/applier/api/promise.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
#
|
||||
# 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 concurrent.futures import Future
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
|
||||
class Promise(object):
|
||||
executor = ThreadPoolExecutor(
|
||||
max_workers=10)
|
||||
|
||||
def __init__(self, func):
|
||||
self.func = func
|
||||
|
||||
def resolve(self, *args, **kwargs):
|
||||
resolved_args = []
|
||||
resolved_kwargs = {}
|
||||
|
||||
for i, arg in enumerate(args):
|
||||
if isinstance(arg, Future):
|
||||
resolved_args.append(arg.result())
|
||||
else:
|
||||
resolved_args.append(arg)
|
||||
|
||||
for kw, arg in kwargs.items():
|
||||
if isinstance(arg, Future):
|
||||
resolved_kwargs[kw] = arg.result()
|
||||
else:
|
||||
resolved_kwargs[kw] = arg
|
||||
|
||||
return self.func(*resolved_args, **resolved_kwargs)
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
return self.executor.submit(self.resolve, *args, **kwargs)
|
||||
0
watcher/applier/framework/__init__.py
Normal file
0
watcher/applier/framework/__init__.py
Normal file
0
watcher/applier/framework/command/__init__.py
Normal file
0
watcher/applier/framework/command/__init__.py
Normal file
@@ -0,0 +1,76 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
#
|
||||
# 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 keystoneclient.auth.identity import v3
|
||||
from keystoneclient import session
|
||||
from oslo_config import cfg
|
||||
|
||||
from watcher.applier.api.primitive_command import PrimitiveCommand
|
||||
from watcher.applier.api.promise import Promise
|
||||
from watcher.applier.framework.command.wrapper.nova_wrapper import NovaWrapper
|
||||
from watcher.decision_engine.framework.model.hypervisor_state import \
|
||||
HypervisorState
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class HypervisorStateCommand(PrimitiveCommand):
|
||||
def __init__(self, host, status):
|
||||
self.host = host
|
||||
self.status = status
|
||||
|
||||
def nova_manage_service(self, status):
|
||||
creds = \
|
||||
{'auth_url': CONF.keystone_authtoken.auth_uri,
|
||||
'username': CONF.keystone_authtoken.admin_user,
|
||||
'password': CONF.keystone_authtoken.admin_password,
|
||||
'project_name': CONF.keystone_authtoken.admin_tenant_name,
|
||||
'user_domain_name': "default",
|
||||
'project_domain_name': "default"}
|
||||
|
||||
auth = v3.Password(auth_url=creds['auth_url'],
|
||||
username=creds['username'],
|
||||
password=creds['password'],
|
||||
project_name=creds['project_name'],
|
||||
user_domain_name=creds[
|
||||
'user_domain_name'],
|
||||
project_domain_name=creds[
|
||||
'project_domain_name'])
|
||||
sess = session.Session(auth=auth)
|
||||
# todo(jed) refactoring
|
||||
wrapper = NovaWrapper(creds, session=sess)
|
||||
if status is True:
|
||||
return wrapper.enable_service_nova_compute(self.host)
|
||||
else:
|
||||
return wrapper.disable_service_nova_compute(self.host)
|
||||
|
||||
@Promise
|
||||
def execute(self):
|
||||
if self.status == HypervisorState.OFFLINE.value:
|
||||
state = False
|
||||
elif self.status == HypervisorState.ONLINE.value:
|
||||
state = True
|
||||
return self.nova_manage_service(state)
|
||||
|
||||
@Promise
|
||||
def undo(self):
|
||||
if self.status == HypervisorState.OFFLINE.value:
|
||||
state = True
|
||||
elif self.status == HypervisorState.ONLINE.value:
|
||||
state = False
|
||||
return self.nova_manage_service(state)
|
||||
100
watcher/applier/framework/command/migrate_command.py
Normal file
100
watcher/applier/framework/command/migrate_command.py
Normal file
@@ -0,0 +1,100 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
#
|
||||
# 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 keystoneclient.auth.identity import v3
|
||||
from keystoneclient import session
|
||||
from oslo_config import cfg
|
||||
|
||||
from watcher.applier.api.primitive_command import PrimitiveCommand
|
||||
from watcher.applier.api.promise import Promise
|
||||
from watcher.applier.framework.command.wrapper.nova_wrapper import NovaWrapper
|
||||
from watcher.decision_engine.framework.default_planner import Primitives
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class MigrateCommand(PrimitiveCommand):
|
||||
def __init__(self, vm_uuid=None,
|
||||
migration_type=None,
|
||||
source_hypervisor=None,
|
||||
destination_hypervisor=None):
|
||||
self.instance_uuid = vm_uuid
|
||||
self.migration_type = migration_type
|
||||
self.source_hypervisor = source_hypervisor
|
||||
self.destination_hypervisor = destination_hypervisor
|
||||
|
||||
def migrate(self, destination):
|
||||
|
||||
creds = \
|
||||
{'auth_url': CONF.keystone_authtoken.auth_uri,
|
||||
'username': CONF.keystone_authtoken.admin_user,
|
||||
'password': CONF.keystone_authtoken.admin_password,
|
||||
'project_name': CONF.keystone_authtoken.admin_tenant_name,
|
||||
'user_domain_name': "default",
|
||||
'project_domain_name': "default"}
|
||||
auth = v3.Password(auth_url=creds['auth_url'],
|
||||
username=creds['username'],
|
||||
password=creds['password'],
|
||||
project_name=creds['project_name'],
|
||||
user_domain_name=creds[
|
||||
'user_domain_name'],
|
||||
project_domain_name=creds[
|
||||
'project_domain_name'])
|
||||
sess = session.Session(auth=auth)
|
||||
# todo(jed) add class
|
||||
wrapper = NovaWrapper(creds, session=sess)
|
||||
instance = wrapper.find_instance(self.instance_uuid)
|
||||
if instance:
|
||||
project_id = getattr(instance, "tenant_id")
|
||||
|
||||
creds2 = \
|
||||
{'auth_url': CONF.keystone_authtoken.auth_uri,
|
||||
'username': CONF.keystone_authtoken.admin_user,
|
||||
'password': CONF.keystone_authtoken.admin_password,
|
||||
'project_id': project_id,
|
||||
'user_domain_name': "default",
|
||||
'project_domain_name': "default"}
|
||||
auth2 = v3.Password(auth_url=creds2['auth_url'],
|
||||
username=creds2['username'],
|
||||
password=creds2['password'],
|
||||
project_id=creds2['project_id'],
|
||||
user_domain_name=creds2[
|
||||
'user_domain_name'],
|
||||
project_domain_name=creds2[
|
||||
'project_domain_name'])
|
||||
sess2 = session.Session(auth=auth2)
|
||||
wrapper2 = NovaWrapper(creds2, session=sess2)
|
||||
|
||||
# todo(jed) remove Primitves
|
||||
if self.migration_type is Primitives.COLD_MIGRATE:
|
||||
return wrapper2.live_migrate_instance(
|
||||
instance_id=self.instance_uuid,
|
||||
dest_hostname=destination,
|
||||
block_migration=True)
|
||||
elif self.migration_type is Primitives.LIVE_MIGRATE:
|
||||
return wrapper2.live_migrate_instance(
|
||||
instance_id=self.instance_uuid,
|
||||
dest_hostname=destination,
|
||||
block_migration=False)
|
||||
|
||||
@Promise
|
||||
def execute(self):
|
||||
return self.migrate(self.destination_hypervisor)
|
||||
|
||||
@Promise
|
||||
def undo(self):
|
||||
return self.migrate(self.source_hypervisor)
|
||||
31
watcher/applier/framework/command/nop_command.py
Normal file
31
watcher/applier/framework/command/nop_command.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
#
|
||||
# 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.applier.api.primitive_command import PrimitiveCommand
|
||||
from watcher.applier.api.promise import Promise
|
||||
|
||||
|
||||
class NopCommand(PrimitiveCommand):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@Promise
|
||||
def execute(self):
|
||||
return True
|
||||
|
||||
@Promise
|
||||
def undo(self):
|
||||
return True
|
||||
33
watcher/applier/framework/command/power_state_command.py
Normal file
33
watcher/applier/framework/command/power_state_command.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
#
|
||||
# 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.applier.api.primitive_command import PrimitiveCommand
|
||||
from watcher.applier.api.promise import Promise
|
||||
|
||||
|
||||
class PowerStateCommand(PrimitiveCommand):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@Promise
|
||||
def execute(self):
|
||||
pass
|
||||
|
||||
@Promise
|
||||
def undo(self):
|
||||
# TODO(jde): migrate VM from target_hypervisor
|
||||
# to current_hypervisor in model
|
||||
return True
|
||||
694
watcher/applier/framework/command/wrapper/nova_wrapper.py
Normal file
694
watcher/applier/framework/command/wrapper/nova_wrapper.py
Normal file
@@ -0,0 +1,694 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
#
|
||||
# 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 random
|
||||
import time
|
||||
|
||||
import cinderclient.exceptions as ciexceptions
|
||||
import cinderclient.v2.client as ciclient
|
||||
import glanceclient.v2.client as glclient
|
||||
import keystoneclient.v3.client as ksclient
|
||||
import neutronclient.neutron.client as netclient
|
||||
import novaclient.exceptions as nvexceptions
|
||||
import novaclient.v2.client as nvclient
|
||||
from watcher.openstack.common import log
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class NovaWrapper(object):
|
||||
def __init__(self, creds, session):
|
||||
self.user = creds['username']
|
||||
self.session = session
|
||||
self.neutron = None
|
||||
self.cinder = None
|
||||
self.nova = nvclient.Client("3", session=session)
|
||||
self.keystone = ksclient.Client(**creds)
|
||||
self.glance = None
|
||||
|
||||
def get_hypervisors_list(self):
|
||||
return self.nova.hypervisors.list()
|
||||
|
||||
def find_instance(self, instance_id):
|
||||
search_opts = {'all_tenants': True}
|
||||
instances = self.nova.servers.list(detailed=True,
|
||||
search_opts=search_opts)
|
||||
instance = None
|
||||
for _instance in instances:
|
||||
if _instance.id == instance_id:
|
||||
instance = _instance
|
||||
break
|
||||
return instance
|
||||
|
||||
def watcher_non_live_migrate_instance(self, instance_id, hypervisor_id,
|
||||
keep_original_image_name=True):
|
||||
"""This method migrates a given instance
|
||||
|
||||
using an image of this instance and creating a new instance
|
||||
from this image. It saves some configuration information
|
||||
about the original instance : security group, list of networks
|
||||
,list of attached volumes, floating IP, ...
|
||||
in order to apply the same settings to the new instance.
|
||||
At the end of the process the original instance is deleted.
|
||||
It returns True if the migration was successful,
|
||||
False otherwise.
|
||||
|
||||
:param instance_id: the unique id of the instance to migrate.
|
||||
:param keep_original_image_name: flag indicating whether the
|
||||
image name from which the original instance was built must be
|
||||
used as the name of the intermediate image used for migration.
|
||||
If this flag is False, a temporary image name is built
|
||||
"""
|
||||
|
||||
new_image_name = ""
|
||||
|
||||
LOG.debug(
|
||||
"Trying a non-live migrate of instance '%s' "
|
||||
"using a temporary image ..." % instance_id)
|
||||
|
||||
# Looking for the instance to migrate
|
||||
instance = self.find_instance(instance_id)
|
||||
if not instance:
|
||||
LOG.debug("Instance %s not found !" % instance_id)
|
||||
return False
|
||||
else:
|
||||
host_name = getattr(instance, "OS-EXT-SRV-ATTR:host")
|
||||
# https://bugs.launchpad.net/nova/+bug/1182965
|
||||
LOG.debug(
|
||||
"Instance %s found on host '%s'." % (instance_id, host_name))
|
||||
|
||||
if not keep_original_image_name:
|
||||
# randrange gives you an integral value
|
||||
irand = random.randint(0, 1000)
|
||||
|
||||
# Building the temporary image name
|
||||
# which will be used for the migration
|
||||
new_image_name = "tmp-migrate-%s-%s" % (instance_id, irand)
|
||||
else:
|
||||
# Get the image name of the current instance.
|
||||
# We'll use the same name for the new instance.
|
||||
imagedict = getattr(instance, "image")
|
||||
image_id = imagedict["id"]
|
||||
image = self.nova.images.get(image_id)
|
||||
new_image_name = getattr(image, "name")
|
||||
|
||||
instance_name = getattr(instance, "name")
|
||||
flavordict = getattr(instance, "flavor")
|
||||
# a_dict = dict([flavorstr.strip('{}').split(":"),])
|
||||
flavor_id = flavordict["id"]
|
||||
flavor = self.nova.flavors.get(flavor_id)
|
||||
flavor_name = getattr(flavor, "name")
|
||||
keypair_name = getattr(instance, "key_name")
|
||||
|
||||
addresses = getattr(instance, "addresses")
|
||||
|
||||
floating_ip = ""
|
||||
network_names_list = []
|
||||
|
||||
for network_name, network_conf_obj in addresses.items():
|
||||
LOG.debug(
|
||||
"Extracting network configuration for network '%s'" %
|
||||
network_name)
|
||||
|
||||
network_names_list.append(network_name)
|
||||
|
||||
for net_conf_item in network_conf_obj:
|
||||
if net_conf_item['OS-EXT-IPS:type'] == "floating":
|
||||
floating_ip = net_conf_item['addr']
|
||||
break
|
||||
|
||||
sec_groups_list = getattr(instance, "security_groups")
|
||||
sec_groups = []
|
||||
|
||||
for sec_group_dict in sec_groups_list:
|
||||
sec_groups.append(sec_group_dict['name'])
|
||||
|
||||
# Stopping the old instance properly so
|
||||
# that no new data is sent to it and to its attached volumes
|
||||
stopped_ok = self.stop_instance(instance_id)
|
||||
|
||||
if not stopped_ok:
|
||||
LOG.debug("Could not stop instance: %s" % instance_id)
|
||||
return False
|
||||
|
||||
# Building the temporary image which will be used
|
||||
# to re-build the same instance on another target host
|
||||
image_uuid = self.create_image_from_instance(instance_id,
|
||||
new_image_name)
|
||||
|
||||
if not image_uuid:
|
||||
LOG.debug(
|
||||
"Could not build temporary image of instance: %s" %
|
||||
instance_id)
|
||||
return False
|
||||
|
||||
#
|
||||
# We need to get the list of attached volumes and detach
|
||||
# them from the instance in order to attache them later
|
||||
# to the new instance
|
||||
#
|
||||
blocks = []
|
||||
|
||||
# Looks like this :
|
||||
# os-extended-volumes:volumes_attached |
|
||||
# [{u'id': u'c5c3245f-dd59-4d4f-8d3a-89d80135859a'}]
|
||||
attached_volumes = getattr(instance,
|
||||
"os-extended-volumes:volumes_attached")
|
||||
|
||||
for attached_volume in attached_volumes:
|
||||
volume_id = attached_volume['id']
|
||||
|
||||
try:
|
||||
if self.cinder is None:
|
||||
self.cinder = ciclient.Client('2',
|
||||
session=self.session)
|
||||
volume = self.cinder.volumes.get(volume_id)
|
||||
|
||||
attachments_list = getattr(volume, "attachments")
|
||||
|
||||
device_name = attachments_list[0]['device']
|
||||
# When a volume is attached to an instance
|
||||
# it contains the following property :
|
||||
# attachments = [{u'device': u'/dev/vdb',
|
||||
# u'server_id': u'742cc508-a2f2-4769-a794-bcdad777e814',
|
||||
# u'id': u'f6d62785-04b8-400d-9626-88640610f65e',
|
||||
# u'host_name': None, u'volume_id':
|
||||
# u'f6d62785-04b8-400d-9626-88640610f65e'}]
|
||||
|
||||
# boot_index indicates a number
|
||||
# designating the boot order of the device.
|
||||
# Use -1 for the boot volume,
|
||||
# choose 0 for an attached volume.
|
||||
block_device_mapping_v2_item = {"device_name": device_name,
|
||||
"source_type": "volume",
|
||||
"destination_type":
|
||||
"volume",
|
||||
"uuid": volume_id,
|
||||
"boot_index": "0"}
|
||||
|
||||
blocks.append(
|
||||
block_device_mapping_v2_item)
|
||||
|
||||
LOG.debug("Detaching volume %s from instance: %s" % (
|
||||
volume_id, instance_id))
|
||||
# volume.detach()
|
||||
self.nova.volumes.delete_server_volume(instance_id,
|
||||
volume_id)
|
||||
|
||||
if not self.wait_for_volume_status(volume, "available", 5,
|
||||
10):
|
||||
LOG.debug(
|
||||
"Could not detach volume %s from instance: %s" % (
|
||||
volume_id, instance_id))
|
||||
return False
|
||||
except ciexceptions.NotFound:
|
||||
LOG.debug("Volume '%s' not found " % image_id)
|
||||
return False
|
||||
|
||||
# We create the new instance from
|
||||
# the intermediate image of the original instance
|
||||
new_instance = self. \
|
||||
create_instance(hypervisor_id,
|
||||
instance_name,
|
||||
image_uuid,
|
||||
flavor_name,
|
||||
sec_groups,
|
||||
network_names_list=network_names_list,
|
||||
keypair_name=keypair_name,
|
||||
create_new_floating_ip=False,
|
||||
block_device_mapping_v2=blocks)
|
||||
|
||||
if not new_instance:
|
||||
LOG.debug(
|
||||
"Could not create new instance "
|
||||
"for non-live migration of instance %s" % instance_id)
|
||||
return False
|
||||
|
||||
try:
|
||||
LOG.debug("Detaching floating ip '%s' from instance %s" % (
|
||||
floating_ip, instance_id))
|
||||
# We detach the floating ip from the current instance
|
||||
instance.remove_floating_ip(floating_ip)
|
||||
|
||||
LOG.debug(
|
||||
"Attaching floating ip '%s' to the new instance %s" % (
|
||||
floating_ip, new_instance.id))
|
||||
|
||||
# We attach the same floating ip to the new instance
|
||||
new_instance.add_floating_ip(floating_ip)
|
||||
except Exception as e:
|
||||
LOG.debug(e)
|
||||
|
||||
new_host_name = getattr(new_instance, "OS-EXT-SRV-ATTR:host")
|
||||
|
||||
# Deleting the old instance (because no more useful)
|
||||
delete_ok = self.delete_instance(instance_id)
|
||||
if not delete_ok:
|
||||
LOG.debug("Could not delete instance: %s" % instance_id)
|
||||
return False
|
||||
|
||||
LOG.debug(
|
||||
"Instance %s has been successfully migrated "
|
||||
"to new host '%s' and its new id is %s." % (
|
||||
instance_id, new_host_name, new_instance.id))
|
||||
|
||||
return True
|
||||
|
||||
def built_in_non_live_migrate_instance(self, instance_id, hypervisor_id):
|
||||
"""This method uses the Nova built-in non-live migrate()
|
||||
action to migrate a given instance.
|
||||
It returns True if the migration was successful, False otherwise.
|
||||
|
||||
:param instance_id: the unique id of the instance to migrate.
|
||||
"""
|
||||
|
||||
LOG.debug(
|
||||
"Trying a Nova built-in non-live "
|
||||
"migrate of instance %s ..." % instance_id)
|
||||
|
||||
# Looking for the instance to migrate
|
||||
instance = self.find_instance(instance_id)
|
||||
|
||||
if not instance:
|
||||
LOG.debug("Instance not found: %s" % instance_id)
|
||||
return False
|
||||
else:
|
||||
host_name = getattr(instance, 'OS-EXT-SRV-ATTR:host')
|
||||
LOG.debug(
|
||||
"Instance %s found on host '%s'." % (instance_id, host_name))
|
||||
|
||||
instance.migrate()
|
||||
|
||||
# Poll at 5 second intervals, until the status is as expected
|
||||
if self.wait_for_instance_status(instance,
|
||||
('VERIFY_RESIZE', 'ERROR'),
|
||||
5, 10):
|
||||
|
||||
instance = self.nova.servers.get(instance.id)
|
||||
|
||||
if instance.status == 'VERIFY_RESIZE':
|
||||
host_name = getattr(instance, 'OS-EXT-SRV-ATTR:host')
|
||||
LOG.debug(
|
||||
"Instance %s has been successfully "
|
||||
"migrated to host '%s'." % (
|
||||
instance_id, host_name))
|
||||
|
||||
# We need to confirm that the resize() operation
|
||||
# has succeeded in order to
|
||||
# get back instance state to 'ACTIVE'
|
||||
instance.confirm_resize()
|
||||
|
||||
return True
|
||||
elif instance.status == 'ERROR':
|
||||
LOG.debug("Instance %s migration failed" % instance_id)
|
||||
|
||||
return False
|
||||
|
||||
def live_migrate_instance(self, instance_id, dest_hostname,
|
||||
block_migration=True, retry=120):
|
||||
"""This method uses the Nova built-in live_migrate()
|
||||
action to do a live migration of a given instance.
|
||||
It returns True if the migration was successful,
|
||||
False otherwise.
|
||||
|
||||
:param instance_id: the unique id of the instance to migrate.
|
||||
:param dest_hostname: the name of the destination compute node.
|
||||
:param block_migration: No shared storage is required.
|
||||
"""
|
||||
|
||||
LOG.debug("Trying a live migrate of instance %s to host '%s'" % (
|
||||
instance_id, dest_hostname))
|
||||
|
||||
# Looking for the instance to migrate
|
||||
instance = self.find_instance(instance_id)
|
||||
|
||||
if not instance:
|
||||
LOG.debug("Instance not found: %s" % instance_id)
|
||||
return False
|
||||
else:
|
||||
host_name = getattr(instance, 'OS-EXT-SRV-ATTR:host')
|
||||
LOG.debug(
|
||||
"Instance %s found on host '%s'." % (instance_id, host_name))
|
||||
|
||||
instance.live_migrate(host=dest_hostname,
|
||||
block_migration=block_migration,
|
||||
disk_over_commit=True)
|
||||
while getattr(instance,
|
||||
'OS-EXT-SRV-ATTR:host') != dest_hostname \
|
||||
and retry:
|
||||
instance = self.nova.servers.get(instance.id)
|
||||
LOG.debug(
|
||||
"Waiting the migration of " + str(
|
||||
instance.human_id) + " to " +
|
||||
getattr(instance,
|
||||
'OS-EXT-SRV-ATTR:host'))
|
||||
time.sleep(1)
|
||||
retry -= 1
|
||||
|
||||
host_name = getattr(instance, 'OS-EXT-SRV-ATTR:host')
|
||||
if host_name != dest_hostname:
|
||||
return False
|
||||
|
||||
LOG.debug(
|
||||
"Live migration succeeded : "
|
||||
"instance %s is now on host '%s'." % (
|
||||
instance_id, host_name))
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def enable_service_nova_compute(self, hostname):
|
||||
if self.nova.services.enable(host=hostname,
|
||||
binary='nova-compute'). \
|
||||
status == 'enabled':
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def disable_service_nova_compute(self, hostname):
|
||||
if self.nova.services.disable(host=hostname,
|
||||
binary='nova-compute'). \
|
||||
status == 'disabled':
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def set_host_offline(self, hostname):
|
||||
# See API on http://developer.openstack.org/api-ref-compute-v2.1.html
|
||||
# especially the PUT request
|
||||
# regarding this resource : /v2.1/os-hosts/{host_name}
|
||||
#
|
||||
# The following body should be sent :
|
||||
# {
|
||||
# "host": {
|
||||
# "host": "65c5d5b7e3bd44308e67fc50f362aee6",
|
||||
# "maintenance_mode": "off_maintenance",
|
||||
# "status": "enabled"
|
||||
# }
|
||||
# }
|
||||
|
||||
# Voir ici
|
||||
# https://github.com/openstack/nova/
|
||||
# blob/master/nova/virt/xenapi/host.py
|
||||
# set_host_enabled(self, enabled):
|
||||
# Sets the compute host's ability to accept new instances.
|
||||
# host_maintenance_mode(self, host, mode):
|
||||
# Start/Stop host maintenance window.
|
||||
# On start, it triggers guest VMs evacuation.
|
||||
host = self.nova.hosts.get(hostname)
|
||||
|
||||
if not host:
|
||||
LOG.debug("host not found: %s" % hostname)
|
||||
return False
|
||||
else:
|
||||
host[0].update(
|
||||
{"maintenance_mode": "disable", "status": "disable"})
|
||||
return True
|
||||
|
||||
def create_image_from_instance(self, instance_id, image_name,
|
||||
metadata={"reason": "instance_migrate"}):
|
||||
"""This method creates a new image from a given instance.
|
||||
It waits for this image to be in 'active' state before returning.
|
||||
It returns the unique UUID of the created image if successful,
|
||||
None otherwise
|
||||
|
||||
:param instance_id: the uniqueid of
|
||||
the instance to backup as an image.
|
||||
:param image_name: the name of the image to create.
|
||||
:param metadata: a dictionary containing the list of
|
||||
key-value pairs to associate to the image as metadata.
|
||||
"""
|
||||
if self.glance is None:
|
||||
glance_endpoint = self.keystone. \
|
||||
service_catalog.url_for(service_type='image',
|
||||
endpoint_type='publicURL')
|
||||
self.glance = glclient.Client(glance_endpoint,
|
||||
token=self.keystone.auth_token)
|
||||
|
||||
LOG.debug(
|
||||
"Trying to create an image from instance %s ..." % instance_id)
|
||||
|
||||
# Looking for the instance
|
||||
instance = self.find_instance(instance_id)
|
||||
|
||||
if not instance:
|
||||
LOG.debug("Instance not found: %s" % instance_id)
|
||||
return None
|
||||
else:
|
||||
host_name = getattr(instance, 'OS-EXT-SRV-ATTR:host')
|
||||
LOG.debug(
|
||||
"Instance %s found on host '%s'." % (instance_id, host_name))
|
||||
|
||||
# We need to wait for an appropriate status
|
||||
# of the instance before we can build an image from it
|
||||
if self.wait_for_instance_status(instance, ('ACTIVE', 'SHUTOFF'),
|
||||
5,
|
||||
10):
|
||||
image_uuid = self.nova.servers.create_image(instance_id,
|
||||
image_name,
|
||||
metadata)
|
||||
|
||||
image = self.glance.images.get(image_uuid)
|
||||
|
||||
# Waiting for the new image to be officially in ACTIVE state
|
||||
# in order to make sure it can be used
|
||||
status = image.status
|
||||
retry = 10
|
||||
while status != 'active' and status != 'error' and retry:
|
||||
time.sleep(5)
|
||||
retry -= 1
|
||||
# Retrieve the instance again so the status field updates
|
||||
image = self.glance.images.get(image_uuid)
|
||||
status = image.status
|
||||
LOG.debug("Current image status: %s" % status)
|
||||
|
||||
if not image:
|
||||
LOG.debug("Image not found: %s" % image_uuid)
|
||||
else:
|
||||
LOG.debug(
|
||||
"Image %s successfully created for instance %s" % (
|
||||
image_uuid, instance_id))
|
||||
return image_uuid
|
||||
return None
|
||||
|
||||
def delete_instance(self, instance_id):
|
||||
"""This method deletes a given instance.
|
||||
|
||||
:param instance_id: the unique id of the instance to delete.
|
||||
"""
|
||||
|
||||
LOG.debug("Trying to remove instance %s ..." % instance_id)
|
||||
|
||||
instance = self.find_instance(instance_id)
|
||||
|
||||
if not instance:
|
||||
LOG.debug("Instance not found: %s" % instance_id)
|
||||
return False
|
||||
else:
|
||||
self.nova.servers.delete(instance_id)
|
||||
LOG.debug("Instance %s removed." % instance_id)
|
||||
return True
|
||||
|
||||
def stop_instance(self, instance_id):
|
||||
"""This method stops a given instance.
|
||||
|
||||
:param instance_id: the unique id of the instance to stop.
|
||||
"""
|
||||
|
||||
LOG.debug("Trying to stop instance %s ..." % instance_id)
|
||||
|
||||
instance = self.find_instance(instance_id)
|
||||
|
||||
if not instance:
|
||||
LOG.debug("Instance not found: %s" % instance_id)
|
||||
return False
|
||||
else:
|
||||
self.nova.servers.stop(instance_id)
|
||||
|
||||
if self.wait_for_vm_state(instance, "stopped", 8, 10):
|
||||
LOG.debug("Instance %s stopped." % instance_id)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def wait_for_vm_state(self, server, vm_state, retry, sleep):
|
||||
"""Waits for server to be in vm_state which can be one of the following :
|
||||
active, stopped
|
||||
|
||||
:param server: server object.
|
||||
:param vm_state: for which state we are waiting for
|
||||
:param retry: how many times to retry
|
||||
:param sleep: seconds to sleep between the retries
|
||||
"""
|
||||
if not server:
|
||||
return False
|
||||
|
||||
while getattr(server, 'OS-EXT-STS:vm_state') != vm_state and retry:
|
||||
time.sleep(sleep)
|
||||
server = self.nova.servers.get(server)
|
||||
retry -= 1
|
||||
return getattr(server, 'OS-EXT-STS:vm_state') == vm_state
|
||||
|
||||
def wait_for_instance_status(self, instance, status_list, retry, sleep):
|
||||
"""Waits for instance to be in status which can be one of the following
|
||||
: BUILD, ACTIVE, ERROR, VERIFY_RESIZE, SHUTOFF
|
||||
|
||||
:param instance: instance object.
|
||||
:param status_list: tuple containing the list of
|
||||
status we are waiting for
|
||||
:param retry: how many times to retry
|
||||
:param sleep: seconds to sleep between the retries
|
||||
"""
|
||||
if not instance:
|
||||
return False
|
||||
|
||||
while instance.status not in status_list and retry:
|
||||
LOG.debug("Current instance status: %s" % instance.status)
|
||||
time.sleep(sleep)
|
||||
instance = self.nova.servers.get(instance.id)
|
||||
retry -= 1
|
||||
LOG.debug("Current instance status: %s" % instance.status)
|
||||
return instance.status in status_list
|
||||
|
||||
def create_instance(self, hypervisor_id, inst_name="test", image_id=None,
|
||||
flavor_name="m1.tiny",
|
||||
sec_group_list=["default"],
|
||||
network_names_list=["private"], keypair_name="mykeys",
|
||||
create_new_floating_ip=True,
|
||||
block_device_mapping_v2=None):
|
||||
"""This method creates a new instance.
|
||||
It also creates, if requested, a new floating IP and associates
|
||||
it with the new instance
|
||||
It returns the unique id of the created instance.
|
||||
"""
|
||||
|
||||
LOG.debug(
|
||||
"Trying to create new instance '%s' "
|
||||
"from image '%s' with flavor '%s' ..." % (
|
||||
inst_name, image_id, flavor_name))
|
||||
# TODO(jed) wait feature
|
||||
# Allow admin users to view any keypair
|
||||
# https://bugs.launchpad.net/nova/+bug/1182965
|
||||
if not self.nova.keypairs.findall(name=keypair_name):
|
||||
LOG.debug("Key pair '%s' not found with user '%s'" % (
|
||||
keypair_name, self.user))
|
||||
return
|
||||
else:
|
||||
LOG.debug("Key pair '%s' found with user '%s'" % (
|
||||
keypair_name, self.user))
|
||||
|
||||
try:
|
||||
image = self.nova.images.get(image_id)
|
||||
except nvexceptions.NotFound:
|
||||
LOG.debug("Image '%s' not found " % image_id)
|
||||
return
|
||||
|
||||
try:
|
||||
flavor = self.nova.flavors.find(name=flavor_name)
|
||||
except nvexceptions.NotFound:
|
||||
LOG.debug("Flavor '%s' not found " % flavor_name)
|
||||
return
|
||||
|
||||
# Make sure all security groups exist
|
||||
for sec_group_name in sec_group_list:
|
||||
try:
|
||||
self.nova.security_groups.find(name=sec_group_name)
|
||||
|
||||
except nvexceptions.NotFound:
|
||||
LOG.debug("Security group '%s' not found " % sec_group_name)
|
||||
return
|
||||
|
||||
net_list = list()
|
||||
|
||||
for network_name in network_names_list:
|
||||
nic_id = self.get_network_id_from_name(network_name)
|
||||
|
||||
if not nic_id:
|
||||
LOG.debug("Network '%s' not found " % network_name)
|
||||
return
|
||||
net_obj = {"net-id": nic_id}
|
||||
net_list.append(net_obj)
|
||||
s = self.nova.servers
|
||||
instance = s.create(inst_name,
|
||||
image, flavor=flavor,
|
||||
key_name=keypair_name,
|
||||
security_groups=sec_group_list,
|
||||
nics=net_list,
|
||||
block_device_mapping_v2=block_device_mapping_v2,
|
||||
availability_zone="nova:" +
|
||||
hypervisor_id)
|
||||
|
||||
# Poll at 5 second intervals, until the status is no longer 'BUILD'
|
||||
if instance:
|
||||
if self.wait_for_instance_status(instance,
|
||||
('ACTIVE', 'ERROR'), 5, 10):
|
||||
instance = self.nova.servers.get(instance.id)
|
||||
|
||||
if create_new_floating_ip and instance.status == 'ACTIVE':
|
||||
LOG.debug(
|
||||
"Creating a new floating IP"
|
||||
" for instance '%s'" % instance.id)
|
||||
# Creating floating IP for the new instance
|
||||
floating_ip = self.nova.floating_ips.create()
|
||||
|
||||
instance.add_floating_ip(floating_ip)
|
||||
|
||||
LOG.debug("Instance %s associated to Floating IP '%s'" % (
|
||||
instance.id, floating_ip.ip))
|
||||
|
||||
return instance
|
||||
|
||||
def get_network_id_from_name(self, net_name="private"):
|
||||
"""This method returns the unique id of the provided network name"""
|
||||
if self.neutron is None:
|
||||
self.neutron = netclient.Client('2.0', session=self.session)
|
||||
self.neutron.format = 'json'
|
||||
|
||||
networks = self.neutron.list_networks(name=net_name)
|
||||
|
||||
# LOG.debug(networks)
|
||||
network_id = networks['networks'][0]['id']
|
||||
|
||||
return network_id
|
||||
|
||||
def get_vms_by_hypervisor(self, host):
|
||||
return [vm for vm in
|
||||
self.nova.servers.list(search_opts={"all_tenants": True})
|
||||
if self.get_hostname(vm) == host]
|
||||
|
||||
def get_hostname(self, vm):
|
||||
return str(getattr(vm, 'OS-EXT-SRV-ATTR:host'))
|
||||
|
||||
def get_flavor_instance(self, instance, cache):
|
||||
fid = instance.flavor['id']
|
||||
if fid in cache:
|
||||
flavor = cache.get(fid)
|
||||
else:
|
||||
try:
|
||||
flavor = self.nova.flavors.get(fid)
|
||||
except ciexceptions.NotFound:
|
||||
flavor = None
|
||||
cache[fid] = flavor
|
||||
attr_defaults = [('name', 'unknown-id-%s' % fid),
|
||||
('vcpus', 0), ('ram', 0), ('disk', 0),
|
||||
('ephemeral', 0)]
|
||||
for attr, default in attr_defaults:
|
||||
if not flavor:
|
||||
instance.flavor[attr] = default
|
||||
continue
|
||||
instance.flavor[attr] = getattr(flavor, attr, default)
|
||||
73
watcher/applier/framework/command_executor.py
Normal file
73
watcher/applier/framework/command_executor.py
Normal file
@@ -0,0 +1,73 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
#
|
||||
# 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.applier.framework.default_command_mapper import \
|
||||
DefaultCommandMapper
|
||||
|
||||
from watcher.applier.framework.deploy_phase import DeployPhase
|
||||
from watcher.applier.framework.messaging.events import Events
|
||||
from watcher.common.messaging.events.event import Event
|
||||
from watcher.objects import Action
|
||||
from watcher.objects.action_plan import Status
|
||||
from watcher.openstack.common import log
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class CommandExecutor(object):
|
||||
def __init__(self, manager_applier, context):
|
||||
self.manager_applier = manager_applier
|
||||
self.context = context
|
||||
self.deploy = DeployPhase(self)
|
||||
self.mapper = DefaultCommandMapper()
|
||||
|
||||
def get_primitive(self, action):
|
||||
return self.mapper.build_primitive_command(action)
|
||||
|
||||
def notify(self, action, state):
|
||||
db_action = Action.get_by_uuid(self.context, action.uuid)
|
||||
db_action.state = state
|
||||
db_action.save()
|
||||
event = Event()
|
||||
event.set_type(Events.LAUNCH_ACTION)
|
||||
event.set_data({})
|
||||
payload = {'action_uuid': action.uuid,
|
||||
'action_status': state}
|
||||
self.manager_applier.topic_status.publish_event(event.get_type().name,
|
||||
payload)
|
||||
|
||||
def execute(self, actions):
|
||||
for action in actions:
|
||||
try:
|
||||
self.notify(action, Status.ONGOING)
|
||||
primitive = self.get_primitive(action)
|
||||
result = self.deploy.execute_primitive(primitive)
|
||||
if result is False:
|
||||
self.notify(action, Status.FAILED)
|
||||
self.deploy.rollback()
|
||||
return False
|
||||
else:
|
||||
self.deploy.populate(primitive)
|
||||
self.notify(action, Status.SUCCESS)
|
||||
except Exception as e:
|
||||
LOG.error(
|
||||
"The applier module failed to execute the action" + str(
|
||||
action) + " with the exception : " + unicode(e))
|
||||
LOG.error("Trigger a rollback")
|
||||
self.notify(action, Status.FAILED)
|
||||
self.deploy.rollback()
|
||||
return False
|
||||
return True
|
||||
35
watcher/applier/framework/default_applier.py
Normal file
35
watcher/applier/framework/default_applier.py
Normal file
@@ -0,0 +1,35 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
#
|
||||
# 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.applier.api.applier import Applier
|
||||
from watcher.applier.framework.command_executor import CommandExecutor
|
||||
from watcher.objects import Action
|
||||
from watcher.objects import ActionPlan
|
||||
|
||||
|
||||
class DefaultApplier(Applier):
|
||||
def __init__(self, manager_applier, context):
|
||||
self.manager_applier = manager_applier
|
||||
self.context = context
|
||||
self.executor = CommandExecutor(manager_applier, context)
|
||||
|
||||
def execute(self, action_plan_uuid):
|
||||
action_plan = ActionPlan.get_by_uuid(self.context, action_plan_uuid)
|
||||
# todo(jed) remove direct access to dbapi need filter in object
|
||||
actions = Action.dbapi.get_action_list(self.context,
|
||||
filters={
|
||||
'action_plan_id':
|
||||
action_plan.id})
|
||||
return self.executor.execute(actions)
|
||||
46
watcher/applier/framework/default_command_mapper.py
Normal file
46
watcher/applier/framework/default_command_mapper.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
#
|
||||
# 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.applier.api.command_mapper import CommandMapper
|
||||
from watcher.applier.framework.command.hypervisor_state_command import \
|
||||
HypervisorStateCommand
|
||||
from watcher.applier.framework.command.migrate_command import MigrateCommand
|
||||
from watcher.applier.framework.command.nop_command import NopCommand
|
||||
from watcher.applier.framework.command.power_state_command import \
|
||||
PowerStateCommand
|
||||
from watcher.common.exception import ActionNotFound
|
||||
from watcher.decision_engine.framework.default_planner import Primitives
|
||||
|
||||
|
||||
class DefaultCommandMapper(CommandMapper):
|
||||
def build_primitive_command(self, action):
|
||||
if action.action_type == Primitives.COLD_MIGRATE.value:
|
||||
return MigrateCommand(action.applies_to, Primitives.COLD_MIGRATE,
|
||||
action.src,
|
||||
action.dst)
|
||||
elif action.action_type == Primitives.LIVE_MIGRATE.value:
|
||||
return MigrateCommand(action.applies_to, Primitives.COLD_MIGRATE,
|
||||
action.src,
|
||||
action.dst)
|
||||
elif action.action_type == Primitives.HYPERVISOR_STATE.value:
|
||||
return HypervisorStateCommand(action.applies_to, action.parameter)
|
||||
elif action.action_type == Primitives.POWER_STATE.value:
|
||||
return PowerStateCommand()
|
||||
elif action.action_type == Primitives.NOP.value:
|
||||
return NopCommand()
|
||||
else:
|
||||
raise ActionNotFound()
|
||||
46
watcher/applier/framework/deploy_phase.py
Normal file
46
watcher/applier/framework/deploy_phase.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
#
|
||||
# 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.openstack.common import log
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class DeployPhase(object):
|
||||
def __init__(self, executor):
|
||||
# todo(jed) oslo_conf 10 secondes
|
||||
self.maxTimeout = 100000
|
||||
self.commands = []
|
||||
self.executor = executor
|
||||
|
||||
def set_max_time(self, mt):
|
||||
self.maxTimeout = mt
|
||||
|
||||
def get_max_time(self):
|
||||
return self.maxTimeout
|
||||
|
||||
def populate(self, action):
|
||||
self.commands.append(action)
|
||||
|
||||
def execute_primitive(self, primitive):
|
||||
futur = primitive.execute(primitive)
|
||||
return futur.result(self.get_max_time())
|
||||
|
||||
def rollback(self):
|
||||
reverted = sorted(self.commands, reverse=True)
|
||||
for primitive in reverted:
|
||||
try:
|
||||
self.execute_primitive(primitive)
|
||||
except Exception as e:
|
||||
LOG.error(e)
|
||||
98
watcher/applier/framework/manager_applier.py
Normal file
98
watcher/applier/framework/manager_applier.py
Normal file
@@ -0,0 +1,98 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
#
|
||||
# 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 concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
from oslo_config import cfg
|
||||
|
||||
from watcher.applier.framework.messaging.trigger_action_plan import \
|
||||
TriggerActionPlan
|
||||
from watcher.common.messaging.messaging_core import MessagingCore
|
||||
from watcher.common.messaging.notification_handler import NotificationHandler
|
||||
from watcher.decision_engine.framework.messaging.events import Events
|
||||
from watcher.openstack.common import log
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
# Register options
|
||||
APPLIER_MANAGER_OPTS = [
|
||||
cfg.IntOpt('applier_worker', default='1', help='The number of worker'),
|
||||
cfg.StrOpt('topic_control',
|
||||
default='watcher.applier.control',
|
||||
help='The topic name used for'
|
||||
'control events, this topic '
|
||||
'used for rpc call '),
|
||||
cfg.StrOpt('topic_status',
|
||||
default='watcher.applier.status',
|
||||
help='The topic name used for '
|
||||
'status events, this topic '
|
||||
'is used so as to notify'
|
||||
'the others components '
|
||||
'of the system'),
|
||||
cfg.StrOpt('publisher_id',
|
||||
default='watcher.applier.api',
|
||||
help='The identifier used by watcher '
|
||||
'module on the message broker')
|
||||
]
|
||||
CONF = cfg.CONF
|
||||
opt_group = cfg.OptGroup(name='watcher_applier',
|
||||
title='Options for the Applier messaging'
|
||||
'core')
|
||||
CONF.register_group(opt_group)
|
||||
CONF.register_opts(APPLIER_MANAGER_OPTS, opt_group)
|
||||
|
||||
CONF.import_opt('admin_user', 'keystonemiddleware.auth_token',
|
||||
group='keystone_authtoken')
|
||||
CONF.import_opt('admin_tenant_name', 'keystonemiddleware.auth_token',
|
||||
group='keystone_authtoken')
|
||||
CONF.import_opt('admin_password', 'keystonemiddleware.auth_token',
|
||||
group='keystone_authtoken')
|
||||
CONF.import_opt('auth_uri', 'keystonemiddleware.auth_token',
|
||||
group='keystone_authtoken')
|
||||
|
||||
|
||||
class ApplierManager(MessagingCore):
|
||||
API_VERSION = '1.0'
|
||||
# todo(jed) need workflow
|
||||
|
||||
def __init__(self):
|
||||
MessagingCore.__init__(self, CONF.watcher_applier.publisher_id,
|
||||
CONF.watcher_applier.topic_control,
|
||||
CONF.watcher_applier.topic_status)
|
||||
# shared executor of the workflow
|
||||
self.executor = ThreadPoolExecutor(max_workers=1)
|
||||
self.handler = NotificationHandler(self.publisher_id)
|
||||
self.handler.register_observer(self)
|
||||
self.add_event_listener(Events.ALL, self.event_receive)
|
||||
# trigger action_plan
|
||||
self.topic_control.add_endpoint(TriggerActionPlan(self))
|
||||
|
||||
def join(self):
|
||||
self.topic_control.join()
|
||||
self.topic_status.join()
|
||||
|
||||
def event_receive(self, event):
|
||||
try:
|
||||
request_id = event.get_request_id()
|
||||
event_type = event.get_type()
|
||||
data = event.get_data()
|
||||
LOG.debug("request id => %s" % request_id)
|
||||
LOG.debug("type_event => %s" % str(event_type))
|
||||
LOG.debug("data => %s" % str(data))
|
||||
except Exception as e:
|
||||
LOG.error("evt %s" % e.message)
|
||||
raise e
|
||||
0
watcher/applier/framework/messaging/__init__.py
Normal file
0
watcher/applier/framework/messaging/__init__.py
Normal file
22
watcher/applier/framework/messaging/events.py
Normal file
22
watcher/applier/framework/messaging/events.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
#
|
||||
# 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 enum import Enum
|
||||
|
||||
|
||||
class Events(Enum):
|
||||
LAUNCH_ACTION_PLAN = "launch_action_plan"
|
||||
LAUNCH_ACTION = "launch_action"
|
||||
65
watcher/applier/framework/messaging/launch_action_plan.py
Normal file
65
watcher/applier/framework/messaging/launch_action_plan.py
Normal file
@@ -0,0 +1,65 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
#
|
||||
# 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.applier.api.messaging.applier_command import ApplierCommand
|
||||
from watcher.applier.framework.default_applier import DefaultApplier
|
||||
from watcher.applier.framework.messaging.events import Events
|
||||
from watcher.common.messaging.events.event import Event
|
||||
from watcher.objects.action_plan import ActionPlan
|
||||
from watcher.objects.action_plan import Status
|
||||
|
||||
from watcher.openstack.common import log
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class LaunchActionPlanCommand(ApplierCommand):
|
||||
def __init__(self, context, manager_applier, action_plan_uuid):
|
||||
self.ctx = context
|
||||
self.action_plan_uuid = action_plan_uuid
|
||||
self.manager_applier = manager_applier
|
||||
|
||||
def notify(self, uuid, event_type, status):
|
||||
action_plan = ActionPlan.get_by_uuid(self.ctx, uuid)
|
||||
action_plan.state = status
|
||||
action_plan.save()
|
||||
event = Event()
|
||||
event.set_type(event_type)
|
||||
event.set_data({})
|
||||
payload = {'action_plan__uuid': uuid,
|
||||
'action_plan_status': status}
|
||||
self.manager_applier.topic_status.publish_event(event.get_type().name,
|
||||
payload)
|
||||
|
||||
def execute(self):
|
||||
try:
|
||||
# update state
|
||||
self.notify(self.action_plan_uuid,
|
||||
Events.LAUNCH_ACTION_PLAN,
|
||||
Status.ONGOING)
|
||||
applier = DefaultApplier(self.manager_applier, self.ctx)
|
||||
result = applier.execute(self.action_plan_uuid)
|
||||
except Exception as e:
|
||||
result = False
|
||||
LOG.error("Launch Action Plan " + unicode(e))
|
||||
finally:
|
||||
if result is True:
|
||||
status = Status.SUCCESS
|
||||
else:
|
||||
status = Status.FAILED
|
||||
# update state
|
||||
self.notify(self.action_plan_uuid, Events.LAUNCH_ACTION_PLAN,
|
||||
status)
|
||||
44
watcher/applier/framework/messaging/trigger_action_plan.py
Normal file
44
watcher/applier/framework/messaging/trigger_action_plan.py
Normal file
@@ -0,0 +1,44 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
#
|
||||
# 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.applier.framework.messaging.launch_action_plan import \
|
||||
LaunchActionPlanCommand
|
||||
from watcher.openstack.common import log
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class TriggerActionPlan(object):
|
||||
def __init__(self, manager_applier):
|
||||
self.manager_applier = manager_applier
|
||||
|
||||
def do_launch_action_plan(self, context, action_plan_uuid):
|
||||
try:
|
||||
cmd = LaunchActionPlanCommand(context,
|
||||
self.manager_applier,
|
||||
action_plan_uuid)
|
||||
cmd.execute()
|
||||
except Exception as e:
|
||||
LOG.error("do_launch_action_plan " + unicode(e))
|
||||
|
||||
def launch_action_plan(self, context, action_plan_uuid):
|
||||
LOG.debug("Trigger ActionPlan %s" % action_plan_uuid)
|
||||
# submit
|
||||
self.manager_applier.executor.submit(self.do_launch_action_plan,
|
||||
context,
|
||||
action_plan_uuid)
|
||||
return action_plan_uuid
|
||||
70
watcher/applier/framework/rpcapi.py
Normal file
70
watcher/applier/framework/rpcapi.py
Normal file
@@ -0,0 +1,70 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
#
|
||||
# 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 oslo_messaging as om
|
||||
|
||||
|
||||
from watcher.applier.framework.manager_applier import APPLIER_MANAGER_OPTS
|
||||
from watcher.applier.framework.manager_applier import opt_group
|
||||
from watcher.common import exception
|
||||
from watcher.common import utils
|
||||
|
||||
|
||||
from watcher.common.messaging.messaging_core import MessagingCore
|
||||
from watcher.common.messaging.notification_handler import NotificationHandler
|
||||
from watcher.common.messaging.utils.transport_url_builder import \
|
||||
TransportUrlBuilder
|
||||
from watcher.openstack.common import log
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
CONF.register_group(opt_group)
|
||||
CONF.register_opts(APPLIER_MANAGER_OPTS, opt_group)
|
||||
|
||||
|
||||
class ApplierAPI(MessagingCore):
|
||||
MessagingCore.API_VERSION = '1.0'
|
||||
|
||||
def __init__(self):
|
||||
MessagingCore.__init__(self, CONF.watcher_applier.publisher_id,
|
||||
CONF.watcher_applier.topic_control,
|
||||
CONF.watcher_applier.topic_status)
|
||||
self.handler = NotificationHandler(self.publisher_id)
|
||||
self.handler.register_observer(self)
|
||||
self.topic_status.add_endpoint(self.handler)
|
||||
transport = om.get_transport(CONF, TransportUrlBuilder().url)
|
||||
target = om.Target(
|
||||
topic=CONF.watcher_applier.topic_control,
|
||||
version=MessagingCore.API_VERSION)
|
||||
|
||||
self.client = om.RPCClient(transport, target,
|
||||
serializer=self.serializer)
|
||||
|
||||
def launch_action_plan(self, context, action_plan_uuid=None):
|
||||
if not utils.is_uuid_like(action_plan_uuid):
|
||||
raise exception.InvalidUuidOrName(name=action_plan_uuid)
|
||||
|
||||
return self.client.call(
|
||||
context.to_dict(), 'launch_action_plan',
|
||||
action_plan_uuid=action_plan_uuid)
|
||||
|
||||
def event_receive(self, event):
|
||||
try:
|
||||
pass
|
||||
except Exception as e:
|
||||
LOG.error("evt %s" % e.message)
|
||||
raise e
|
||||
0
watcher/cmd/__init__.py
Normal file
0
watcher/cmd/__init__.py
Normal file
57
watcher/cmd/api.py
Normal file
57
watcher/cmd/api.py
Normal file
@@ -0,0 +1,57 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||
# 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.
|
||||
|
||||
"""Starter script for the Watcher API service."""
|
||||
|
||||
import logging as std_logging
|
||||
import os
|
||||
from wsgiref import simple_server
|
||||
|
||||
from oslo_config import cfg
|
||||
|
||||
from watcher.api import app as api_app
|
||||
from watcher.common.i18n import _
|
||||
from watcher.openstack.common import log as logging
|
||||
from watcher import service
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def main():
|
||||
service.prepare_service()
|
||||
|
||||
app = api_app.setup_app()
|
||||
|
||||
# Create the WSGI server and start it
|
||||
host, port = cfg.CONF.api.host, cfg.CONF.api.port
|
||||
srv = simple_server.make_server(host, port, app)
|
||||
|
||||
logging.setup('watcher')
|
||||
LOG.info(_('Starting server in PID %s') % os.getpid())
|
||||
LOG.debug("Watcher configuration:")
|
||||
cfg.CONF.log_opt_values(LOG, std_logging.DEBUG)
|
||||
|
||||
if host == '0.0.0.0':
|
||||
LOG.info(_('serving on 0.0.0.0:%(port)s, '
|
||||
'view at http://127.0.0.1:%(port)s') %
|
||||
dict(port=port))
|
||||
else:
|
||||
LOG.info(_('serving on http://%(host)s:%(port)s') %
|
||||
dict(host=host, port=port))
|
||||
|
||||
srv.serve_forever()
|
||||
44
watcher/cmd/applier.py
Normal file
44
watcher/cmd/applier.py
Normal file
@@ -0,0 +1,44 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||
# 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.
|
||||
|
||||
"""Starter script for the Applier service."""
|
||||
|
||||
import logging as std_logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
from oslo_config import cfg
|
||||
from watcher.applier.framework.manager_applier import ApplierManager
|
||||
|
||||
from watcher.openstack.common._i18n import _LI
|
||||
from watcher.openstack.common import log as logging
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def main():
|
||||
cfg.CONF(sys.argv[1:], project='watcher')
|
||||
logging.setup('watcher')
|
||||
|
||||
LOG.info(_LI('Starting server in PID %s') % os.getpid())
|
||||
LOG.debug("Configuration:")
|
||||
cfg.CONF.log_opt_values(LOG, std_logging.DEBUG)
|
||||
|
||||
server = ApplierManager()
|
||||
server.connect()
|
||||
server.join()
|
||||
115
watcher/cmd/dbmanage.py
Normal file
115
watcher/cmd/dbmanage.py
Normal file
@@ -0,0 +1,115 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Run storage database migration.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
from oslo_config import cfg
|
||||
|
||||
from watcher.common import service
|
||||
from watcher.db import migration
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class DBCommand(object):
|
||||
|
||||
def upgrade(self):
|
||||
migration.upgrade(CONF.command.revision)
|
||||
|
||||
def downgrade(self):
|
||||
migration.downgrade(CONF.command.revision)
|
||||
|
||||
def revision(self):
|
||||
migration.revision(CONF.command.message, CONF.command.autogenerate)
|
||||
|
||||
def stamp(self):
|
||||
migration.stamp(CONF.command.revision)
|
||||
|
||||
def version(self):
|
||||
print(migration.version())
|
||||
|
||||
def create_schema(self):
|
||||
migration.create_schema()
|
||||
|
||||
|
||||
def add_command_parsers(subparsers):
|
||||
command_object = DBCommand()
|
||||
|
||||
parser = subparsers.add_parser(
|
||||
'upgrade',
|
||||
help="Upgrade the database schema to the latest version. "
|
||||
"Optionally, use --revision to specify an alembic revision "
|
||||
"string to upgrade to.")
|
||||
parser.set_defaults(func=command_object.upgrade)
|
||||
parser.add_argument('--revision', nargs='?')
|
||||
|
||||
parser = subparsers.add_parser(
|
||||
'downgrade',
|
||||
help="Downgrade the database schema to the oldest revision. "
|
||||
"While optional, one should generally use --revision to "
|
||||
"specify the alembic revision string to downgrade to.")
|
||||
parser.set_defaults(func=command_object.downgrade)
|
||||
parser.add_argument('--revision', nargs='?')
|
||||
|
||||
parser = subparsers.add_parser('stamp')
|
||||
parser.add_argument('--revision', nargs='?')
|
||||
parser.set_defaults(func=command_object.stamp)
|
||||
|
||||
parser = subparsers.add_parser(
|
||||
'revision',
|
||||
help="Create a new alembic revision. "
|
||||
"Use --message to set the message string.")
|
||||
parser.add_argument('-m', '--message')
|
||||
parser.add_argument('--autogenerate', action='store_true')
|
||||
parser.set_defaults(func=command_object.revision)
|
||||
|
||||
parser = subparsers.add_parser(
|
||||
'version',
|
||||
help="Print the current version information and exit.")
|
||||
parser.set_defaults(func=command_object.version)
|
||||
|
||||
parser = subparsers.add_parser(
|
||||
'create_schema',
|
||||
help="Create the database schema.")
|
||||
parser.set_defaults(func=command_object.create_schema)
|
||||
|
||||
|
||||
command_opt = cfg.SubCommandOpt('command',
|
||||
title='Command',
|
||||
help='Available commands',
|
||||
handler=add_command_parsers)
|
||||
|
||||
CONF.register_cli_opt(command_opt)
|
||||
|
||||
|
||||
def main():
|
||||
# this is hack to work with previous usage of watcher-dbsync
|
||||
# pls change it to watcher-dbsync upgrade
|
||||
valid_commands = set([
|
||||
'upgrade', 'downgrade', 'revision',
|
||||
'version', 'stamp', 'create_schema',
|
||||
])
|
||||
if not set(sys.argv) & valid_commands:
|
||||
sys.argv.append('upgrade')
|
||||
|
||||
service.prepare_service(sys.argv)
|
||||
CONF.command.func()
|
||||
45
watcher/cmd/decisionengine.py
Normal file
45
watcher/cmd/decisionengine.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||
# 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.
|
||||
|
||||
"""Starter script for the Decision Engine manager service."""
|
||||
|
||||
import logging as std_logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
from oslo_config import cfg
|
||||
|
||||
from watcher.decision_engine.framework.manager_decision_engine import \
|
||||
DecisionEngineManager
|
||||
from watcher.openstack.common._i18n import _LI
|
||||
from watcher.openstack.common import log as logging
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def main():
|
||||
cfg.CONF(sys.argv[1:], project='watcher')
|
||||
logging.setup('watcher')
|
||||
|
||||
LOG.info(_LI('Starting server in PID %s') % os.getpid())
|
||||
LOG.debug("Configuration:")
|
||||
cfg.CONF.log_opt_values(LOG, std_logging.DEBUG)
|
||||
|
||||
server = DecisionEngineManager()
|
||||
server.connect()
|
||||
server.join()
|
||||
0
watcher/common/__init__.py
Normal file
0
watcher/common/__init__.py
Normal file
30
watcher/common/config.py
Normal file
30
watcher/common/config.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# Copyright 2010 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# All Rights Reserved.
|
||||
# Copyright 2012 Red Hat, Inc.
|
||||
#
|
||||
# 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.common import rpc
|
||||
from watcher import version
|
||||
|
||||
|
||||
def parse_args(argv, default_config_files=None):
|
||||
rpc.set_defaults(control_exchange='watcher')
|
||||
cfg.CONF(argv[1:],
|
||||
project='watcher',
|
||||
version=version.version_info.release_string(),
|
||||
default_config_files=default_config_files)
|
||||
rpc.init(cfg.CONF)
|
||||
72
watcher/common/context.py
Normal file
72
watcher/common/context.py
Normal file
@@ -0,0 +1,72 @@
|
||||
# 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_context import context
|
||||
|
||||
|
||||
class RequestContext(context.RequestContext):
|
||||
"""Extends security contexts from the OpenStack common library."""
|
||||
|
||||
def __init__(self, auth_token=None, auth_url=None, domain_id=None,
|
||||
domain_name=None, user=None, user_id=None, project=None,
|
||||
project_id=None, is_admin=False, is_public_api=False,
|
||||
read_only=False, show_deleted=False, request_id=None,
|
||||
trust_id=None, auth_token_info=None):
|
||||
"""Stores several additional request parameters:
|
||||
|
||||
:param domain_id: The ID of the domain.
|
||||
:param domain_name: The name of the domain.
|
||||
:param is_public_api: Specifies whether the request should be processed
|
||||
without authentication.
|
||||
|
||||
"""
|
||||
self.is_public_api = is_public_api
|
||||
self.user_id = user_id
|
||||
self.project = project
|
||||
self.project_id = project_id
|
||||
self.domain_id = domain_id
|
||||
self.domain_name = domain_name
|
||||
self.auth_url = auth_url
|
||||
self.auth_token_info = auth_token_info
|
||||
self.trust_id = trust_id
|
||||
|
||||
super(RequestContext, self).__init__(auth_token=auth_token,
|
||||
user=user, tenant=project,
|
||||
is_admin=is_admin,
|
||||
read_only=read_only,
|
||||
show_deleted=show_deleted,
|
||||
request_id=request_id)
|
||||
|
||||
def to_dict(self):
|
||||
return {'auth_token': self.auth_token,
|
||||
'auth_url': self.auth_url,
|
||||
'domain_id': self.domain_id,
|
||||
'domain_name': self.domain_name,
|
||||
'user': self.user,
|
||||
'user_id': self.user_id,
|
||||
'project': self.project,
|
||||
'project_id': self.project_id,
|
||||
'is_admin': self.is_admin,
|
||||
'is_public_api': self.is_public_api,
|
||||
'read_only': self.read_only,
|
||||
'show_deleted': self.show_deleted,
|
||||
'request_id': self.request_id,
|
||||
'trust_id': self.trust_id,
|
||||
'auth_token_info': self.auth_token_info}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, values):
|
||||
return cls(**values)
|
||||
|
||||
|
||||
def make_context(*args, **kwargs):
|
||||
return RequestContext(*args, **kwargs)
|
||||
253
watcher/common/exception.py
Normal file
253
watcher/common/exception.py
Normal file
@@ -0,0 +1,253 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Watcher base exception handling.
|
||||
|
||||
Includes decorator for re-raising Watcher-type exceptions.
|
||||
|
||||
SHOULD include dedicated exception logging.
|
||||
|
||||
"""
|
||||
|
||||
from oslo_config import cfg
|
||||
import six
|
||||
|
||||
from watcher.common.i18n import _
|
||||
from watcher.common.i18n import _LE
|
||||
from watcher.openstack.common import log as logging
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
exc_log_opts = [
|
||||
cfg.BoolOpt('fatal_exception_format_errors',
|
||||
default=False,
|
||||
help='Make exception message format errors fatal.'),
|
||||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(exc_log_opts)
|
||||
|
||||
|
||||
def _cleanse_dict(original):
|
||||
"""Strip all admin_password, new_pass, rescue_pass keys from a dict."""
|
||||
return dict((k, v) for k, v in original.iteritems() if "_pass" not in k)
|
||||
|
||||
|
||||
class WatcherException(Exception):
|
||||
"""Base Watcher Exception
|
||||
|
||||
To correctly use this class, inherit from it and define
|
||||
a 'message' property. That message will get printf'd
|
||||
with the keyword arguments provided to the constructor.
|
||||
|
||||
"""
|
||||
message = _("An unknown exception occurred.")
|
||||
code = 500
|
||||
headers = {}
|
||||
safe = False
|
||||
|
||||
def __init__(self, message=None, **kwargs):
|
||||
self.kwargs = kwargs
|
||||
|
||||
if 'code' not in self.kwargs:
|
||||
try:
|
||||
self.kwargs['code'] = self.code
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
if not message:
|
||||
try:
|
||||
message = self.message % kwargs
|
||||
|
||||
except Exception as e:
|
||||
# kwargs doesn't match a variable in the message
|
||||
# log the issue and the kwargs
|
||||
LOG.exception(_LE('Exception in string format operation'))
|
||||
for name, value in kwargs.iteritems():
|
||||
LOG.error("%s: %s" % (name, value))
|
||||
|
||||
if CONF.fatal_exception_format_errors:
|
||||
raise e
|
||||
else:
|
||||
# at least get the core message out if something happened
|
||||
message = self.message
|
||||
|
||||
super(WatcherException, self).__init__(message)
|
||||
|
||||
def __str__(self):
|
||||
"""Encode to utf-8 then wsme api can consume it as well."""
|
||||
if not six.PY3:
|
||||
return unicode(self.args[0]).encode('utf-8')
|
||||
else:
|
||||
return self.args[0]
|
||||
|
||||
def __unicode__(self):
|
||||
return self.message
|
||||
|
||||
def format_message(self):
|
||||
if self.__class__.__name__.endswith('_Remote'):
|
||||
return self.args[0]
|
||||
else:
|
||||
return six.text_type(self)
|
||||
|
||||
|
||||
class NotAuthorized(WatcherException):
|
||||
message = _("Not authorized.")
|
||||
code = 403
|
||||
|
||||
|
||||
class OperationNotPermitted(NotAuthorized):
|
||||
message = _("Operation not permitted.")
|
||||
|
||||
|
||||
class Invalid(WatcherException):
|
||||
message = _("Unacceptable parameters.")
|
||||
code = 400
|
||||
|
||||
|
||||
class ObjectNotFound(WatcherException):
|
||||
message = _("The %(name)s %(id)s could not be found.")
|
||||
|
||||
|
||||
class Conflict(WatcherException):
|
||||
message = _('Conflict.')
|
||||
code = 409
|
||||
|
||||
|
||||
class ResourceNotFound(ObjectNotFound):
|
||||
message = _("The %(name)s resource %(id)s could not be found.")
|
||||
code = 404
|
||||
|
||||
|
||||
class InvalidIdentity(Invalid):
|
||||
message = _("Expected an uuid or int but received %(identity)s.")
|
||||
|
||||
|
||||
class InvalidGoal(Invalid):
|
||||
message = _("Goal %(goal)s is not defined in Watcher configuration file.")
|
||||
|
||||
|
||||
# Cannot be templated as the error syntax varies.
|
||||
# msg needs to be constructed when raised.
|
||||
class InvalidParameterValue(Invalid):
|
||||
message = _("%(err)s")
|
||||
|
||||
|
||||
class InvalidUUID(Invalid):
|
||||
message = _("Expected a uuid but received %(uuid)s.")
|
||||
|
||||
|
||||
class InvalidName(Invalid):
|
||||
message = _("Expected a logical name but received %(name)s.")
|
||||
|
||||
|
||||
class InvalidUuidOrName(Invalid):
|
||||
message = _("Expected a logical name or uuid but received %(name)s.")
|
||||
|
||||
|
||||
class AuditTemplateNotFound(ResourceNotFound):
|
||||
message = _("AuditTemplate %(audit_template)s could not be found.")
|
||||
|
||||
|
||||
class AuditTemplateAlreadyExists(Conflict):
|
||||
message = _("An audit_template with UUID %(uuid)s or name %(name)s "
|
||||
"already exists.")
|
||||
|
||||
|
||||
class AuditTemplateReferenced(Invalid):
|
||||
message = _("AuditTemplate %(audit_template)s is referenced by one or "
|
||||
"multiple audit.")
|
||||
|
||||
|
||||
class AuditNotFound(ResourceNotFound):
|
||||
message = _("Audit %(audit)s could not be found.")
|
||||
|
||||
|
||||
class AuditAlreadyExists(Conflict):
|
||||
message = _("An audit with UUID %(uuid)s already exists.")
|
||||
|
||||
|
||||
class AuditReferenced(Invalid):
|
||||
message = _("Audit %(audit)s is referenced by one or multiple action "
|
||||
"plans.")
|
||||
|
||||
|
||||
class ActionPlanNotFound(ResourceNotFound):
|
||||
message = _("ActionPlan %(action plan)s could not be found.")
|
||||
|
||||
|
||||
class ActionPlanAlreadyExists(Conflict):
|
||||
message = _("An action plan with UUID %(uuid)s already exists.")
|
||||
|
||||
|
||||
class ActionPlanReferenced(Invalid):
|
||||
message = _("Action Plan %(action_plan)s is referenced by one or "
|
||||
"multiple actions.")
|
||||
|
||||
|
||||
class ActionNotFound(ResourceNotFound):
|
||||
message = _("Action %(action)s could not be found.")
|
||||
|
||||
|
||||
class ActionAlreadyExists(Conflict):
|
||||
message = _("An action with UUID %(uuid)s already exists.")
|
||||
|
||||
|
||||
class ActionReferenced(Invalid):
|
||||
message = _("Action plan %(action_plan)s is referenced by one or "
|
||||
"multiple goals.")
|
||||
|
||||
|
||||
class ActionFilterCombinationProhibited(Invalid):
|
||||
message = _("Filtering actions on both audit and action-plan is "
|
||||
"prohibited.")
|
||||
|
||||
|
||||
class HTTPNotFound(ResourceNotFound):
|
||||
pass
|
||||
|
||||
|
||||
class PatchError(Invalid):
|
||||
message = _("Couldn't apply patch '%(patch)s'. Reason: %(reason)s")
|
||||
|
||||
|
||||
# decision engine
|
||||
|
||||
class ClusterEmpty(WatcherException):
|
||||
message = _("The list of hypervisor(s) in the cluster is empty.'")
|
||||
|
||||
|
||||
class MetricCollectorNotDefined(WatcherException):
|
||||
message = _("The metrics resource collector is not defined.'")
|
||||
|
||||
|
||||
class ClusteStateNotDefined(WatcherException):
|
||||
message = _("the cluster state is not defined")
|
||||
|
||||
|
||||
# Model
|
||||
|
||||
class VMNotFound(WatcherException):
|
||||
message = _("The VM could not be found.")
|
||||
|
||||
|
||||
class HypervisorNotFound(WatcherException):
|
||||
message = _("The hypervisor could not be found.")
|
||||
|
||||
|
||||
class MetaActionNotFound(WatcherException):
|
||||
message = _("The Meta-Action could not be found.")
|
||||
26
watcher/common/i18n.py
Normal file
26
watcher/common/i18n.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
#
|
||||
# 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 oslo_i18n
|
||||
|
||||
_translators = oslo_i18n.TranslatorFactory(domain='watcher')
|
||||
oslo_i18n.enable_lazy()
|
||||
|
||||
_ = _translators.primary
|
||||
_LI = _translators.log_info
|
||||
_LW = _translators.log_warning
|
||||
_LE = _translators.log_error
|
||||
_LC = _translators.log_critical
|
||||
0
watcher/common/messaging/__init__.py
Normal file
0
watcher/common/messaging/__init__.py
Normal file
0
watcher/common/messaging/events/__init__.py
Normal file
0
watcher/common/messaging/events/__init__.py
Normal file
48
watcher/common/messaging/events/event.py
Normal file
48
watcher/common/messaging/events/event.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
#
|
||||
# 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.
|
||||
|
||||
|
||||
class Event(object):
|
||||
"""Generic event to use with EventDispatcher"""
|
||||
|
||||
def __init__(self, event_type=None, data=None, request_id=None):
|
||||
"""Default constructor
|
||||
|
||||
:param event_type: the type of the event
|
||||
:param data: a dictionary which contains data
|
||||
:param request_id: a string which represent the uuid of the request
|
||||
"""
|
||||
self._type = event_type
|
||||
self._data = data
|
||||
self._request_id = request_id
|
||||
|
||||
def get_type(self):
|
||||
return self._type
|
||||
|
||||
def set_type(self, type):
|
||||
self._type = type
|
||||
|
||||
def get_data(self):
|
||||
return self._data
|
||||
|
||||
def set_data(self, data):
|
||||
self._data = data
|
||||
|
||||
def set_request_id(self, id):
|
||||
self._request_id = id
|
||||
|
||||
def get_request_id(self):
|
||||
return self._request_id
|
||||
78
watcher/common/messaging/events/event_dispatcher.py
Normal file
78
watcher/common/messaging/events/event_dispatcher.py
Normal file
@@ -0,0 +1,78 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
#
|
||||
# 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.decision_engine.framework.messaging.events import Events
|
||||
from watcher.openstack.common import log
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class EventDispatcher(object):
|
||||
"""Generic event dispatcher which listen and dispatch events"""
|
||||
|
||||
def __init__(self):
|
||||
self._events = dict()
|
||||
|
||||
def __del__(self):
|
||||
self._events = None
|
||||
|
||||
def has_listener(self, event_type, listener):
|
||||
"""Return true if listener is register to event_type """
|
||||
# Check for event type and for the listener
|
||||
if event_type in self._events.keys():
|
||||
return listener in self._events[event_type]
|
||||
else:
|
||||
return False
|
||||
|
||||
def dispatch_event(self, event):
|
||||
LOG.debug("dispatch evt : %s" % str(event.get_type()))
|
||||
"""
|
||||
Dispatch an instance of Event class
|
||||
"""
|
||||
if Events.ALL in self._events.keys():
|
||||
listeners = self._events[Events.ALL]
|
||||
for listener in listeners:
|
||||
listener(event)
|
||||
|
||||
# Dispatch the event to all the associated listeners
|
||||
if event.get_type() in self._events.keys():
|
||||
listeners = self._events[event.get_type()]
|
||||
for listener in listeners:
|
||||
listener(event)
|
||||
|
||||
def add_event_listener(self, event_type, listener):
|
||||
"""Add an event listener for an event type"""
|
||||
# Add listener to the event type
|
||||
if not self.has_listener(event_type, listener):
|
||||
listeners = self._events.get(event_type, [])
|
||||
listeners.append(listener)
|
||||
self._events[event_type] = listeners
|
||||
|
||||
def remove_event_listener(self, event_type, listener):
|
||||
"""Remove event listener. """
|
||||
# Remove the listener from the event type
|
||||
if self.has_listener(event_type, listener):
|
||||
listeners = self._events[event_type]
|
||||
|
||||
if len(listeners) == 1:
|
||||
# Only this listener remains so remove the key
|
||||
del self._events[event_type]
|
||||
|
||||
else:
|
||||
# Update listeners chain
|
||||
listeners.remove(listener)
|
||||
self._events[event_type] = listeners
|
||||
109
watcher/common/messaging/messaging_core.py
Normal file
109
watcher/common/messaging/messaging_core.py
Normal file
@@ -0,0 +1,109 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
#
|
||||
# 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.common.messaging.events.event_dispatcher import \
|
||||
EventDispatcher
|
||||
from watcher.common.messaging.messaging_handler import \
|
||||
MessagingHandler
|
||||
from watcher.common.rpc import RequestContextSerializer
|
||||
|
||||
from watcher.objects.base import WatcherObjectSerializer
|
||||
from watcher.openstack.common import log
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
|
||||
WATCHER_MESSAGING_OPTS = [
|
||||
cfg.StrOpt('notifier_driver',
|
||||
default='messaging', help='The name of the driver used by'
|
||||
' oslo messaging'),
|
||||
cfg.StrOpt('executor',
|
||||
default='eventlet', help='The name of a message executor, for'
|
||||
'example: eventlet, blocking'),
|
||||
cfg.StrOpt('protocol',
|
||||
default='rabbit', help='The protocol used by the message'
|
||||
' broker, for example rabbit'),
|
||||
cfg.StrOpt('user',
|
||||
default='guest', help='The username used by the message '
|
||||
'broker'),
|
||||
cfg.StrOpt('password',
|
||||
default='guest', help='The password of user used by the '
|
||||
'message broker'),
|
||||
cfg.StrOpt('host',
|
||||
default='localhost', help='The host where the message broker'
|
||||
'is installed'),
|
||||
cfg.StrOpt('port',
|
||||
default='5672', help='The port used bythe message broker'),
|
||||
cfg.StrOpt('virtual_host',
|
||||
default='', help='The virtual host used by the message '
|
||||
'broker')
|
||||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
opt_group = cfg.OptGroup(name='watcher_messaging',
|
||||
title='Options for the messaging core')
|
||||
CONF.register_group(opt_group)
|
||||
CONF.register_opts(WATCHER_MESSAGING_OPTS, opt_group)
|
||||
|
||||
|
||||
class MessagingCore(EventDispatcher):
|
||||
API_VERSION = '1.0'
|
||||
|
||||
def __init__(self, publisher_id, topic_control, topic_status):
|
||||
EventDispatcher.__init__(self)
|
||||
self.serializer = RequestContextSerializer(WatcherObjectSerializer())
|
||||
self.publisher_id = publisher_id
|
||||
self.topic_control = self.build_topic(topic_control)
|
||||
self.topic_status = self.build_topic(topic_status)
|
||||
|
||||
def build_topic(self, topic_name):
|
||||
return MessagingHandler(self.publisher_id, topic_name, self,
|
||||
self.API_VERSION, self.serializer)
|
||||
|
||||
def connect(self):
|
||||
LOG.debug("connecting to rabbitMQ broker")
|
||||
self.topic_control.start()
|
||||
self.topic_status.start()
|
||||
|
||||
def disconnect(self):
|
||||
LOG.debug("Disconnect to rabbitMQ broker")
|
||||
self.topic_control.stop()
|
||||
self.topic_status.stop()
|
||||
|
||||
def publish_control(self, event, payload):
|
||||
return self.topic_control.publish_event(event, payload)
|
||||
|
||||
def publish_status(self, event, payload, request_id=None):
|
||||
return self.topic_status.publish_event(event, payload, request_id)
|
||||
|
||||
def get_version(self):
|
||||
return self.API_VERSION
|
||||
|
||||
def check_api_version(self, context):
|
||||
api_manager_version = self.client.call(
|
||||
context.to_dict(), 'check_api_version',
|
||||
api_version=self.API_VERSION)
|
||||
return api_manager_version
|
||||
|
||||
def response(self, evt, ctx, message):
|
||||
payload = {
|
||||
'request_id': ctx['request_id'],
|
||||
'msg': message
|
||||
}
|
||||
self.publish_status(evt, payload)
|
||||
107
watcher/common/messaging/messaging_handler.py
Normal file
107
watcher/common/messaging/messaging_handler.py
Normal file
@@ -0,0 +1,107 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
#
|
||||
# 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 eventlet
|
||||
from oslo_config import cfg
|
||||
import oslo_messaging as om
|
||||
from threading import Thread
|
||||
from watcher.common.messaging.utils.transport_url_builder import \
|
||||
TransportUrlBuilder
|
||||
from watcher.common.rpc import JsonPayloadSerializer
|
||||
from watcher.common.rpc import RequestContextSerializer
|
||||
from watcher.openstack.common import log
|
||||
|
||||
eventlet.monkey_patch()
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class MessagingHandler(Thread):
|
||||
def __init__(self, publisher_id, topic_watcher, endpoint, version,
|
||||
serializer=None):
|
||||
Thread.__init__(self)
|
||||
self.__server = None
|
||||
self.__notifier = None
|
||||
self.__endpoints = []
|
||||
self.__topics = []
|
||||
self._publisher_id = publisher_id
|
||||
self._topic_watcher = topic_watcher
|
||||
self.__endpoints.append(endpoint)
|
||||
self.__version = version
|
||||
self.__serializer = serializer
|
||||
|
||||
def add_endpoint(self, endpoint):
|
||||
self.__endpoints.append(endpoint)
|
||||
|
||||
def remove_endpoint(self, endpoint):
|
||||
if endpoint in self.__endpoints:
|
||||
self.__endpoints.remove(endpoint)
|
||||
|
||||
def build_notifier(self):
|
||||
serializer = RequestContextSerializer(JsonPayloadSerializer())
|
||||
return om.Notifier(
|
||||
self.transport,
|
||||
driver=CONF.watcher_messaging.notifier_driver,
|
||||
publisher_id=self._publisher_id,
|
||||
topic=self._topic_watcher,
|
||||
serializer=serializer)
|
||||
|
||||
def build_server(self, targets):
|
||||
|
||||
return om.get_rpc_server(self.transport, targets,
|
||||
self.__endpoints,
|
||||
executor=CONF.
|
||||
watcher_messaging.executor,
|
||||
serializer=self.__serializer)
|
||||
|
||||
def __build_transport_url(self):
|
||||
return TransportUrlBuilder().url
|
||||
|
||||
def __config(self):
|
||||
try:
|
||||
self.transport = om.get_transport(
|
||||
cfg.CONF,
|
||||
url=self.__build_transport_url())
|
||||
self.__notifier = self.build_notifier()
|
||||
if 0 < len(self.__endpoints):
|
||||
targets = om.Target(
|
||||
topic=self._topic_watcher,
|
||||
server=CONF.watcher_messaging.host,
|
||||
version=self.__version)
|
||||
self.__server = self.build_server(targets)
|
||||
else:
|
||||
LOG.warn("you have no defined endpoint, \
|
||||
so you can only publish events")
|
||||
except Exception as e:
|
||||
LOG.error("configure : %s" % str(e.message))
|
||||
|
||||
def run(self):
|
||||
LOG.debug("configure MessagingHandler for %s" % self._topic_watcher)
|
||||
self.__config()
|
||||
if len(self.__endpoints) > 0:
|
||||
LOG.debug("Starting up server")
|
||||
self.__server.start()
|
||||
|
||||
def stop(self):
|
||||
LOG.debug('Stop up server')
|
||||
self.__server.wait()
|
||||
self.__server.stop()
|
||||
|
||||
def publish_event(self, event_type, payload, request_id=None):
|
||||
self.__notifier.info({'version_api': self.__version,
|
||||
'request_id': request_id},
|
||||
{'event_id': event_type}, payload)
|
||||
50
watcher/common/messaging/notification_handler.py
Normal file
50
watcher/common/messaging/notification_handler.py
Normal file
@@ -0,0 +1,50 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
#
|
||||
# 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 eventlet
|
||||
from oslo import messaging
|
||||
|
||||
from watcher.common.messaging.utils.observable import \
|
||||
Observable
|
||||
from watcher.openstack.common import log
|
||||
|
||||
|
||||
eventlet.monkey_patch()
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class NotificationHandler(Observable):
|
||||
def __init__(self, publisher_id):
|
||||
Observable.__init__(self)
|
||||
self.publisher_id = publisher_id
|
||||
|
||||
def info(self, ctx, publisher_id, event_type, payload, metadata):
|
||||
if publisher_id == self.publisher_id:
|
||||
self.set_changed()
|
||||
self.notify(ctx, publisher_id, event_type, metadata, payload)
|
||||
return messaging.NotificationResult.HANDLED
|
||||
|
||||
def warn(self, ctx, publisher_id, event_type, payload, metadata):
|
||||
if publisher_id == self.publisher_id:
|
||||
self.set_changed()
|
||||
self.notify(ctx, publisher_id, event_type, metadata, payload)
|
||||
return messaging.NotificationResult.HANDLED
|
||||
|
||||
def error(self, ctx, publisher_id, event_type, payload, metadata):
|
||||
if publisher_id == self.publisher_id:
|
||||
self.set_changed()
|
||||
self.notify(ctx, publisher_id, event_type, metadata, payload)
|
||||
return messaging.NotificationResult.HANDLED
|
||||
0
watcher/common/messaging/utils/__init__.py
Normal file
0
watcher/common/messaging/utils/__init__.py
Normal file
62
watcher/common/messaging/utils/observable.py
Normal file
62
watcher/common/messaging/utils/observable.py
Normal file
@@ -0,0 +1,62 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
#
|
||||
# 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.common.messaging.utils.synchronization import \
|
||||
Synchronization
|
||||
from watcher.openstack.common import log
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class Observable(Synchronization):
|
||||
def __init__(self):
|
||||
self.__observers = []
|
||||
self.changed = 0
|
||||
Synchronization.__init__(self)
|
||||
|
||||
def set_changed(self):
|
||||
self.changed = 1
|
||||
|
||||
def clear_changed(self):
|
||||
self.changed = 0
|
||||
|
||||
def has_changed(self):
|
||||
return self.changed
|
||||
|
||||
def register_observer(self, observer):
|
||||
if observer not in self.__observers:
|
||||
self.__observers.append(observer)
|
||||
|
||||
def unregister_observer(self, observer):
|
||||
try:
|
||||
self.__observers.remove(observer)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def notify(self, ctx=None, publisherid=None, event_type=None,
|
||||
metadata=None, payload=None, modifier=None):
|
||||
self.mutex.acquire()
|
||||
try:
|
||||
if not self.changed:
|
||||
return
|
||||
for observer in self.__observers:
|
||||
if modifier != observer:
|
||||
observer.update(self, ctx, metadata, publisherid,
|
||||
event_type, payload)
|
||||
self.clear_changed()
|
||||
finally:
|
||||
self.mutex.release()
|
||||
22
watcher/common/messaging/utils/synchronization.py
Normal file
22
watcher/common/messaging/utils/synchronization.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
#
|
||||
# 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 threading
|
||||
|
||||
|
||||
class Synchronization(object):
|
||||
def __init__(self):
|
||||
self.mutex = threading.RLock()
|
||||
35
watcher/common/messaging/utils/transport_url_builder.py
Normal file
35
watcher/common/messaging/utils/transport_url_builder.py
Normal file
@@ -0,0 +1,35 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
#
|
||||
# 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.openstack.common import log
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class TransportUrlBuilder(object):
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return "%s://%s:%s@%s:%s/%s" % (
|
||||
CONF.watcher_messaging.protocol,
|
||||
CONF.watcher_messaging.user,
|
||||
CONF.watcher_messaging.password,
|
||||
CONF.watcher_messaging.host,
|
||||
CONF.watcher_messaging.port,
|
||||
CONF.watcher_messaging.virtual_host
|
||||
)
|
||||
66
watcher/common/paths.py
Normal file
66
watcher/common/paths.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# Copyright 2010 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# All Rights Reserved.
|
||||
# Copyright 2012 Red Hat, Inc.
|
||||
#
|
||||
# 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 os
|
||||
|
||||
from oslo_config import cfg
|
||||
|
||||
PATH_OPTS = [
|
||||
cfg.StrOpt('pybasedir',
|
||||
default=os.path.abspath(os.path.join(os.path.dirname(__file__),
|
||||
'../')),
|
||||
help='Directory where the watcher python module is installed.'),
|
||||
cfg.StrOpt('bindir',
|
||||
default='$pybasedir/bin',
|
||||
help='Directory where watcher binaries are installed.'),
|
||||
cfg.StrOpt('state_path',
|
||||
default='$pybasedir',
|
||||
help="Top-level directory for maintaining watcher's state."),
|
||||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(PATH_OPTS)
|
||||
|
||||
|
||||
def basedir_def(*args):
|
||||
"""Return an uninterpolated path relative to $pybasedir."""
|
||||
return os.path.join('$pybasedir', *args)
|
||||
|
||||
|
||||
def bindir_def(*args):
|
||||
"""Return an uninterpolated path relative to $bindir."""
|
||||
return os.path.join('$bindir', *args)
|
||||
|
||||
|
||||
def state_path_def(*args):
|
||||
"""Return an uninterpolated path relative to $state_path."""
|
||||
return os.path.join('$state_path', *args)
|
||||
|
||||
|
||||
def basedir_rel(*args):
|
||||
"""Return a path relative to $pybasedir."""
|
||||
return os.path.join(CONF.pybasedir, *args)
|
||||
|
||||
|
||||
def bindir_rel(*args):
|
||||
"""Return a path relative to $bindir."""
|
||||
return os.path.join(CONF.bindir, *args)
|
||||
|
||||
|
||||
def state_path_rel(*args):
|
||||
"""Return a path relative to $state_path."""
|
||||
return os.path.join(CONF.state_path, *args)
|
||||
69
watcher/common/policy.py
Normal file
69
watcher/common/policy.py
Normal file
@@ -0,0 +1,69 @@
|
||||
# Copyright (c) 2011 OpenStack Foundation
|
||||
# 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.
|
||||
|
||||
"""Policy Engine For Watcher."""
|
||||
|
||||
from oslo_concurrency import lockutils
|
||||
from oslo_config import cfg
|
||||
|
||||
from watcher.openstack.common import policy
|
||||
|
||||
_ENFORCER = None
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
@lockutils.synchronized('policy_enforcer', 'watcher-')
|
||||
def init_enforcer(policy_file=None, rules=None,
|
||||
default_rule=None, use_conf=True):
|
||||
"""Synchronously initializes the policy enforcer
|
||||
|
||||
:param policy_file: Custom policy file to use, if none is specified,
|
||||
`CONF.policy_file` will be used.
|
||||
:param rules: Default dictionary / Rules to use. It will be
|
||||
considered just in the first instantiation.
|
||||
:param default_rule: Default rule to use, CONF.default_rule will
|
||||
be used if none is specified.
|
||||
:param use_conf: Whether to load rules from config file.
|
||||
|
||||
"""
|
||||
global _ENFORCER
|
||||
|
||||
if _ENFORCER:
|
||||
return
|
||||
|
||||
_ENFORCER = policy.Enforcer(policy_file=policy_file,
|
||||
rules=rules,
|
||||
default_rule=default_rule,
|
||||
use_conf=use_conf)
|
||||
|
||||
|
||||
def get_enforcer():
|
||||
"""Provides access to the single instance of Policy enforcer."""
|
||||
|
||||
if not _ENFORCER:
|
||||
init_enforcer()
|
||||
|
||||
return _ENFORCER
|
||||
|
||||
|
||||
def enforce(rule, target, creds, do_raise=False, exc=None, *args, **kwargs):
|
||||
"""A shortcut for policy.Enforcer.enforce()
|
||||
|
||||
Checks authorization of a rule against the target and credentials.
|
||||
|
||||
"""
|
||||
enforcer = get_enforcer()
|
||||
return enforcer.enforce(rule, target, creds, do_raise=do_raise,
|
||||
exc=exc, *args, **kwargs)
|
||||
148
watcher/common/rpc.py
Normal file
148
watcher/common/rpc.py
Normal file
@@ -0,0 +1,148 @@
|
||||
# Copyright 2014 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.
|
||||
|
||||
|
||||
from oslo_config import cfg
|
||||
import oslo_messaging as messaging
|
||||
from oslo_serialization import jsonutils
|
||||
|
||||
from watcher.common import context as watcher_context
|
||||
from watcher.common import exception
|
||||
|
||||
__all__ = [
|
||||
'init',
|
||||
'cleanup',
|
||||
'set_defaults',
|
||||
'add_extra_exmods',
|
||||
'clear_extra_exmods',
|
||||
'get_allowed_exmods',
|
||||
'RequestContextSerializer',
|
||||
'get_client',
|
||||
'get_server',
|
||||
'get_notifier',
|
||||
'TRANSPORT_ALIASES',
|
||||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
TRANSPORT = None
|
||||
NOTIFIER = None
|
||||
|
||||
ALLOWED_EXMODS = [
|
||||
exception.__name__,
|
||||
]
|
||||
EXTRA_EXMODS = []
|
||||
|
||||
# NOTE(lucasagomes): The watcher.openstack.common.rpc entries are for
|
||||
# backwards compat with IceHouse rpc_backend configuration values.
|
||||
TRANSPORT_ALIASES = {
|
||||
'watcher.openstack.common.rpc.impl_kombu': 'rabbit',
|
||||
'watcher.openstack.common.rpc.impl_qpid': 'qpid',
|
||||
'watcher.openstack.common.rpc.impl_zmq': 'zmq',
|
||||
'watcher.rpc.impl_kombu': 'rabbit',
|
||||
'watcher.rpc.impl_qpid': 'qpid',
|
||||
'watcher.rpc.impl_zmq': 'zmq',
|
||||
}
|
||||
|
||||
|
||||
def init(conf):
|
||||
global TRANSPORT, NOTIFIER
|
||||
exmods = get_allowed_exmods()
|
||||
TRANSPORT = messaging.get_transport(conf,
|
||||
allowed_remote_exmods=exmods,
|
||||
aliases=TRANSPORT_ALIASES)
|
||||
serializer = RequestContextSerializer(JsonPayloadSerializer())
|
||||
NOTIFIER = messaging.Notifier(TRANSPORT, serializer=serializer)
|
||||
|
||||
|
||||
def cleanup():
|
||||
global TRANSPORT, NOTIFIER
|
||||
assert TRANSPORT is not None
|
||||
assert NOTIFIER is not None
|
||||
TRANSPORT.cleanup()
|
||||
TRANSPORT = NOTIFIER = None
|
||||
|
||||
|
||||
def set_defaults(control_exchange):
|
||||
messaging.set_transport_defaults(control_exchange)
|
||||
|
||||
|
||||
def add_extra_exmods(*args):
|
||||
EXTRA_EXMODS.extend(args)
|
||||
|
||||
|
||||
def clear_extra_exmods():
|
||||
del EXTRA_EXMODS[:]
|
||||
|
||||
|
||||
def get_allowed_exmods():
|
||||
return ALLOWED_EXMODS + EXTRA_EXMODS
|
||||
|
||||
|
||||
class JsonPayloadSerializer(messaging.NoOpSerializer):
|
||||
@staticmethod
|
||||
def serialize_entity(context, entity):
|
||||
return jsonutils.to_primitive(entity, convert_instances=True)
|
||||
|
||||
|
||||
class RequestContextSerializer(messaging.Serializer):
|
||||
|
||||
def __init__(self, base):
|
||||
self._base = base
|
||||
|
||||
def serialize_entity(self, context, entity):
|
||||
if not self._base:
|
||||
return entity
|
||||
return self._base.serialize_entity(context, entity)
|
||||
|
||||
def deserialize_entity(self, context, entity):
|
||||
if not self._base:
|
||||
return entity
|
||||
return self._base.deserialize_entity(context, entity)
|
||||
|
||||
def serialize_context(self, context):
|
||||
return context
|
||||
|
||||
def deserialize_context(self, context):
|
||||
return watcher_context.RequestContext.from_dict(context)
|
||||
|
||||
|
||||
def get_transport_url(url_str=None):
|
||||
return messaging.TransportURL.parse(CONF, url_str, TRANSPORT_ALIASES)
|
||||
|
||||
|
||||
def get_client(target, version_cap=None, serializer=None):
|
||||
assert TRANSPORT is not None
|
||||
serializer = RequestContextSerializer(serializer)
|
||||
return messaging.RPCClient(TRANSPORT,
|
||||
target,
|
||||
version_cap=version_cap,
|
||||
serializer=serializer)
|
||||
|
||||
|
||||
def get_server(target, endpoints, serializer=None):
|
||||
assert TRANSPORT is not None
|
||||
serializer = RequestContextSerializer(serializer)
|
||||
return messaging.get_rpc_server(TRANSPORT,
|
||||
target,
|
||||
endpoints,
|
||||
executor='eventlet',
|
||||
serializer=serializer)
|
||||
|
||||
|
||||
def get_notifier(service=None, host=None, publisher_id=None):
|
||||
assert NOTIFIER is not None
|
||||
if not publisher_id:
|
||||
publisher_id = "%s.%s" % (service, host or CONF.host)
|
||||
return NOTIFIER.prepare(publisher_id=publisher_id)
|
||||
107
watcher/common/rpc_service.py
Normal file
107
watcher/common/rpc_service.py
Normal file
@@ -0,0 +1,107 @@
|
||||
# Copyright 2014 - Rackspace Hosting
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Common RPC service and API tools for Watcher."""
|
||||
|
||||
import eventlet
|
||||
from oslo_config import cfg
|
||||
import oslo_messaging as messaging
|
||||
|
||||
from watcher.common import context as watcher_context
|
||||
from watcher.common import rpc
|
||||
from watcher.objects import base as objects_base
|
||||
|
||||
|
||||
# NOTE(paulczar):
|
||||
# Ubuntu 14.04 forces librabbitmq when kombu is used
|
||||
# Unfortunately it forces a version that has a crash
|
||||
# bug. Calling eventlet.monkey_patch() tells kombu
|
||||
# to use libamqp instead.
|
||||
eventlet.monkey_patch()
|
||||
|
||||
# NOTE(asalkeld):
|
||||
# The watcher.openstack.common.rpc entries are for compatability
|
||||
# with devstack rpc_backend configuration values.
|
||||
TRANSPORT_ALIASES = {
|
||||
'watcher.openstack.common.rpc.impl_kombu': 'rabbit',
|
||||
'watcher.openstack.common.rpc.impl_qpid': 'qpid',
|
||||
'watcher.openstack.common.rpc.impl_zmq': 'zmq',
|
||||
}
|
||||
|
||||
|
||||
class RequestContextSerializer(messaging.Serializer):
|
||||
|
||||
def __init__(self, base):
|
||||
self._base = base
|
||||
|
||||
def serialize_entity(self, context, entity):
|
||||
if not self._base:
|
||||
return entity
|
||||
return self._base.serialize_entity(context, entity)
|
||||
|
||||
def deserialize_entity(self, context, entity):
|
||||
if not self._base:
|
||||
return entity
|
||||
return self._base.deserialize_entity(context, entity)
|
||||
|
||||
def serialize_context(self, context):
|
||||
return context.to_dict()
|
||||
|
||||
def deserialize_context(self, context):
|
||||
return watcher_context.RequestContext.from_dict(context)
|
||||
|
||||
|
||||
class Service(object):
|
||||
_server = None
|
||||
|
||||
def __init__(self, topic, server, handlers):
|
||||
serializer = RequestContextSerializer(
|
||||
objects_base.WatcherObjectSerializer())
|
||||
transport = messaging.get_transport(cfg.CONF,
|
||||
aliases=TRANSPORT_ALIASES)
|
||||
# TODO(asalkeld) add support for version='x.y'
|
||||
target = messaging.Target(topic=topic, server=server)
|
||||
self._server = messaging.get_rpc_server(transport, target, handlers,
|
||||
serializer=serializer)
|
||||
|
||||
def serve(self):
|
||||
self._server.start()
|
||||
self._server.wait()
|
||||
|
||||
|
||||
class API(object):
|
||||
def __init__(self, transport=None, context=None, topic=None):
|
||||
serializer = RequestContextSerializer(
|
||||
objects_base.WatcherObjectSerializer())
|
||||
if transport is None:
|
||||
exmods = rpc.get_allowed_exmods()
|
||||
transport = messaging.get_transport(cfg.CONF,
|
||||
allowed_remote_exmods=exmods,
|
||||
aliases=TRANSPORT_ALIASES)
|
||||
self._context = context
|
||||
if topic is None:
|
||||
topic = ''
|
||||
target = messaging.Target(topic=topic)
|
||||
self._client = messaging.RPCClient(transport, target,
|
||||
serializer=serializer)
|
||||
|
||||
def _call(self, method, *args, **kwargs):
|
||||
# import pdb; pdb.set_trace()
|
||||
return self._client.call(self._context, method, *args, **kwargs)
|
||||
|
||||
def _cast(self, method, *args, **kwargs):
|
||||
self._client.cast(self._context, method, *args, **kwargs)
|
||||
|
||||
def echo(self, message):
|
||||
self._cast('echo', message=message)
|
||||
136
watcher/common/service.py
Normal file
136
watcher/common/service.py
Normal file
@@ -0,0 +1,136 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Copyright © 2012 eNovance <licensing@enovance.com>
|
||||
##
|
||||
# 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 signal
|
||||
import socket
|
||||
|
||||
from oslo import messaging
|
||||
from oslo_config import cfg
|
||||
from oslo_utils import importutils
|
||||
|
||||
from watcher.common import config
|
||||
from watcher.common.i18n import _LE
|
||||
from watcher.common.i18n import _LI
|
||||
from watcher.common import rpc
|
||||
from watcher.objects import base as objects_base
|
||||
from watcher.openstack.common import context
|
||||
from watcher.openstack.common import log
|
||||
from watcher.openstack.common import service
|
||||
|
||||
|
||||
service_opts = [
|
||||
cfg.IntOpt('periodic_interval',
|
||||
default=60,
|
||||
help='Seconds between running periodic tasks.'),
|
||||
cfg.StrOpt('host',
|
||||
default=socket.getfqdn(),
|
||||
help='Name of this node. This can be an opaque identifier. '
|
||||
'It is not necessarily a hostname, FQDN, or IP address. '
|
||||
'However, the node name must be valid within '
|
||||
'an AMQP key, and if using ZeroMQ, a valid '
|
||||
'hostname, FQDN, or IP address.'),
|
||||
]
|
||||
|
||||
cfg.CONF.register_opts(service_opts)
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class RPCService(service.Service):
|
||||
|
||||
def __init__(self, host, manager_module, manager_class):
|
||||
super(RPCService, self).__init__()
|
||||
self.host = host
|
||||
manager_module = importutils.try_import(manager_module)
|
||||
manager_class = getattr(manager_module, manager_class)
|
||||
self.manager = manager_class(host, manager_module.MANAGER_TOPIC)
|
||||
self.topic = self.manager.topic
|
||||
self.rpcserver = None
|
||||
self.deregister = True
|
||||
|
||||
def start(self):
|
||||
super(RPCService, self).start()
|
||||
admin_context = context.RequestContext('admin', 'admin', is_admin=True)
|
||||
|
||||
target = messaging.Target(topic=self.topic, server=self.host)
|
||||
endpoints = [self.manager]
|
||||
serializer = objects_base.IronicObjectSerializer()
|
||||
self.rpcserver = rpc.get_server(target, endpoints, serializer)
|
||||
self.rpcserver.start()
|
||||
|
||||
self.handle_signal()
|
||||
self.manager.init_host()
|
||||
self.tg.add_dynamic_timer(
|
||||
self.manager.periodic_tasks,
|
||||
periodic_interval_max=cfg.CONF.periodic_interval,
|
||||
context=admin_context)
|
||||
|
||||
LOG.info(_LI('Created RPC server for service %(service)s on host '
|
||||
'%(host)s.'),
|
||||
{'service': self.topic, 'host': self.host})
|
||||
|
||||
def stop(self):
|
||||
try:
|
||||
self.rpcserver.stop()
|
||||
self.rpcserver.wait()
|
||||
except Exception as e:
|
||||
LOG.exception(_LE('Service error occurred when stopping the '
|
||||
'RPC server. Error: %s'), e)
|
||||
try:
|
||||
self.manager.del_host(deregister=self.deregister)
|
||||
except Exception as e:
|
||||
LOG.exception(_LE('Service error occurred when cleaning up '
|
||||
'the RPC manager. Error: %s'), e)
|
||||
|
||||
super(RPCService, self).stop(graceful=True)
|
||||
LOG.info(_LI('Stopped RPC server for service %(service)s on host '
|
||||
'%(host)s.'),
|
||||
{'service': self.topic, 'host': self.host})
|
||||
|
||||
def _handle_signal(self, signo, frame):
|
||||
LOG.info(_LI('Got signal SIGUSR1. Not deregistering on next shutdown '
|
||||
'of service %(service)s on host %(host)s.'),
|
||||
{'service': self.topic, 'host': self.host})
|
||||
self.deregister = False
|
||||
|
||||
def handle_signal(self):
|
||||
"""Add a signal handler for SIGUSR1.
|
||||
|
||||
The handler ensures that the manager is not deregistered when it is
|
||||
shutdown.
|
||||
"""
|
||||
signal.signal(signal.SIGUSR1, self._handle_signal)
|
||||
|
||||
|
||||
def prepare_service(argv=[]):
|
||||
config.parse_args(argv)
|
||||
cfg.set_defaults(log.log_opts,
|
||||
default_log_levels=['amqp=WARN',
|
||||
'amqplib=WARN',
|
||||
'qpid.messaging=INFO',
|
||||
'oslo.messaging=INFO',
|
||||
'sqlalchemy=WARN',
|
||||
'keystoneclient=INFO',
|
||||
'stevedore=INFO',
|
||||
'eventlet.wsgi.server=WARN',
|
||||
'iso8601=WARN',
|
||||
'paramiko=WARN',
|
||||
'requests=WARN',
|
||||
'neutronclient=WARN',
|
||||
'glanceclient=WARN',
|
||||
'watcher.openstack.common=WARN',
|
||||
])
|
||||
log.setup('watcher')
|
||||
99
watcher/common/utils.py
Normal file
99
watcher/common/utils.py
Normal file
@@ -0,0 +1,99 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Utilities and helper functions."""
|
||||
|
||||
from oslo_config import cfg
|
||||
|
||||
import re
|
||||
import six
|
||||
import uuid
|
||||
|
||||
|
||||
from watcher.common.i18n import _LW
|
||||
from watcher.openstack.common import log as logging
|
||||
|
||||
UTILS_OPTS = [
|
||||
cfg.StrOpt('rootwrap_config',
|
||||
default="/etc/watcher/rootwrap.conf",
|
||||
help='Path to the rootwrap configuration file to use for '
|
||||
'running commands as root.'),
|
||||
cfg.StrOpt('tempdir',
|
||||
help='Explicitly specify the temporary working directory.'),
|
||||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(UTILS_OPTS)
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def safe_rstrip(value, chars=None):
|
||||
"""Removes trailing characters from a string if that does not make it empty
|
||||
|
||||
:param value: A string value that will be stripped.
|
||||
:param chars: Characters to remove.
|
||||
:return: Stripped value.
|
||||
|
||||
"""
|
||||
if not isinstance(value, six.string_types):
|
||||
LOG.warn(_LW("Failed to remove trailing character. Returning original "
|
||||
"object. Supplied object is not a string: %s,"), value)
|
||||
return value
|
||||
|
||||
return value.rstrip(chars) or value
|
||||
|
||||
|
||||
def generate_uuid():
|
||||
return str(uuid.uuid4())
|
||||
|
||||
|
||||
def is_int_like(val):
|
||||
"""Check if a value looks like an int."""
|
||||
try:
|
||||
return str(int(val)) == str(val)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def is_uuid_like(val):
|
||||
"""Returns validation of a value as a UUID.
|
||||
|
||||
For our purposes, a UUID is a canonical form string:
|
||||
aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa
|
||||
|
||||
"""
|
||||
try:
|
||||
return str(uuid.UUID(val)) == val
|
||||
except (TypeError, ValueError, AttributeError):
|
||||
return False
|
||||
|
||||
|
||||
def is_hostname_safe(hostname):
|
||||
"""Determine if the supplied hostname is RFC compliant.
|
||||
|
||||
Check that the supplied hostname conforms to:
|
||||
* http://en.wikipedia.org/wiki/Hostname
|
||||
* http://tools.ietf.org/html/rfc952
|
||||
* http://tools.ietf.org/html/rfc1123
|
||||
|
||||
:param hostname: The hostname to be validated.
|
||||
:returns: True if valid. False if not.
|
||||
|
||||
"""
|
||||
m = '^[a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?$'
|
||||
return (isinstance(hostname, six.string_types) and
|
||||
(re.match(m, hostname) is not None))
|
||||
0
watcher/contrib/tempest/tempest/__init__.py
Normal file
0
watcher/contrib/tempest/tempest/__init__.py
Normal file
25
watcher/contrib/tempest/tempest/api/infra_optim/README.rst
Normal file
25
watcher/contrib/tempest/tempest/api/infra_optim/README.rst
Normal file
@@ -0,0 +1,25 @@
|
||||
Tempest Field Guide to Infrastructure Optimization API tests
|
||||
============================================================
|
||||
|
||||
|
||||
What are these tests?
|
||||
---------------------
|
||||
|
||||
These tests stress the OpenStack Infrastructure Optimization API provided by
|
||||
Watcher.
|
||||
|
||||
|
||||
Why are these tests in tempest?
|
||||
------------------------------
|
||||
|
||||
The purpose of these tests is to exercise the various APIs provided by Watcher
|
||||
for optimizing the infrastructure.
|
||||
|
||||
|
||||
Scope of these tests
|
||||
--------------------
|
||||
|
||||
The Infrastructure Optimization API test perform basic CRUD operations on the Watcher node
|
||||
inventory. They do not actually perform placement or migration of virtual resources. It is important
|
||||
to note that all Watcher API actions are admin operations meant to be used
|
||||
either by cloud operators.
|
||||
133
watcher/contrib/tempest/tempest/api/infra_optim/admin/base.py
Normal file
133
watcher/contrib/tempest/tempest/api/infra_optim/admin/base.py
Normal file
@@ -0,0 +1,133 @@
|
||||
# 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 functools
|
||||
|
||||
from tempest_lib.common.utils import data_utils
|
||||
from tempest_lib import exceptions as lib_exc
|
||||
|
||||
from tempest import clients_infra_optim as clients
|
||||
from tempest.common import credentials
|
||||
from tempest import config
|
||||
from tempest import test
|
||||
|
||||
CONF = config.CONF
|
||||
|
||||
|
||||
# Resources must be deleted in a specific order, this list
|
||||
# defines the resource types to clean up, and the correct order.
|
||||
RESOURCE_TYPES = ['audit_template']
|
||||
# RESOURCE_TYPES = ['action', 'action_plan', 'audit', 'audit_template']
|
||||
|
||||
|
||||
def creates(resource):
|
||||
"""Decorator that adds resources to the appropriate cleanup list."""
|
||||
|
||||
def decorator(f):
|
||||
@functools.wraps(f)
|
||||
def wrapper(cls, *args, **kwargs):
|
||||
resp, body = f(cls, *args, **kwargs)
|
||||
|
||||
if 'uuid' in body:
|
||||
cls.created_objects[resource].add(body['uuid'])
|
||||
|
||||
return resp, body
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
class BaseInfraOptimTest(test.BaseTestCase):
|
||||
"""Base class for Infrastructure Optimization API tests."""
|
||||
|
||||
@classmethod
|
||||
# def skip_checks(cls):
|
||||
# super(BaseInfraOptimTest, cls).skip_checks()
|
||||
# if not CONF.service_available.watcher:
|
||||
# skip_msg = \
|
||||
# ('%s skipped as Watcher is not available' % cls.__name__)
|
||||
# raise cls.skipException(skip_msg)
|
||||
@classmethod
|
||||
def setup_credentials(cls):
|
||||
super(BaseInfraOptimTest, cls).setup_credentials()
|
||||
if (not hasattr(cls, 'isolated_creds') or
|
||||
not cls.isolated_creds.name == cls.__name__):
|
||||
cls.isolated_creds = credentials.get_isolated_credentials(
|
||||
name=cls.__name__, network_resources=cls.network_resources)
|
||||
cls.mgr = clients.Manager(cls.isolated_creds.get_admin_creds())
|
||||
|
||||
@classmethod
|
||||
def setup_clients(cls):
|
||||
super(BaseInfraOptimTest, cls).setup_clients()
|
||||
cls.client = cls.mgr.io_client
|
||||
|
||||
@classmethod
|
||||
def resource_setup(cls):
|
||||
super(BaseInfraOptimTest, cls).resource_setup()
|
||||
|
||||
cls.created_objects = {}
|
||||
for resource in RESOURCE_TYPES:
|
||||
cls.created_objects[resource] = set()
|
||||
|
||||
@classmethod
|
||||
def resource_cleanup(cls):
|
||||
"""Ensure that all created objects get destroyed."""
|
||||
|
||||
try:
|
||||
for resource in RESOURCE_TYPES:
|
||||
uuids = cls.created_objects[resource]
|
||||
delete_method = getattr(cls.client, 'delete_%s' % resource)
|
||||
for u in uuids:
|
||||
delete_method(u, ignore_errors=lib_exc.NotFound)
|
||||
finally:
|
||||
super(BaseInfraOptimTest, cls).resource_cleanup()
|
||||
|
||||
@classmethod
|
||||
@creates('audit_template')
|
||||
def create_audit_template(cls, description=None, expect_errors=False):
|
||||
"""
|
||||
Wrapper utility for creating test audit_template.
|
||||
|
||||
:param description: A description of the audit template.
|
||||
if not supplied, a random value will be generated.
|
||||
:return: Created audit template.
|
||||
|
||||
"""
|
||||
description = description or data_utils.rand_name(
|
||||
'test-audit_template')
|
||||
resp, body = cls.client.create_audit_template(description=description)
|
||||
return resp, body
|
||||
|
||||
@classmethod
|
||||
def delete_audit_template(cls, audit_template_id):
|
||||
"""
|
||||
Deletes a audit_template having the specified UUID.
|
||||
|
||||
:param uuid: The unique identifier of the audit_template.
|
||||
:return: Server response.
|
||||
|
||||
"""
|
||||
|
||||
resp, body = cls.client.delete_audit_template(audit_template_id)
|
||||
|
||||
if audit_template_id in cls.created_objects['audit_template']:
|
||||
cls.created_objects['audit_template'].remove(audit_template_id)
|
||||
|
||||
return resp
|
||||
|
||||
def validate_self_link(self, resource, uuid, link):
|
||||
"""Check whether the given self link formatted correctly."""
|
||||
expected_link = "{base}/{pref}/{res}/{uuid}".format(
|
||||
base=self.client.base_url,
|
||||
pref=self.client.uri_prefix,
|
||||
res=resource,
|
||||
uuid=uuid)
|
||||
self.assertEqual(expected_link, link)
|
||||
@@ -0,0 +1,42 @@
|
||||
# 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 tempest.api.infra_optim.admin import base
|
||||
from tempest import test
|
||||
|
||||
|
||||
class TestApiDiscovery(base.BaseInfraOptimTest):
|
||||
"""Tests for API discovery features."""
|
||||
|
||||
@test.attr(type='smoke')
|
||||
def test_api_versions(self):
|
||||
_, descr = self.client.get_api_description()
|
||||
expected_versions = ('v1',)
|
||||
versions = [version['id'] for version in descr['versions']]
|
||||
|
||||
for v in expected_versions:
|
||||
self.assertIn(v, versions)
|
||||
|
||||
@test.attr(type='smoke')
|
||||
def test_default_version(self):
|
||||
_, descr = self.client.get_api_description()
|
||||
default_version = descr['default_version']
|
||||
self.assertEqual(default_version['id'], 'v1')
|
||||
|
||||
@test.attr(type='smoke')
|
||||
def test_version_1_resources(self):
|
||||
_, descr = self.client.get_version_description(version='v1')
|
||||
expected_resources = ('audit_templates', 'audits', 'action_plans',
|
||||
'actions', 'links', 'media_types')
|
||||
|
||||
for res in expected_resources:
|
||||
self.assertIn(res, descr)
|
||||
@@ -0,0 +1,236 @@
|
||||
# -*- coding: 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 tempest_lib import exceptions as lib_exc
|
||||
|
||||
from tempest.api.infra_optim.admin import base
|
||||
from tempest import test
|
||||
|
||||
|
||||
class TestAuditTemplate(base.BaseInfraOptimTest):
|
||||
"""Tests for audit_template."""
|
||||
|
||||
@classmethod
|
||||
def resource_setup(cls):
|
||||
super(TestAuditTemplate, cls).resource_setup()
|
||||
_, cls.audit_template = cls.create_audit_template()
|
||||
|
||||
def _assertExpected(self, expected, actual):
|
||||
# Check if not expected keys/values exists in actual response body
|
||||
for key, value in expected.iteritems():
|
||||
if key not in ('created_at', 'updated_at', 'deleted_at'):
|
||||
self.assertIn(key, actual)
|
||||
self.assertEqual(value, actual[key])
|
||||
|
||||
@test.attr(type='smoke')
|
||||
def test_create_audit_template(self):
|
||||
params = {'name': 'my at name',
|
||||
'description': 'my at description',
|
||||
'host_aggregate': 12,
|
||||
'goal': 'A GOAL',
|
||||
'extra': {'str': 'value', 'int': 123, 'float': 0.123,
|
||||
'bool': True, 'list': [1, 2, 3],
|
||||
'dict': {'foo': 'bar'}}}
|
||||
|
||||
_, body = self.create_audit_template(**params)
|
||||
self._assertExpected(params, body['properties'])
|
||||
|
||||
_, audit_template = self.client.show_audit_template(body['uuid'])
|
||||
self._assertExpected(audit_template, body)
|
||||
|
||||
@test.attr(type='smoke')
|
||||
def test_create_audit_template_unicode_description(self):
|
||||
# Use a unicode string for testing:
|
||||
params = {'name': 'my at name',
|
||||
'description': 'my àt déscrïptïôn',
|
||||
'host_aggregate': 12,
|
||||
'goal': 'A GOAL',
|
||||
'extra': {'foo': 'bar'}}
|
||||
|
||||
_, body = self.create_audit_template(**params)
|
||||
self._assertExpected(params, body['properties'])
|
||||
|
||||
_, audit_template = self.client.show_audit_template(body['uuid'])
|
||||
self._assertExpected(audit_template, body)
|
||||
|
||||
@test.attr(type='smoke')
|
||||
def test_show_audit_template(self):
|
||||
_, audit_template = self.client.show_audit_template(
|
||||
self.audit_template['uuid'])
|
||||
self._assertExpected(self.audit_template, audit_template)
|
||||
|
||||
@test.attr(type='smoke')
|
||||
def test_show_audit_template_by_goal(self):
|
||||
_, audit_template = self.client.\
|
||||
show_audit_template_by_goal(self.audit_template['goal'])
|
||||
self._assertExpected(self.audit_template,
|
||||
audit_template['audit_templates'][0])
|
||||
|
||||
@test.attr(type='smoke')
|
||||
def test_show_audit_template_by_host_aggregate(self):
|
||||
_, audit_template = self.client.\
|
||||
show_audit_template_by_host_aggregate(
|
||||
self.audit_template['host_aggregate'])
|
||||
self._assertExpected(self.audit_template,
|
||||
audit_template['audit_templates'][0])
|
||||
|
||||
@test.attr(type='smoke')
|
||||
def test_show_audit_template_with_links(self):
|
||||
_, audit_template = self.client.show_audit_template(
|
||||
self.audit_template['uuid'])
|
||||
self.assertIn('links', audit_template.keys())
|
||||
self.assertEqual(2, len(audit_template['links']))
|
||||
self.assertIn(audit_template['uuid'],
|
||||
audit_template['links'][0]['href'])
|
||||
|
||||
@test.attr(type="smoke")
|
||||
def test_list_audit_templates(self):
|
||||
_, body = self.client.list_audit_templates()
|
||||
self.assertIn(self.audit_template['uuid'],
|
||||
[i['uuid'] for i in body['audit_templates']])
|
||||
# Verify self links.
|
||||
for audit_template in body['audit_templates']:
|
||||
self.validate_self_link('audit_templates', audit_template['uuid'],
|
||||
audit_template['links'][0]['href'])
|
||||
|
||||
@test.attr(type='smoke')
|
||||
def test_list_with_limit(self):
|
||||
_, body = self.client.list_audit_templates(limit=3)
|
||||
|
||||
next_marker = body['audit_templates'][-1]['uuid']
|
||||
self.assertIn(next_marker, body['next'])
|
||||
|
||||
@test.attr(type='smoke')
|
||||
def test_delete_audit_template(self):
|
||||
_, body = self.create_audit_template()
|
||||
uuid = body['uuid']
|
||||
|
||||
self.delete_audit_template(uuid)
|
||||
self.assertRaises(lib_exc.NotFound, self.client.show_audit_template,
|
||||
uuid)
|
||||
|
||||
@test.attr(type='smoke')
|
||||
def test_update_audit_template_replace(self):
|
||||
params = {'name': 'my at name',
|
||||
'description': 'my at description',
|
||||
'host_aggregate': 12,
|
||||
'goal': 'A GOAL',
|
||||
'extra': {'key1': 'value1', 'key2': 'value2'}}
|
||||
|
||||
_, body = self.create_audit_template(**params)
|
||||
|
||||
new_name = 'my at new name'
|
||||
new_description = 'my new at description'
|
||||
new_host_aggregate = 10
|
||||
new_goal = 'A NEW GOAL'
|
||||
new_extra = {'key1': 'new-value1', 'key2': 'new-value2'}
|
||||
|
||||
patch = [{'path': '/name',
|
||||
'op': 'replace',
|
||||
'value': new_name},
|
||||
{'path': '/description',
|
||||
'op': 'replace',
|
||||
'value': new_description},
|
||||
{'path': '/host_aggregate',
|
||||
'op': 'replace',
|
||||
'value': new_host_aggregate},
|
||||
{'path': '/goal',
|
||||
'op': 'replace',
|
||||
'value': new_goal},
|
||||
{'path': '/extra/key1',
|
||||
'op': 'replace',
|
||||
'value': new_extra['key1']},
|
||||
{'path': '/extra/key2',
|
||||
'op': 'replace',
|
||||
'value': new_extra['key2']}]
|
||||
|
||||
self.client.update_audit_template(body['uuid'], patch)
|
||||
|
||||
_, body = self.client.show_audit_template(body['uuid'])
|
||||
self.assertEqual(new_name, body['name'])
|
||||
self.assertEqual(new_description, body['description'])
|
||||
self.assertEqual(new_host_aggregate, body['host_aggregate'])
|
||||
self.assertEqual(new_goal, body['goal'])
|
||||
self.assertEqual(new_extra, body['extra'])
|
||||
|
||||
@test.attr(type='smoke')
|
||||
def test_update_audit_template_remove(self):
|
||||
extra = {'key1': 'value1', 'key2': 'value2', 'key3': 'value3'}
|
||||
description = 'my at description'
|
||||
goal = 'A GOAL'
|
||||
name = 'my at name'
|
||||
params = {'name': name,
|
||||
'description': description,
|
||||
'host_aggregate': 12,
|
||||
'goal': goal,
|
||||
'extra': extra}
|
||||
|
||||
_, audit_template = self.create_audit_template(**params)
|
||||
|
||||
# Removing one item from the collection
|
||||
self.client.update_audit_template(
|
||||
audit_template['uuid'],
|
||||
[{'path': '/extra/key2', 'op': 'remove'}])
|
||||
|
||||
extra.pop('key2')
|
||||
_, body = self.client.show_audit_template(audit_template['uuid'])
|
||||
self.assertEqual(extra, body['extra'])
|
||||
|
||||
# Removing the collection
|
||||
self.client.update_audit_template(
|
||||
audit_template['uuid'],
|
||||
[{'path': '/extra', 'op': 'remove'}])
|
||||
_, body = self.client.show_audit_template(audit_template['uuid'])
|
||||
self.assertEqual({}, body['extra'])
|
||||
|
||||
# Removing the Host Aggregate ID
|
||||
self.client.update_audit_template(
|
||||
audit_template['uuid'],
|
||||
[{'path': '/host_aggregate', 'op': 'remove'}])
|
||||
_, body = self.client.show_audit_template(audit_template['uuid'])
|
||||
self.assertEqual('', body['extra'])
|
||||
|
||||
# Assert nothing else was changed
|
||||
self.assertEqual(name, body['name'])
|
||||
self.assertEqual(description, body['description'])
|
||||
self.assertEqual(goal, body['goal'])
|
||||
|
||||
@test.attr(type='smoke')
|
||||
def test_update_audit_template_add(self):
|
||||
params = {'name': 'my at name',
|
||||
'description': 'my at description',
|
||||
'host_aggregate': 12,
|
||||
'goal': 'A GOAL'}
|
||||
|
||||
_, body = self.create_audit_template(**params)
|
||||
|
||||
extra = {'key1': 'value1', 'key2': 'value2'}
|
||||
|
||||
patch = [{'path': '/extra/key1',
|
||||
'op': 'add',
|
||||
'value': extra['key1']},
|
||||
{'path': '/extra/key2',
|
||||
'op': 'add',
|
||||
'value': extra['key2']}]
|
||||
|
||||
self.client.update_audit_template(body['uuid'], patch)
|
||||
|
||||
_, body = self.client.show_audit_template(body['uuid'])
|
||||
self.assertEqual(extra, body['extra'])
|
||||
|
||||
@test.attr(type='smoke')
|
||||
def test_audit_template_audit_list(self):
|
||||
_, audit = self.create_audit(self.audit_template['uuid'])
|
||||
_, body = self.client.list_audit_template_audits(
|
||||
self.audit_template['uuid'])
|
||||
self.assertIn(audit['uuid'], [n['uuid'] for n in body['audits']])
|
||||
50
watcher/contrib/tempest/tempest/cli/README.rst
Normal file
50
watcher/contrib/tempest/tempest/cli/README.rst
Normal file
@@ -0,0 +1,50 @@
|
||||
.. _cli_field_guide:
|
||||
|
||||
Tempest Field Guide to CLI tests
|
||||
================================
|
||||
|
||||
|
||||
What are these tests?
|
||||
---------------------
|
||||
The cli tests test the various OpenStack command line interface tools
|
||||
to ensure that they minimally function. The current scope is read only
|
||||
operations on a cloud that are hard to test via unit tests.
|
||||
|
||||
|
||||
Why are these tests in tempest?
|
||||
-------------------------------
|
||||
These tests exist here because it is extremely difficult to build a
|
||||
functional enough environment in the python-\*client unit tests to
|
||||
provide this kind of testing. Because we already put up a cloud in the
|
||||
gate with devstack + tempest it was decided it was better to have
|
||||
these as a side tree in tempest instead of another QA effort which
|
||||
would split review time.
|
||||
|
||||
|
||||
Scope of these tests
|
||||
--------------------
|
||||
This should stay limited to the scope of testing the cli. Functional
|
||||
testing of the cloud should be elsewhere, this is about exercising the
|
||||
cli code.
|
||||
|
||||
|
||||
Example of a good test
|
||||
----------------------
|
||||
Tests should be isolated to a single command in one of the python
|
||||
clients.
|
||||
|
||||
Tests should not modify the cloud.
|
||||
|
||||
If a test is validating the cli for bad data, it should do it with
|
||||
assertRaises.
|
||||
|
||||
A reasonable example of an existing test is as follows::
|
||||
|
||||
def test_admin_list(self):
|
||||
self.nova('list')
|
||||
self.nova('list', params='--all-tenants 1')
|
||||
self.nova('list', params='--all-tenants 0')
|
||||
self.assertRaises(subprocess.CalledProcessError,
|
||||
self.nova,
|
||||
'list',
|
||||
params='--all-tenants bad')
|
||||
126
watcher/contrib/tempest/tempest/cli/__init__.py
Normal file
126
watcher/contrib/tempest/tempest/cli/__init__.py
Normal file
@@ -0,0 +1,126 @@
|
||||
# Copyright 2013 OpenStack Foundation
|
||||
# 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 functools
|
||||
|
||||
from tempest_lib.cli import base
|
||||
from tempest_lib.cli import output_parser
|
||||
import testtools
|
||||
|
||||
from tempest.common import credentials
|
||||
from tempest import config
|
||||
from tempest import exceptions
|
||||
from tempest.openstack.common import versionutils
|
||||
from tempest import test
|
||||
|
||||
|
||||
CONF = config.CONF
|
||||
|
||||
|
||||
def check_client_version(client, version):
|
||||
"""Checks if the client's version is compatible with the given version
|
||||
|
||||
@param client: The client to check.
|
||||
@param version: The version to compare against.
|
||||
@return: True if the client version is compatible with the given version
|
||||
parameter, False otherwise.
|
||||
"""
|
||||
current_version = base.execute(client, '', params='--version',
|
||||
merge_stderr=True, cli_dir=CONF.cli.cli_dir)
|
||||
|
||||
if not current_version.strip():
|
||||
raise exceptions.TempestException('"%s --version" output was empty' %
|
||||
client)
|
||||
|
||||
return versionutils.is_compatible(version, current_version,
|
||||
same_major=False)
|
||||
|
||||
|
||||
def min_client_version(*args, **kwargs):
|
||||
"""A decorator to skip tests if the client used isn't of the right version.
|
||||
|
||||
@param client: The client command to run. For python-novaclient, this is
|
||||
'nova', for python-cinderclient this is 'cinder', etc.
|
||||
@param version: The minimum version required to run the CLI test.
|
||||
"""
|
||||
def decorator(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(*func_args, **func_kwargs):
|
||||
if not check_client_version(kwargs['client'], kwargs['version']):
|
||||
msg = "requires %s client version >= %s" % (kwargs['client'],
|
||||
kwargs['version'])
|
||||
raise testtools.TestCase.skipException(msg)
|
||||
return func(*func_args, **func_kwargs)
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
class ClientTestBase(test.BaseTestCase):
|
||||
|
||||
@classmethod
|
||||
def skip_checks(cls):
|
||||
super(ClientTestBase, cls).skip_checks()
|
||||
if not CONF.identity_feature_enabled.api_v2:
|
||||
raise cls.skipException("CLI clients rely on identity v2 API, "
|
||||
"which is configured as not available")
|
||||
|
||||
@classmethod
|
||||
def resource_setup(cls):
|
||||
if not CONF.cli.enabled:
|
||||
msg = "cli testing disabled"
|
||||
raise cls.skipException(msg)
|
||||
super(ClientTestBase, cls).resource_setup()
|
||||
cls.isolated_creds = credentials.get_isolated_credentials(cls.__name__)
|
||||
cls.creds = cls.isolated_creds.get_admin_creds()
|
||||
|
||||
def _get_clients(self):
|
||||
clients = base.CLIClient(self.creds.username,
|
||||
self.creds.password,
|
||||
self.creds.tenant_name,
|
||||
CONF.identity.uri, CONF.cli.cli_dir)
|
||||
return clients
|
||||
|
||||
# TODO(mtreinish): The following code is basically copied from tempest-lib.
|
||||
# The base cli test class in tempest-lib 0.0.1 doesn't work as a mixin like
|
||||
# is needed here. The code below should be removed when tempest-lib
|
||||
# provides a way to provide this functionality
|
||||
def setUp(self):
|
||||
super(ClientTestBase, self).setUp()
|
||||
self.clients = self._get_clients()
|
||||
self.parser = output_parser
|
||||
|
||||
def assertTableStruct(self, items, field_names):
|
||||
"""Verify that all items has keys listed in field_names.
|
||||
|
||||
:param items: items to assert are field names in the output table
|
||||
:type items: list
|
||||
:param field_names: field names from the output table of the cmd
|
||||
:type field_names: list
|
||||
"""
|
||||
for item in items:
|
||||
for field in field_names:
|
||||
self.assertIn(field, item)
|
||||
|
||||
def assertFirstLineStartsWith(self, lines, beginning):
|
||||
"""Verify that the first line starts with a string
|
||||
|
||||
:param lines: strings for each line of output
|
||||
:type lines: list
|
||||
:param beginning: verify this is at the beginning of the first line
|
||||
:type beginning: string
|
||||
"""
|
||||
self.assertTrue(lines[0].startswith(beginning),
|
||||
msg=('Beginning of first line has invalid content: %s'
|
||||
% lines[:3]))
|
||||
@@ -0,0 +1 @@
|
||||
This directory consists of simple read only python client tests.
|
||||
@@ -0,0 +1,220 @@
|
||||
# Copyright 2013 OpenStack Foundation
|
||||
# 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 logging
|
||||
import re
|
||||
|
||||
from tempest_lib import exceptions
|
||||
import testtools
|
||||
|
||||
from tempest import cli
|
||||
from tempest import clients
|
||||
from tempest import config
|
||||
from tempest import test
|
||||
|
||||
|
||||
CONF = config.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SimpleReadOnlyCinderClientTest(cli.ClientTestBase):
|
||||
"""Basic, read-only tests for Cinder CLI client.
|
||||
|
||||
Checks return values and output of read-only commands.
|
||||
These tests do not presume any content, nor do they create
|
||||
their own. They only verify the structure of output if present.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def resource_setup(cls):
|
||||
# if not CONF.service_available.cinder:
|
||||
# msg = ("%s skipped as Cinder is not available" % cls.__name__)
|
||||
# raise cls.skipException(msg)
|
||||
super(SimpleReadOnlyCinderClientTest, cls).resource_setup()
|
||||
id_cl = clients.AdminManager().identity_client
|
||||
tenant = id_cl.get_tenant_by_name(CONF.identity.admin_tenant_name)
|
||||
cls.admin_tenant_id = tenant['id']
|
||||
|
||||
def cinder(self, *args, **kwargs):
|
||||
return self.clients.cinder(*args,
|
||||
endpoint_type=CONF.volume.endpoint_type,
|
||||
**kwargs)
|
||||
|
||||
@test.idempotent_id('229bc6dc-d804-4668-b753-b590caf63061')
|
||||
def test_cinder_fake_action(self):
|
||||
self.assertRaises(exceptions.CommandFailed,
|
||||
self.cinder,
|
||||
'this-does-not-exist')
|
||||
|
||||
@test.idempotent_id('77140216-14db-4fc5-a246-e2a587e9e99b')
|
||||
def test_cinder_absolute_limit_list(self):
|
||||
roles = self.parser.listing(self.cinder('absolute-limits'))
|
||||
self.assertTableStruct(roles, ['Name', 'Value'])
|
||||
|
||||
@test.idempotent_id('2206b9ce-1a36-4a0a-a129-e5afc7cee1dd')
|
||||
def test_cinder_backup_list(self):
|
||||
backup_list = self.parser.listing(self.cinder('backup-list'))
|
||||
self.assertTableStruct(backup_list, ['ID', 'Volume ID', 'Status',
|
||||
'Name', 'Size', 'Object Count',
|
||||
'Container'])
|
||||
|
||||
@test.idempotent_id('c7f50346-cd99-4e0b-953f-796ff5f47295')
|
||||
def test_cinder_extra_specs_list(self):
|
||||
extra_specs_list = self.parser.listing(self.cinder('extra-specs-list'))
|
||||
self.assertTableStruct(extra_specs_list, ['ID', 'Name', 'extra_specs'])
|
||||
|
||||
@test.idempotent_id('9de694cb-b40b-442c-a30c-5f9873e144f7')
|
||||
def test_cinder_volumes_list(self):
|
||||
list = self.parser.listing(self.cinder('list'))
|
||||
self.assertTableStruct(list, ['ID', 'Status', 'Name', 'Size',
|
||||
'Volume Type', 'Bootable',
|
||||
'Attached to'])
|
||||
self.cinder('list', params='--all-tenants 1')
|
||||
self.cinder('list', params='--all-tenants 0')
|
||||
self.assertRaises(exceptions.CommandFailed,
|
||||
self.cinder,
|
||||
'list',
|
||||
params='--all-tenants bad')
|
||||
|
||||
@test.idempotent_id('56f7c15c-ee82-4f23-bbe8-ce99b66da493')
|
||||
def test_cinder_quota_class_show(self):
|
||||
"""This CLI can accept and string as param."""
|
||||
roles = self.parser.listing(self.cinder('quota-class-show',
|
||||
params='abc'))
|
||||
self.assertTableStruct(roles, ['Property', 'Value'])
|
||||
|
||||
@test.idempotent_id('a919a811-b7f0-47a7-b4e5-f3eb674dd200')
|
||||
def test_cinder_quota_defaults(self):
|
||||
"""This CLI can accept and string as param."""
|
||||
roles = self.parser.listing(self.cinder('quota-defaults',
|
||||
params=self.admin_tenant_id))
|
||||
self.assertTableStruct(roles, ['Property', 'Value'])
|
||||
|
||||
@test.idempotent_id('18166673-ffa8-4df3-b60c-6375532288bc')
|
||||
def test_cinder_quota_show(self):
|
||||
"""This CLI can accept and string as param."""
|
||||
roles = self.parser.listing(self.cinder('quota-show',
|
||||
params=self.admin_tenant_id))
|
||||
self.assertTableStruct(roles, ['Property', 'Value'])
|
||||
|
||||
@test.idempotent_id('b2c66ed9-ca96-4dc4-94cc-8083e664e516')
|
||||
def test_cinder_rate_limits(self):
|
||||
rate_limits = self.parser.listing(self.cinder('rate-limits'))
|
||||
self.assertTableStruct(rate_limits, ['Verb', 'URI', 'Value', 'Remain',
|
||||
'Unit', 'Next_Available'])
|
||||
|
||||
@test.idempotent_id('7a19955b-807c-481a-a2ee-9d76733eac28')
|
||||
@testtools.skipUnless(CONF.volume_feature_enabled.snapshot,
|
||||
'Volume snapshot not available.')
|
||||
def test_cinder_snapshot_list(self):
|
||||
snapshot_list = self.parser.listing(self.cinder('snapshot-list'))
|
||||
self.assertTableStruct(snapshot_list, ['ID', 'Volume ID', 'Status',
|
||||
'Name', 'Size'])
|
||||
|
||||
@test.idempotent_id('6e54ecd9-7ba9-490d-8e3b-294b67139e73')
|
||||
def test_cinder_type_list(self):
|
||||
type_list = self.parser.listing(self.cinder('type-list'))
|
||||
self.assertTableStruct(type_list, ['ID', 'Name'])
|
||||
|
||||
@test.idempotent_id('2c363583-24a0-4980-b9cb-b50c0d241e82')
|
||||
def test_cinder_list_extensions(self):
|
||||
roles = self.parser.listing(self.cinder('list-extensions'))
|
||||
self.assertTableStruct(roles, ['Name', 'Summary', 'Alias', 'Updated'])
|
||||
|
||||
@test.idempotent_id('691bd6df-30ad-4be7-927b-a02d62aaa38a')
|
||||
def test_cinder_credentials(self):
|
||||
credentials = self.parser.listing(self.cinder('credentials'))
|
||||
self.assertTableStruct(credentials, ['User Credentials', 'Value'])
|
||||
|
||||
@test.idempotent_id('5c6d71a3-4904-4a3a-aec9-7fd4aa830e95')
|
||||
def test_cinder_availability_zone_list(self):
|
||||
zone_list = self.parser.listing(self.cinder('availability-zone-list'))
|
||||
self.assertTableStruct(zone_list, ['Name', 'Status'])
|
||||
|
||||
@test.idempotent_id('9b0fd5a6-f955-42b9-a42f-6f542a80b9a3')
|
||||
def test_cinder_endpoints(self):
|
||||
out = self.cinder('endpoints')
|
||||
tables = self.parser.tables(out)
|
||||
for table in tables:
|
||||
headers = table['headers']
|
||||
self.assertTrue(2 >= len(headers))
|
||||
self.assertEqual('Value', headers[1])
|
||||
|
||||
@test.idempotent_id('301b5ae1-9591-4e9f-999c-d525a9bdf822')
|
||||
def test_cinder_service_list(self):
|
||||
service_list = self.parser.listing(self.cinder('service-list'))
|
||||
self.assertTableStruct(service_list, ['Binary', 'Host', 'Zone',
|
||||
'Status', 'State', 'Updated_at'])
|
||||
|
||||
@test.idempotent_id('7260ae52-b462-461e-9048-36d0bccf92c6')
|
||||
def test_cinder_transfer_list(self):
|
||||
transfer_list = self.parser.listing(self.cinder('transfer-list'))
|
||||
self.assertTableStruct(transfer_list, ['ID', 'Volume ID', 'Name'])
|
||||
|
||||
@test.idempotent_id('0976dea8-14f3-45a9-8495-3617fc4fbb13')
|
||||
def test_cinder_bash_completion(self):
|
||||
self.cinder('bash-completion')
|
||||
|
||||
@test.idempotent_id('b7c00361-be80-4512-8735-5f98fc54f2a9')
|
||||
def test_cinder_qos_list(self):
|
||||
qos_list = self.parser.listing(self.cinder('qos-list'))
|
||||
self.assertTableStruct(qos_list, ['ID', 'Name', 'Consumer', 'specs'])
|
||||
|
||||
@test.idempotent_id('2e92dc6e-22b5-4d94-abfc-b543b0c50a89')
|
||||
def test_cinder_encryption_type_list(self):
|
||||
encrypt_list = self.parser.listing(self.cinder('encryption-type-list'))
|
||||
self.assertTableStruct(encrypt_list, ['Volume Type ID', 'Provider',
|
||||
'Cipher', 'Key Size',
|
||||
'Control Location'])
|
||||
|
||||
@test.idempotent_id('0ee6cb4c-8de6-4811-a7be-7f4bb75b80cc')
|
||||
def test_admin_help(self):
|
||||
help_text = self.cinder('help')
|
||||
lines = help_text.split('\n')
|
||||
self.assertFirstLineStartsWith(lines, 'usage: cinder')
|
||||
|
||||
commands = []
|
||||
cmds_start = lines.index('Positional arguments:')
|
||||
cmds_end = lines.index('Optional arguments:')
|
||||
command_pattern = re.compile('^ {4}([a-z0-9\-\_]+)')
|
||||
for line in lines[cmds_start:cmds_end]:
|
||||
match = command_pattern.match(line)
|
||||
if match:
|
||||
commands.append(match.group(1))
|
||||
commands = set(commands)
|
||||
wanted_commands = set(('absolute-limits', 'list', 'help',
|
||||
'quota-show', 'type-list', 'snapshot-list'))
|
||||
self.assertFalse(wanted_commands - commands)
|
||||
|
||||
# Optional arguments:
|
||||
|
||||
@test.idempotent_id('2fd6f530-183c-4bda-8918-1e59e36c26b9')
|
||||
def test_cinder_version(self):
|
||||
self.cinder('', flags='--version')
|
||||
|
||||
@test.idempotent_id('306bac51-c443-4426-a6cf-583a953fcd68')
|
||||
def test_cinder_debug_list(self):
|
||||
self.cinder('list', flags='--debug')
|
||||
|
||||
@test.idempotent_id('6d97fcd2-5dd1-429d-af70-030c949d86cd')
|
||||
def test_cinder_retries_list(self):
|
||||
self.cinder('list', flags='--retries 3')
|
||||
|
||||
@test.idempotent_id('95a2850c-35b4-4159-bb93-51647a5ad232')
|
||||
def test_cinder_region_list(self):
|
||||
region = CONF.volume.region
|
||||
if not region:
|
||||
region = CONF.identity.region
|
||||
self.cinder('list', flags='--os-region-name ' + region)
|
||||
42
watcher/contrib/tempest/tempest/clients_infra_optim.py
Normal file
42
watcher/contrib/tempest/tempest/clients_infra_optim.py
Normal file
@@ -0,0 +1,42 @@
|
||||
# Copyright 2014 Mirantis 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.
|
||||
|
||||
from tempest import clients
|
||||
from tempest.common import cred_provider
|
||||
from tempest import config
|
||||
from tempest.services.infra_optim.v1.json import infra_optim_client as ioc
|
||||
|
||||
CONF = config.CONF
|
||||
|
||||
|
||||
class Manager(clients.Manager):
|
||||
def __init__(self, credentials=None, service=None):
|
||||
super(Manager, self).__init__(credentials, service)
|
||||
self.io_client = ioc.InfraOptimClientJSON(self.auth_provider,
|
||||
'infra-optim',
|
||||
CONF.identity.region)
|
||||
|
||||
|
||||
class AltManager(Manager):
|
||||
def __init__(self, service=None):
|
||||
super(AltManager, self).__init__(
|
||||
cred_provider.get_configured_credentials('alt_user'), service)
|
||||
|
||||
|
||||
class AdminManager(Manager):
|
||||
def __init__(self, service=None):
|
||||
super(AdminManager, self).__init__(
|
||||
cred_provider.get_configured_credentials('identity_admin'),
|
||||
service)
|
||||
45
watcher/contrib/tempest/tempest/config_infra_optim.py
Normal file
45
watcher/contrib/tempest/tempest/config_infra_optim.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# Copyright 2014 Mirantis 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.
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
from oslo_config import cfg
|
||||
|
||||
from tempest import config # noqa
|
||||
|
||||
service_available_group = cfg.OptGroup(name="service_available",
|
||||
title="Available OpenStack Services")
|
||||
|
||||
ServiceAvailableGroup = [
|
||||
cfg.BoolOpt("watcher",
|
||||
default=True,
|
||||
help="Whether or not watcher is expected to be available"),
|
||||
]
|
||||
|
||||
|
||||
class TempestConfigProxyWatcher(object):
|
||||
"""Wrapper over standard Tempest config that sets Watcher opts."""
|
||||
|
||||
def __init__(self):
|
||||
self._config = config.CONF
|
||||
config.register_opt_group(
|
||||
cfg.CONF, service_available_group, ServiceAvailableGroup)
|
||||
self._config.share = cfg.CONF.share
|
||||
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self._config, attr)
|
||||
|
||||
|
||||
CONF = TempestConfigProxyWatcher()
|
||||
219
watcher/contrib/tempest/tempest/services/infra_optim/base.py
Normal file
219
watcher/contrib/tempest/tempest/services/infra_optim/base.py
Normal file
@@ -0,0 +1,219 @@
|
||||
# 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 functools
|
||||
import json
|
||||
import urllib
|
||||
|
||||
import six
|
||||
|
||||
from tempest.common import service_client
|
||||
|
||||
|
||||
def handle_errors(f):
|
||||
"""A decorator that allows to ignore certain types of errors."""
|
||||
|
||||
@functools.wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
param_name = 'ignore_errors'
|
||||
ignored_errors = kwargs.get(param_name, tuple())
|
||||
|
||||
if param_name in kwargs:
|
||||
del kwargs[param_name]
|
||||
|
||||
try:
|
||||
return f(*args, **kwargs)
|
||||
except ignored_errors:
|
||||
# Silently ignore errors
|
||||
pass
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class InfraOptimClient(service_client.ServiceClient):
|
||||
"""
|
||||
Base Tempest REST client for Watcher API.
|
||||
|
||||
"""
|
||||
|
||||
uri_prefix = ''
|
||||
|
||||
def serialize(self, object_dict):
|
||||
"""Serialize an Watcher object."""
|
||||
|
||||
return json.dumps(object_dict)
|
||||
|
||||
def deserialize(self, object_str):
|
||||
"""Deserialize an Watcher object."""
|
||||
|
||||
return json.loads(object_str)
|
||||
|
||||
def _get_uri(self, resource_name, uuid=None, permanent=False):
|
||||
"""
|
||||
Get URI for a specific resource or object.
|
||||
|
||||
:param resource_name: The name of the REST resource, e.g., 'audits'.
|
||||
:param uuid: The unique identifier of an object in UUID format.
|
||||
:return: Relative URI for the resource or object.
|
||||
|
||||
"""
|
||||
prefix = self.uri_prefix if not permanent else ''
|
||||
|
||||
return '{pref}/{res}{uuid}'.format(pref=prefix,
|
||||
res=resource_name,
|
||||
uuid='/%s' % uuid if uuid else '')
|
||||
|
||||
def _make_patch(self, allowed_attributes, **kw):
|
||||
"""
|
||||
Create a JSON patch according to RFC 6902.
|
||||
|
||||
:param allowed_attributes: An iterable object that contains a set of
|
||||
allowed attributes for an object.
|
||||
:param **kw: Attributes and new values for them.
|
||||
:return: A JSON path that sets values of the specified attributes to
|
||||
the new ones.
|
||||
|
||||
"""
|
||||
def get_change(kw, path='/'):
|
||||
for name, value in six.iteritems(kw):
|
||||
if isinstance(value, dict):
|
||||
for ch in get_change(value, path + '%s/' % name):
|
||||
yield ch
|
||||
else:
|
||||
if value is None:
|
||||
yield {'path': path + name,
|
||||
'op': 'remove'}
|
||||
else:
|
||||
yield {'path': path + name,
|
||||
'value': value,
|
||||
'op': 'replace'}
|
||||
|
||||
patch = [ch for ch in get_change(kw)
|
||||
if ch['path'].lstrip('/') in allowed_attributes]
|
||||
|
||||
return patch
|
||||
|
||||
def _list_request(self, resource, permanent=False, **kwargs):
|
||||
"""
|
||||
Get the list of objects of the specified type.
|
||||
|
||||
:param resource: The name of the REST resource, e.g., 'audits'.
|
||||
"param **kw: Parameters for the request.
|
||||
:return: A tuple with the server response and deserialized JSON list
|
||||
of objects
|
||||
|
||||
"""
|
||||
uri = self._get_uri(resource, permanent=permanent)
|
||||
if kwargs:
|
||||
uri += "?%s" % urllib.urlencode(kwargs)
|
||||
|
||||
resp, body = self.get(uri)
|
||||
self.expected_success(200, resp['status'])
|
||||
|
||||
return resp, self.deserialize(body)
|
||||
|
||||
def _show_request(self, resource, uuid, permanent=False, **kwargs):
|
||||
"""
|
||||
Gets a specific object of the specified type.
|
||||
|
||||
:param uuid: Unique identifier of the object in UUID format.
|
||||
:return: Serialized object as a dictionary.
|
||||
|
||||
"""
|
||||
if 'uri' in kwargs:
|
||||
uri = kwargs['uri']
|
||||
else:
|
||||
uri = self._get_uri(resource, uuid=uuid, permanent=permanent)
|
||||
resp, body = self.get(uri)
|
||||
self.expected_success(200, resp['status'])
|
||||
|
||||
return resp, self.deserialize(body)
|
||||
|
||||
def _create_request(self, resource, object_dict):
|
||||
"""
|
||||
Create an object of the specified type.
|
||||
|
||||
:param resource: The name of the REST resource, e.g., 'audits'.
|
||||
:param object_dict: A Python dict that represents an object of the
|
||||
specified type.
|
||||
:return: A tuple with the server response and the deserialized created
|
||||
object.
|
||||
|
||||
"""
|
||||
body = self.serialize(object_dict)
|
||||
uri = self._get_uri(resource)
|
||||
|
||||
resp, body = self.post(uri, body=body)
|
||||
self.expected_success(201, resp['status'])
|
||||
|
||||
return resp, self.deserialize(body)
|
||||
|
||||
def _delete_request(self, resource, uuid):
|
||||
"""
|
||||
Delete specified object.
|
||||
|
||||
:param resource: The name of the REST resource, e.g., 'audits'.
|
||||
:param uuid: The unique identifier of an object in UUID format.
|
||||
:return: A tuple with the server response and the response body.
|
||||
|
||||
"""
|
||||
uri = self._get_uri(resource, uuid)
|
||||
|
||||
resp, body = self.delete(uri)
|
||||
self.expected_success(204, resp['status'])
|
||||
return resp, body
|
||||
|
||||
def _patch_request(self, resource, uuid, patch_object):
|
||||
"""
|
||||
Update specified object with JSON-patch.
|
||||
|
||||
:param resource: The name of the REST resource, e.g., 'audits'.
|
||||
:param uuid: The unique identifier of an object in UUID format.
|
||||
:return: A tuple with the server response and the serialized patched
|
||||
object.
|
||||
|
||||
"""
|
||||
uri = self._get_uri(resource, uuid)
|
||||
patch_body = json.dumps(patch_object)
|
||||
|
||||
resp, body = self.patch(uri, body=patch_body)
|
||||
self.expected_success(200, resp['status'])
|
||||
return resp, self.deserialize(body)
|
||||
|
||||
@handle_errors
|
||||
def get_api_description(self):
|
||||
"""Retrieves all versions of the Watcher API."""
|
||||
|
||||
return self._list_request('', permanent=True)
|
||||
|
||||
@handle_errors
|
||||
def get_version_description(self, version='v1'):
|
||||
"""
|
||||
Retrieves the description of the API.
|
||||
|
||||
:param version: The version of the API. Default: 'v1'.
|
||||
:return: Serialized description of API resources.
|
||||
|
||||
"""
|
||||
return self._list_request(version, permanent=True)
|
||||
|
||||
def _put_request(self, resource, put_object):
|
||||
"""
|
||||
Update specified object with JSON-patch.
|
||||
|
||||
"""
|
||||
uri = self._get_uri(resource)
|
||||
put_body = json.dumps(put_object)
|
||||
|
||||
resp, body = self.put(uri, body=put_body)
|
||||
self.expected_success(202, resp['status'])
|
||||
return resp, body
|
||||
@@ -0,0 +1,151 @@
|
||||
# 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 tempest.services.infra_optim import base
|
||||
|
||||
|
||||
class InfraOptimClientJSON(base.InfraOptimClient):
|
||||
"""
|
||||
Base Tempest REST client for Watcher API v1.
|
||||
"""
|
||||
version = '1'
|
||||
uri_prefix = 'v1'
|
||||
|
||||
# Audit Template
|
||||
|
||||
@base.handle_errors
|
||||
def list_audit_templates(self, **kwargs):
|
||||
"""List all existing audit templates."""
|
||||
return self._list_request('audit_templates', **kwargs)
|
||||
|
||||
@base.handle_errors
|
||||
def list_audit_template_audits(self, audit_template_uuid):
|
||||
"""Lists all audits associated with a audit template."""
|
||||
return self._list_request(
|
||||
'/audit_templates/%s/audits' % audit_template_uuid)
|
||||
|
||||
@base.handle_errors
|
||||
def list_audit_templates_detail(self, **kwargs):
|
||||
"""Lists details of all existing audit templates."""
|
||||
return self._list_request('/audit_templates/detail', **kwargs)
|
||||
|
||||
@base.handle_errors
|
||||
def show_audit_template(self, uuid):
|
||||
"""
|
||||
Gets a specific audit template.
|
||||
|
||||
:param uuid: Unique identifier of the audit template in UUID format.
|
||||
:return: Serialized audit template as a dictionary.
|
||||
|
||||
"""
|
||||
return self._show_request('audit_templates', uuid)
|
||||
|
||||
@base.handle_errors
|
||||
def show_audit_template_by_host_agregate(self, host_agregate_id):
|
||||
"""
|
||||
Gets an audit template associated with given host agregate ID.
|
||||
|
||||
:param uuid: Unique identifier of the audit_template in UUID format.
|
||||
:return: Serialized audit_template as a dictionary.
|
||||
|
||||
"""
|
||||
uri = '/audit_templates/detail?host_agregate=%s' % host_agregate_id
|
||||
|
||||
return self._show_request('audit_templates', uuid=None, uri=uri)
|
||||
|
||||
@base.handle_errors
|
||||
def show_audit_template_by_goal(self, goal):
|
||||
"""
|
||||
Gets an audit template associated with given goal.
|
||||
|
||||
:param uuid: Unique identifier of the audit_template in UUID format.
|
||||
:return: Serialized audit_template as a dictionary.
|
||||
|
||||
"""
|
||||
uri = '/audit_templates/detail?goal=%s' % goal
|
||||
|
||||
return self._show_request('audit_templates', uuid=None, uri=uri)
|
||||
|
||||
@base.handle_errors
|
||||
def create_audit_template(self, **kwargs):
|
||||
"""
|
||||
Creates an audit template with the specified parameters.
|
||||
|
||||
:param name: The name of the audit template. Default: My Audit Template
|
||||
:param description: The description of the audit template.
|
||||
Default: AT Description
|
||||
:param goal: The goal associated within the audit template.
|
||||
Default: SERVERS_CONSOLIDATION
|
||||
:param host_aggregate: ID of the host aggregate targeted by
|
||||
this audit template. Default: 1
|
||||
:param extra: IMetadata associated to this audit template.
|
||||
Default: {}
|
||||
:return: A tuple with the server response and the created audit
|
||||
template.
|
||||
|
||||
"""
|
||||
audit_template = {
|
||||
'name': kwargs.get('name', 'My Audit Template'),
|
||||
'description': kwargs.get('description', 'AT Description'),
|
||||
'goal': kwargs.get('goal', 'SERVERS_CONSOLIDATION'),
|
||||
'host_aggregate': kwargs.get('host_aggregate', 1),
|
||||
'extra': kwargs.get('extra', {}),
|
||||
}
|
||||
|
||||
return self._create_request('audit_templates', audit_template)
|
||||
|
||||
# @base.handle_errors
|
||||
# def create_audit(self, audit_template_id=None, **kwargs):
|
||||
# """
|
||||
# Create a infra_optim audit with the specified parameters.
|
||||
|
||||
# :param cpu_arch: CPU architecture of the audit. Default: x86_64.
|
||||
# :param cpus: Number of CPUs. Default: 8.
|
||||
# :param local_gb: Disk size. Default: 1024.
|
||||
# :param memory_mb: Available RAM. Default: 4096.
|
||||
# :param driver: Driver name. Default: "fake"
|
||||
# :return: A tuple with the server response and the created audit.
|
||||
|
||||
# """
|
||||
# audit = {'audit_template_uuid': audit_template_id,
|
||||
# 'properties': {'cpu_arch': kwargs.get('cpu_arch', 'x86_64'),
|
||||
# 'cpus': kwargs.get('cpus', 8),
|
||||
# 'local_gb': kwargs.get('local_gb', 1024),
|
||||
# 'memory_mb': kwargs.get('memory_mb', 4096)},
|
||||
# 'driver': kwargs.get('driver', 'fake')}
|
||||
|
||||
# return self._create_request('audits', audit)
|
||||
|
||||
@base.handle_errors
|
||||
def delete_audit_template(self, uuid):
|
||||
"""
|
||||
Deletes an audit template having the specified UUID.
|
||||
|
||||
:param uuid: The unique identifier of the audit template.
|
||||
:return: A tuple with the server response and the response body.
|
||||
|
||||
"""
|
||||
return self._delete_request('audit_templates', uuid)
|
||||
|
||||
@base.handle_errors
|
||||
def update_audit_template(self, uuid, patch):
|
||||
"""
|
||||
Update the specified audit template.
|
||||
|
||||
:param uuid: The unique identifier of the audit template.
|
||||
:param patch: List of dicts representing json patches.
|
||||
:return: A tuple with the server response and the updated audit
|
||||
template.
|
||||
|
||||
"""
|
||||
|
||||
return self._patch_request('audit_templates', uuid, patch)
|
||||
15
watcher/db/README.md
Normal file
15
watcher/db/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Watcher Database
|
||||
|
||||
This database stores all the watcher business objects which can be requested by the Watcher API :
|
||||
* Audit templates
|
||||
* Audits
|
||||
* Action plans
|
||||
* Actions history
|
||||
* Watcher settings :
|
||||
* metrics/events collector endpoints for each type of metric
|
||||
* manual/automatic mode
|
||||
* Business Objects states
|
||||
|
||||
It may be any relational database or a key-value database.
|
||||
|
||||
Business objects are read/created/updated/deleted from/to the Watcher database using a common Python package which provides a high-level Service API.
|
||||
0
watcher/db/__init__.py
Normal file
0
watcher/db/__init__.py
Normal file
379
watcher/db/api.py
Normal file
379
watcher/db/api.py
Normal file
@@ -0,0 +1,379 @@
|
||||
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# 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.
|
||||
"""
|
||||
Base classes for storage engines
|
||||
"""
|
||||
|
||||
import abc
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_db import api as db_api
|
||||
import six
|
||||
|
||||
|
||||
_BACKEND_MAPPING = {'sqlalchemy': 'watcher.db.sqlalchemy.api'}
|
||||
IMPL = db_api.DBAPI.from_config(cfg.CONF, backend_mapping=_BACKEND_MAPPING,
|
||||
lazy=True)
|
||||
|
||||
|
||||
def get_instance():
|
||||
"""Return a DB API instance."""
|
||||
return IMPL
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class Connection(object):
|
||||
"""Base class for storage system connections."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def __init__(self):
|
||||
"""Constructor."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_audit_template_list(self, context, columns=None, filters=None,
|
||||
limit=None, marker=None, sort_key=None,
|
||||
sort_dir=None):
|
||||
"""Get specific columns for matching audit templates.
|
||||
|
||||
Return a list of the specified columns for all audit templates that
|
||||
match the specified filters.
|
||||
|
||||
:param context: The security context
|
||||
:param columns: List of column names to return.
|
||||
Defaults to 'id' column when columns == None.
|
||||
:param filters: Filters to apply. Defaults to None.
|
||||
|
||||
:param limit: Maximum number of audit templates to return.
|
||||
:param marker: the last item of the previous page; we return the next
|
||||
result set.
|
||||
:param sort_key: Attribute by which results should be sorted.
|
||||
:param sort_dir: direction in which results should be sorted.
|
||||
(asc, desc)
|
||||
:returns: A list of tuples of the specified columns.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def create_audit_template(self, values):
|
||||
"""Create a new audit template.
|
||||
|
||||
:param values: A dict containing several items used to identify
|
||||
and track the audit template. For example:
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
'uuid': utils.generate_uuid(),
|
||||
'name': 'example',
|
||||
'description': 'free text description'
|
||||
'host_aggregate': 'nova aggregate name or id'
|
||||
'goal': 'SERVER_CONSOLiDATION'
|
||||
'extra': {'automatic': True}
|
||||
}
|
||||
:returns: An audit template.
|
||||
:raises: AuditTemplateAlreadyExists
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_audit_template_by_id(self, context, audit_template_id):
|
||||
"""Return an audit template.
|
||||
|
||||
:param context: The security context
|
||||
:param audit_template_id: The id of an audit template.
|
||||
:returns: An audit template.
|
||||
:raises: AuditTemplateNotFound
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_audit_template_by_uuid(self, context, audit_template_uuid):
|
||||
"""Return an audit template.
|
||||
|
||||
:param context: The security context
|
||||
:param audit_template_uuid: The uuid of an audit template.
|
||||
:returns: An audit template.
|
||||
:raises: AuditTemplateNotFound
|
||||
"""
|
||||
|
||||
def get_audit_template_by__name(self, context, audit_template_name):
|
||||
"""Return an audit template.
|
||||
|
||||
:param context: The security context
|
||||
:param audit_template_name: The name of an audit template.
|
||||
:returns: An audit template.
|
||||
:raises: AuditTemplateNotFound
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def destroy_audit_template(self, audit_template_id):
|
||||
"""Destroy an audit_template.
|
||||
|
||||
:param audit_template_id: The id or uuid of an audit template.
|
||||
:raises: AuditTemplateNotFound
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def update_audit_template(self, audit_template_id, values):
|
||||
"""Update properties of an audit template.
|
||||
|
||||
:param audit_template_id: The id or uuid of an audit template.
|
||||
:returns: An audit template.
|
||||
:raises: AuditTemplateNotFound
|
||||
:raises: InvalidParameterValue
|
||||
"""
|
||||
@abc.abstractmethod
|
||||
def soft_delete_audit_template(self, audit_template_id):
|
||||
"""Soft delete an audit_template.
|
||||
|
||||
:param audit_template_id: The id or uuid of an audit template.
|
||||
:raises: AuditTemplateNotFound
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_audit_list(self, context, columns=None, filters=None, limit=None,
|
||||
marker=None, sort_key=None, sort_dir=None):
|
||||
"""Get specific columns for matching audits.
|
||||
|
||||
Return a list of the specified columns for all audits that match the
|
||||
specified filters.
|
||||
|
||||
:param context: The security context
|
||||
:param columns: List of column names to return.
|
||||
Defaults to 'id' column when columns == None.
|
||||
:param filters: Filters to apply. Defaults to None.
|
||||
|
||||
:param limit: Maximum number of audits to return.
|
||||
:param marker: the last item of the previous page; we return the next
|
||||
result set.
|
||||
:param sort_key: Attribute by which results should be sorted.
|
||||
:param sort_dir: direction in which results should be sorted.
|
||||
(asc, desc)
|
||||
:returns: A list of tuples of the specified columns.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def create_audit(self, values):
|
||||
"""Create a new audit.
|
||||
|
||||
:param values: A dict containing several items used to identify
|
||||
and track the audit, and several dicts which are passed
|
||||
into the Drivers when managing this audit. For example:
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
'uuid': utils.generate_uuid(),
|
||||
'type': 'ONESHOT',
|
||||
'deadline': None
|
||||
}
|
||||
:returns: An audit.
|
||||
:raises: AuditAlreadyExists
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_audit_by_id(self, context, audit_id):
|
||||
"""Return an audit.
|
||||
|
||||
:param context: The security context
|
||||
:param audit_id: The id of an audit.
|
||||
:returns: An audit.
|
||||
:raises: AuditNotFound
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_audit_by_uuid(self, context, audit_uuid):
|
||||
"""Return an audit.
|
||||
|
||||
:param context: The security context
|
||||
:param audit_uuid: The uuid of an audit.
|
||||
:returns: An audit.
|
||||
:raises: AuditNotFound
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def destroy_audit(self, audit_id):
|
||||
"""Destroy an audit and all associated action plans.
|
||||
|
||||
:param audit_id: The id or uuid of an audit.
|
||||
:raises: AuditNotFound
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def update_audit(self, audit_id, values):
|
||||
"""Update properties of an audit.
|
||||
|
||||
:param audit_id: The id or uuid of an audit.
|
||||
:returns: An audit.
|
||||
:raises: AuditNotFound
|
||||
:raises: InvalidParameterValue
|
||||
"""
|
||||
|
||||
def soft_delete_audit(self, audit_id):
|
||||
"""Soft delete an audit and all associated action plans.
|
||||
|
||||
:param audit_id: The id or uuid of an audit.
|
||||
:returns: An audit.
|
||||
:raises: AuditNotFound
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_action_list(self, context, columns=None, filters=None, limit=None,
|
||||
marker=None, sort_key=None, sort_dir=None):
|
||||
"""Get specific columns for matching actions.
|
||||
|
||||
Return a list of the specified columns for all actions that match the
|
||||
specified filters.
|
||||
|
||||
:param context: The security context
|
||||
:param columns: List of column names to return.
|
||||
Defaults to 'id' column when columns == None.
|
||||
:param filters: Filters to apply. Defaults to None.
|
||||
|
||||
:param limit: Maximum number of actions to return.
|
||||
:param marker: the last item of the previous page; we return the next
|
||||
result set.
|
||||
:param sort_key: Attribute by which results should be sorted.
|
||||
:param sort_dir: direction in which results should be sorted.
|
||||
(asc, desc)
|
||||
:returns: A list of tuples of the specified columns.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def create_action(self, values):
|
||||
"""Create a new action.
|
||||
|
||||
:param values: A dict containing several items used to identify
|
||||
and track the action, and several dicts which are passed
|
||||
into the Drivers when managing this action. For example:
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
'uuid': utils.generate_uuid(),
|
||||
'name': 'example',
|
||||
'description': 'free text description'
|
||||
'aggregate': 'nova aggregate name or uuid'
|
||||
}
|
||||
:returns: A action.
|
||||
:raises: ActionAlreadyExists
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_action_by_id(self, context, action_id):
|
||||
"""Return a action.
|
||||
|
||||
:param context: The security context
|
||||
:param action_id: The id of a action.
|
||||
:returns: A action.
|
||||
:raises: ActionNotFound
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_action_by_uuid(self, context, action_uuid):
|
||||
"""Return a action.
|
||||
|
||||
:param context: The security context
|
||||
:param action_uuid: The uuid of a action.
|
||||
:returns: A action.
|
||||
:raises: ActionNotFound
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def destroy_action(self, action_id):
|
||||
"""Destroy a action and all associated interfaces.
|
||||
|
||||
:param action_id: The id or uuid of a action.
|
||||
:raises: ActionNotFound
|
||||
:raises: ActionReferenced
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def update_action(self, action_id, values):
|
||||
"""Update properties of a action.
|
||||
|
||||
:param action_id: The id or uuid of a action.
|
||||
:returns: A action.
|
||||
:raises: ActionNotFound
|
||||
:raises: ActionReferenced
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_action_plan_list(
|
||||
self, context, columns=None, filters=None, limit=None,
|
||||
marker=None, sort_key=None, sort_dir=None):
|
||||
"""Get specific columns for matching action plans.
|
||||
|
||||
Return a list of the specified columns for all action plans that
|
||||
match the specified filters.
|
||||
|
||||
:param context: The security context
|
||||
:param columns: List of column names to return.
|
||||
Defaults to 'id' column when columns == None.
|
||||
:param filters: Filters to apply. Defaults to None.
|
||||
|
||||
:param limit: Maximum number of audits to return.
|
||||
:param marker: the last item of the previous page; we return the next
|
||||
result set.
|
||||
:param sort_key: Attribute by which results should be sorted.
|
||||
:param sort_dir: direction in which results should be sorted.
|
||||
(asc, desc)
|
||||
:returns: A list of tuples of the specified columns.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def create_action_plan(self, values):
|
||||
"""Create a new action plan.
|
||||
|
||||
:param values: A dict containing several items used to identify
|
||||
and track the action plan.
|
||||
:returns: An action plan.
|
||||
:raises: ActionPlanAlreadyExists
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_action_plan_by_id(self, context, action_plan_id):
|
||||
"""Return an action plan.
|
||||
|
||||
:param context: The security context
|
||||
:param action_plan_id: The id of an action plan.
|
||||
:returns: An action plan.
|
||||
:raises: ActionPlanNotFound
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_action_plan_by_uuid(self, context, action_plan__uuid):
|
||||
"""Return a action plan.
|
||||
|
||||
:param context: The security context
|
||||
:param action_plan__uuid: The uuid of an action plan.
|
||||
:returns: An action plan.
|
||||
:raises: ActionPlanNotFound
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def destroy_action_plan(self, action_plan_id):
|
||||
"""Destroy an action plan and all associated interfaces.
|
||||
|
||||
:param action_plan_id: The id or uuid of a action plan.
|
||||
:raises: ActionPlanNotFound
|
||||
:raises: ActionPlanReferenced
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def update_action_plan(self, action_plan_id, values):
|
||||
"""Update properties of an action plan.
|
||||
|
||||
:param action_plan_id: The id or uuid of an action plan.
|
||||
:returns: An action plan.
|
||||
:raises: ActionPlanNotFound
|
||||
:raises: ActionPlanReferenced
|
||||
"""
|
||||
56
watcher/db/migration.py
Normal file
56
watcher/db/migration.py
Normal file
@@ -0,0 +1,56 @@
|
||||
# Copyright 2010 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# 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.
|
||||
|
||||
"""Database setup and migration commands."""
|
||||
|
||||
from oslo_config import cfg
|
||||
from stevedore import driver
|
||||
|
||||
_IMPL = None
|
||||
|
||||
|
||||
def get_backend():
|
||||
global _IMPL
|
||||
if not _IMPL:
|
||||
cfg.CONF.import_opt('backend', 'oslo_db.options', group='database')
|
||||
_IMPL = driver.DriverManager("watcher.database.migration_backend",
|
||||
cfg.CONF.database.backend).driver
|
||||
return _IMPL
|
||||
|
||||
|
||||
def upgrade(version=None):
|
||||
"""Migrate the database to `version` or the most recent version."""
|
||||
return get_backend().upgrade(version)
|
||||
|
||||
|
||||
def downgrade(version=None):
|
||||
return get_backend().downgrade(version)
|
||||
|
||||
|
||||
def version():
|
||||
return get_backend().version()
|
||||
|
||||
|
||||
def stamp(version):
|
||||
return get_backend().stamp(version)
|
||||
|
||||
|
||||
def revision(message, autogenerate):
|
||||
return get_backend().revision(message, autogenerate)
|
||||
|
||||
|
||||
def create_schema():
|
||||
return get_backend().create_schema()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user