initial version

Change-Id: I699e0ab082657880998d8618fe29eb7f56c6c661
This commit is contained in:
David TARDIVEL
2015-06-04 15:26:55 +02:00
parent 073c6e49cb
commit d14e057da1
316 changed files with 27260 additions and 0 deletions

19
watcher/__init__.py Normal file
View 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
View 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
View File

49
watcher/api/acl.py Normal file
View 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
View 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
View 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,
}

View File

View 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)

View 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

View 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)

View 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)

View 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()

View 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)

View 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()

View 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()

View 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

View 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

View 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
View 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

View 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)

View 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)

View 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
View 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.

View File

View File

View 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")

View 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")

View 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")

View 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")

View 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)

View File

View 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)

View 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)

View 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

View 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

View 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)

View 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

View 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)

View 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()

View 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)

View 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

View 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"

View 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)

View 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

View 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
View File

57
watcher/cmd/api.py Normal file
View 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
View 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
View 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()

View 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()

View File

30
watcher/common/config.py Normal file
View 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
View 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
View 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
View 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

View File

View 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

View 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

View 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)

View 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)

View 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

View 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()

View 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()

View 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
View 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
View 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
View 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)

View 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
View 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
View 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))

View 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.

View 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)

View File

@@ -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)

View File

@@ -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']])

View 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')

View 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]))

View File

@@ -0,0 +1 @@
This directory consists of simple read only python client tests.

View File

@@ -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)

View 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)

View 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()

View 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

View File

@@ -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
View 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
View File

379
watcher/db/api.py Normal file
View 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
View 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