adopt ruff-format

autopep8 is now replaced by ruff-format as the code
formatter

Assisted-By: claude-code sonnet 4.6
Change-Id: If237599df59552d7aff9726be8af5d780f077e9a
Signed-off-by: Sean Mooney <work@seanmooney.info>
This commit is contained in:
Sean Mooney
2026-03-20 19:31:34 +00:00
parent c66dceb705
commit e19327e780
401 changed files with 24071 additions and 15629 deletions
+6 -10
View File
@@ -29,6 +29,12 @@ repos:
hooks:
- id: remove-tabs
exclude: '.*\.(svg)$'
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.1
hooks:
- id: ruff-check
args: ['--fix', '--unsafe-fixes']
- id: ruff-format
- repo: https://opendev.org/openstack/hacking
rev: 7.0.0
hooks:
@@ -40,16 +46,6 @@ repos:
hooks:
- id: bandit
args: ['-c', 'pyproject.toml']
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.1
hooks:
- id: ruff-check
args: ['--fix', '--unsafe-fixes']
- repo: https://github.com/hhatto/autopep8
rev: v2.3.2
hooks:
- id: autopep8
files: '^.*\.py$'
- repo: https://github.com/codespell-project/codespell
rev: v2.4.1
hooks:
+9 -9
View File
@@ -22,10 +22,7 @@
# All configuration values have a default; values that are commented out
# serve to show the default.
extensions = [
'openstackdocstheme',
'os_api_ref',
]
extensions = ['openstackdocstheme', 'os_api_ref']
# -- General configuration ----------------------------------------------------
@@ -60,9 +57,7 @@ html_theme = 'openstackdocs'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
html_theme_options = {
"sidebar_mode": "toc",
}
html_theme_options = {"sidebar_mode": "toc"}
# -- Options for LaTeX output -------------------------------------------------
@@ -70,6 +65,11 @@ html_theme_options = {
# (source start file, target name, title, author, documentclass
# [howto/manual]).
latex_documents = [
('index', 'Watcher.tex', 'Infrastructure Optimization API Reference',
'OpenStack Foundation', 'manual'),
(
'index',
'Watcher.tex',
'Infrastructure Optimization API Reference',
'OpenStack Foundation',
'manual',
)
]
+27 -8
View File
@@ -24,12 +24,29 @@ from watcher.version import version_string
class BaseWatcherDirective(rst.Directive):
def __init__(self, name, arguments, options, content, lineno,
content_offset, block_text, state, state_machine):
def __init__(
self,
name,
arguments,
options,
content,
lineno,
content_offset,
block_text,
state,
state_machine,
):
super().__init__(
name, arguments, options, content, lineno,
content_offset, block_text, state, state_machine)
name,
arguments,
options,
content,
lineno,
content_offset,
block_text,
state,
state_machine,
)
self.result = statemachine.ViewList()
def run(self):
@@ -143,7 +160,8 @@ class WatcherFunc(BaseWatcherDirective):
error = self.state_machine.reporter.error(
f'The "{self.name}" directive is empty; content required.',
nodes.literal_block(self.block_text, self.block_text),
line=self.lineno)
line=self.lineno,
)
return [error]
func_path = self.content[0]
@@ -164,8 +182,9 @@ class WatcherFunc(BaseWatcherDirective):
self.add_textblock(textblock)
try:
node_class = getattr(nodes,
self.options.get('format', 'paragraph'))
node_class = getattr(
nodes, self.options.get('format', 'paragraph')
)
except Exception as exc:
raise self.error(exc)
+20 -15
View File
@@ -28,7 +28,6 @@ from watcher.objects import base
class VersionedNotificationDirective(Directive):
SAMPLE_ROOT = 'doc/notification_samples/'
TOGGLE_SCRIPT = """
<script>
@@ -51,15 +50,16 @@ jQuery(document).ready(function(){
ovos = base.WatcherObjectRegistry.obj_classes()
for name, cls in ovos.items():
cls = cls[0]
if (issubclass(cls, notification.NotificationBase) and
cls != notification.NotificationBase):
if (
issubclass(cls, notification.NotificationBase)
and cls != notification.NotificationBase
):
payload_name = cls.fields['payload'].objname
payload_cls = ovos[payload_name][0]
for sample in cls.samples:
notifications.append((cls.__name__,
payload_cls.__name__,
sample))
notifications.append(
(cls.__name__, payload_cls.__name__, sample)
)
return sorted(notifications)
def _build_markup(self, notifications):
@@ -90,7 +90,7 @@ jQuery(document).ready(function(){
# fill the table content, one notification per row
for name, payload, sample_file in notifications:
event_type = sample_file[0: -5].replace('-', '.')
event_type = sample_file[0:-5].replace('-', '.')
row = nodes.row()
body.append(row)
@@ -115,11 +115,15 @@ jQuery(document).ready(function(){
with open(self.SAMPLE_ROOT + sample_file) as f:
sample_content = f.read()
event_type = sample_file[0: -5]
html_str = self.TOGGLE_SCRIPT % ((event_type, ) * 3)
html_str += (f"<input type='button' id='{event_type}-hideshow' "
"value='hide/show sample'>")
html_str += (f"<div id='{event_type}-div'><pre>{sample_content}</pre></div>")
event_type = sample_file[0:-5]
html_str = self.TOGGLE_SCRIPT % ((event_type,) * 3)
html_str += (
f"<input type='button' id='{event_type}-hideshow' "
"value='hide/show sample'>"
)
html_str += (
f"<div id='{event_type}-div'><pre>{sample_content}</pre></div>"
)
raw = nodes.raw('', html_str, format="html")
col.append(raw)
@@ -128,5 +132,6 @@ jQuery(document).ready(function(){
def setup(app):
app.add_directive('versioned_notifications',
VersionedNotificationDirective)
app.add_directive(
'versioned_notifications', VersionedNotificationDirective
)
+32 -15
View File
@@ -45,9 +45,9 @@ extensions = [
]
wsme_protocols = ['restjson']
config_generator_config_file = [(
'../../etc/watcher/oslo-config-generator/watcher.conf',
'_static/watcher')]
config_generator_config_file = [
('../../etc/watcher/oslo-config-generator/watcher.conf', '_static/watcher')
]
sample_config_basename = 'watcher'
# The suffix of source filenames.
@@ -92,14 +92,28 @@ pygments_style = 'native'
# List of tuples 'sourcefile', 'target', u'title', u'Authors name', 'manual'
man_pages = [
('man/watcher-api', 'watcher-api', 'Watcher API Server',
['OpenStack'], 1),
('man/watcher-applier', 'watcher-applier', 'Watcher Applier',
['OpenStack'], 1),
('man/watcher-db-manage', 'watcher-db-manage',
'Watcher Db Management Utility', ['OpenStack'], 1),
('man/watcher-decision-engine', 'watcher-decision-engine',
'Watcher Decision Engine', ['OpenStack'], 1),
('man/watcher-api', 'watcher-api', 'Watcher API Server', ['OpenStack'], 1),
(
'man/watcher-applier',
'watcher-applier',
'Watcher Applier',
['OpenStack'],
1,
),
(
'man/watcher-db-manage',
'watcher-db-manage',
'Watcher Db Management Utility',
['OpenStack'],
1,
),
(
'man/watcher-decision-engine',
'watcher-decision-engine',
'Watcher Decision Engine',
['OpenStack'],
1,
),
]
# -- Options for HTML output --------------------------------------------------
@@ -127,10 +141,13 @@ openstackdocs_bug_tag = ''
# (source start file, target name, title, author, documentclass
# [howto/manual]).
latex_documents = [
('index',
'doc-watcher.tex',
'Watcher Documentation',
'OpenStack Foundation', 'manual'),
(
'index',
'doc-watcher.tex',
'Watcher Documentation',
'OpenStack Foundation',
'manual',
)
]
# If false, no module index is generated.
+18 -11
View File
@@ -35,8 +35,7 @@
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['reno.sphinxext',
'openstackdocstheme']
extensions = ['reno.sphinxext', 'openstackdocstheme']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
@@ -183,10 +182,8 @@ htmlhelp_basename = 'watcherdoc'
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
# 'preamble': '',
}
@@ -194,8 +191,13 @@ latex_elements = {
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual])
latex_documents = [
('index', 'watcher.tex', 'Watcher Documentation',
'Watcher developers', 'manual'),
(
'index',
'watcher.tex',
'Watcher Documentation',
'Watcher developers',
'manual',
)
]
# The name of an image file (relative to this directory) to place at the top of
@@ -224,8 +226,7 @@ latex_documents = [
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'watcher', 'Watcher Documentation',
['Watcher developers'], 1)
('index', 'watcher', 'Watcher Documentation', ['Watcher developers'], 1)
]
# If true, show URL addresses after external links.
@@ -238,9 +239,15 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'watcher', 'Watcher Documentation',
'Watcher developers', 'watcher', 'One line description of project.',
'Miscellaneous'),
(
'index',
'watcher',
'Watcher Documentation',
'Watcher developers',
'watcher',
'One line description of project.',
'Miscellaneous',
)
]
# Documents to append as an appendix to all manuals.
+1 -3
View File
@@ -16,6 +16,4 @@
import setuptools
setuptools.setup(
setup_requires=['pbr>=2.0.0'],
pbr=True)
setuptools.setup(setup_requires=['pbr>=2.0.0'], pbr=True)
+2 -1
View File
@@ -134,11 +134,12 @@ commands =
[flake8]
filename = *.py,app.wsgi
show-source=True
select = H,N
# W504 line break after binary operator
# H301 one import per line incorrectly triggers when
# imports need to be wrapped in () due to line length
# H306 ruff handles imports
ignore= H105,E123,E226,N320,H202,W504,W503,H306,H301
ignore= H105,H202,H306,H301,N320
builtins= _
enable-extensions = H106,H203,H904
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,*sqlalchemy/alembic/versions/*,demo/,releasenotes
+5 -3
View File
@@ -36,6 +36,8 @@ def install(app, conf, public_routes):
"""
if not CONF.get('enable_authentication'):
return app
return auth_token.AuthTokenMiddleware(app,
conf=dict(conf.keystone_authtoken),
public_api_routes=public_routes)
return auth_token.AuthTokenMiddleware(
app,
conf=dict(conf.keystone_authtoken),
public_api_routes=public_routes,
)
+1 -1
View File
@@ -46,7 +46,7 @@ def setup_app(config=None):
logging=getattr(config, 'logging', {}),
debug=CONF.debug,
wrap_app=_wrap_app,
**app_conf
**app_conf,
)
return acl.install(app, CONF, config.app.acl_public_routes)
-1
View File
@@ -15,7 +15,6 @@
Use this file for deploying the API service under Apache2 mod_wsgi.
"""
# This script is deprecated and it will be removed in U release.
# Please switch to automatically generated watcher-api-wsgi script instead.
from watcher.api import wsgi
+4 -16
View File
@@ -19,10 +19,7 @@ from watcher.api import hooks
# Server Specific Configurations
# See https://pecan.readthedocs.org/en/latest/configuration.html#server-configuration # noqa
server = {
'port': '9322',
'host': '127.0.0.1'
}
server = {'port': '9322', 'host': '127.0.0.1'}
# Pecan Application Configurations
# See https://pecan.readthedocs.org/en/latest/configuration.html#application-configuration # noqa
@@ -33,10 +30,7 @@ if not cfg.CONF.api.get("enable_webhooks_auth"):
app = {
'root': 'watcher.api.controllers.root.RootController',
'modules': ['watcher.api'],
'hooks': [
hooks.ContextHook(),
hooks.NoExceptionTracebackHook(),
],
'hooks': [hooks.ContextHook(), hooks.NoExceptionTracebackHook()],
'static_root': '%(confdir)s/public',
'enable_acl': True,
'acl_public_routes': acl_public_routes,
@@ -44,12 +38,6 @@ app = {
# WSME Configurations
# See https://wsme.readthedocs.org/en/latest/integrate.html#configuration
wsme = {
'debug': cfg.CONF.get("debug") if "debug" in cfg.CONF else False,
}
wsme = {'debug': cfg.CONF.get("debug") if "debug" in cfg.CONF else False}
PECAN_CONFIG = {
"server": server,
"app": app,
"wsme": wsme,
}
PECAN_CONFIG = {"server": server, "app": app, "wsme": wsme}
+11 -9
View File
@@ -23,7 +23,6 @@ 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"""
@@ -35,10 +34,11 @@ class APIBase(wtypes.Base):
def as_dict(self):
"""Render this object as a dict of its fields."""
return {k: getattr(self, k)
for k in self.fields
if hasattr(self, k) and
getattr(self, k) != wsme.Unset}
return {
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.
@@ -77,7 +77,8 @@ class Version:
"""
(self.major, self.minor) = Version.parse_headers(
headers, default_version, latest_version)
headers, default_version, latest_version
)
def __repr__(self):
return f'{self.major}.{self.minor}'
@@ -94,8 +95,8 @@ class Version:
"""
version_str = microversion_parse.get_version(
headers,
service_type='infra-optim')
headers, service_type='infra-optim'
)
minimal_version = (1, 0)
@@ -122,7 +123,8 @@ class Version:
if len(version) != 2:
raise exc.HTTPNotAcceptable(
f"Invalid value for {Version.string} header")
f"Invalid value for {Version.string} header"
)
return version
def __gt__(self, other):
+16 -7
View File
@@ -46,15 +46,24 @@ class Link(base.APIBase):
"""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)
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")
sample = cls(
href="http://localhost:6385/chassis/"
"eaaca217-e7d8-47b4-bb41-3f99f20eed89",
rel="bookmark",
)
return sample
+10 -8
View File
@@ -59,14 +59,15 @@ class Version(base.APIBase):
version.status = status
version.max_version = v.max_version_string()
version.min_version = v.min_version_string()
version.links = [link.Link.make_link('self',
pecan.request.application_url,
id, '', bookmark=True)]
version.links = [
link.Link.make_link(
'self', pecan.request.application_url, id, '', bookmark=True
)
]
return version
class Root(base.APIBase):
name = wtypes.text
"""The name of the API"""
@@ -83,16 +84,17 @@ class Root(base.APIBase):
def convert():
root = Root()
root.name = "OpenStack Watcher API"
root.description = ("Watcher is an OpenStack project which aims to "
"improve physical resources usage through "
"better VM placement.")
root.description = (
"Watcher is an OpenStack project which aims 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"""
+101 -86
View File
@@ -47,20 +47,29 @@ from watcher.api.controllers.v1 import webhooks
def min_version():
return base.Version(
{base.Version.string: ' '.join([versions.service_type_string(),
versions.min_version_string()])},
versions.min_version_string(), versions.max_version_string())
{
base.Version.string: ' '.join(
[versions.service_type_string(), versions.min_version_string()]
)
},
versions.min_version_string(),
versions.max_version_string(),
)
def max_version():
return base.Version(
{base.Version.string: ' '.join([versions.service_type_string(),
versions.max_version_string()])},
versions.min_version_string(), versions.max_version_string())
{
base.Version.string: ' '.join(
[versions.service_type_string(), versions.max_version_string()]
)
},
versions.min_version_string(),
versions.max_version_string(),
)
class APIBase(wtypes.Base):
created_at = wsme.wsattr(datetime.datetime, readonly=True)
"""The time in UTC at which the object is created"""
@@ -72,10 +81,11 @@ class APIBase(wtypes.Base):
def as_dict(self):
"""Render this object as a dict of its fields."""
return {k: getattr(self, k)
for k in self.fields
if hasattr(self, k) and
getattr(self, k) != wsme.Unset}
return {
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.
@@ -143,77 +153,74 @@ class V1(APIBase):
v1 = V1()
v1.id = "v1"
base_url = pecan.request.application_url
v1.links = [link.Link.make_link('self', base_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',
base_url,
'audit_templates', ''),
link.Link.make_link('bookmark',
base_url,
'audit_templates', '',
bookmark=True)
]
v1.audits = [link.Link.make_link('self', base_url,
'audits', ''),
link.Link.make_link('bookmark',
base_url,
'audits', '',
bookmark=True)
]
v1.links = [
link.Link.make_link('self', base_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', base_url, 'audit_templates', ''),
link.Link.make_link(
'bookmark', base_url, 'audit_templates', '', bookmark=True
),
]
v1.audits = [
link.Link.make_link('self', base_url, 'audits', ''),
link.Link.make_link(
'bookmark', base_url, 'audits', '', bookmark=True
),
]
if utils.allow_list_datamodel():
v1.data_model = [link.Link.make_link('self', base_url,
'data_model', ''),
link.Link.make_link('bookmark',
base_url,
'data_model', '',
bookmark=True)
]
v1.actions = [link.Link.make_link('self', base_url,
'actions', ''),
link.Link.make_link('bookmark',
base_url,
'actions', '',
bookmark=True)
]
v1.action_plans = [link.Link.make_link(
'self', base_url, 'action_plans', ''),
link.Link.make_link('bookmark',
base_url,
'action_plans', '',
bookmark=True)
v1.data_model = [
link.Link.make_link('self', base_url, 'data_model', ''),
link.Link.make_link(
'bookmark', base_url, 'data_model', '', bookmark=True
),
]
v1.actions = [
link.Link.make_link('self', base_url, 'actions', ''),
link.Link.make_link(
'bookmark', base_url, 'actions', '', bookmark=True
),
]
v1.action_plans = [
link.Link.make_link('self', base_url, 'action_plans', ''),
link.Link.make_link(
'bookmark', base_url, 'action_plans', '', bookmark=True
),
]
v1.scoring_engines = [link.Link.make_link(
'self', base_url, 'scoring_engines', ''),
link.Link.make_link('bookmark',
base_url,
'scoring_engines', '',
bookmark=True)
]
v1.scoring_engines = [
link.Link.make_link('self', base_url, 'scoring_engines', ''),
link.Link.make_link(
'bookmark', base_url, 'scoring_engines', '', bookmark=True
),
]
v1.services = [link.Link.make_link(
'self', base_url, 'services', ''),
link.Link.make_link('bookmark',
base_url,
'services', '',
bookmark=True)
]
v1.services = [
link.Link.make_link('self', base_url, 'services', ''),
link.Link.make_link(
'bookmark', base_url, 'services', '', bookmark=True
),
]
if utils.allow_webhook_api():
v1.webhooks = [link.Link.make_link(
'self', base_url, 'webhooks', ''),
link.Link.make_link('bookmark',
base_url,
'webhooks', '',
bookmark=True)
]
v1.webhooks = [
link.Link.make_link('self', base_url, 'webhooks', ''),
link.Link.make_link(
'bookmark', base_url, 'webhooks', '', bookmark=True
),
]
return v1
@@ -249,7 +256,8 @@ class Controller(rest.RestController):
f"Mutually exclusive versions requested. Version {version} "
"requested but not supported by this service. The supported "
f"version range is: [{min_ver}, {max_ver}].",
headers=headers)
headers=headers,
)
# ensure the minor version is within the supported range
if version < min_version() or version > max_version():
min_ver = versions.min_version_string()
@@ -259,12 +267,16 @@ class Controller(rest.RestController):
"not "
"supported by this service. The supported version range is: "
f"[{min_ver}, {max_ver}].",
headers=headers)
headers=headers,
)
@pecan.expose()
def _route(self, args, request=None):
v = base.Version(pecan.request.headers, versions.min_version_string(),
versions.max_version_string())
v = base.Version(
pecan.request.headers,
versions.min_version_string(),
versions.max_version_string(),
)
# The Vary header is used as a hint to caching proxies and user agents
# that the response is also dependent on the OpenStack-API-Version and
@@ -273,17 +285,20 @@ class Controller(rest.RestController):
# Always set the min and max headers
pecan.response.headers[base.Version.min_string] = (
versions.min_version_string())
versions.min_version_string()
)
pecan.response.headers[base.Version.max_string] = (
versions.max_version_string())
versions.max_version_string()
)
# assert that requested version is supported
self._check_version(v, pecan.response.headers)
pecan.response.headers[base.Version.string] = (
' '.join([versions.service_type_string(), str(v)]))
pecan.response.headers[base.Version.string] = ' '.join(
[versions.service_type_string(), str(v)]
)
pecan.request.version = v
return super()._route(args, request)
__all__ = ("Controller", )
__all__ = ("Controller",)
+180 -98
View File
@@ -89,7 +89,6 @@ def hide_fields_in_newer_versions(obj):
class ActionPatchType(types.JsonPatchType):
@staticmethod
def _validate_state(patch):
serialized_patch = {'path': patch.path, 'op': patch.op}
@@ -100,7 +99,8 @@ class ActionPatchType(types.JsonPatchType):
if state_value and not hasattr(objects.action.State, state_value):
msg = _("Invalid state: %(state)s")
raise exception.PatchError(
patch=serialized_patch, reason=msg % dict(state=state_value))
patch=serialized_patch, reason=msg % dict(state=state_value)
)
@staticmethod
def validate(patch):
@@ -126,6 +126,7 @@ class Action(base.APIBase):
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
def _get_action_plan_uuid(self):
@@ -137,7 +138,8 @@ class Action(base.APIBase):
elif value and self._action_plan_uuid != value:
try:
action_plan = objects.ActionPlan.get(
pecan.request.context, value)
pecan.request.context, value
)
self._action_plan_uuid = action_plan.uuid
self.action_plan_id = action_plan.id
except exception.ActionPlanNotFound:
@@ -146,9 +148,12 @@ class Action(base.APIBase):
uuid = wtypes.wsattr(types.uuid, readonly=True)
"""Unique UUID for this action"""
action_plan_uuid = wtypes.wsproperty(types.uuid, _get_action_plan_uuid,
_set_action_plan_uuid,
mandatory=True)
action_plan_uuid = wtypes.wsproperty(
types.uuid,
_get_action_plan_uuid,
_set_action_plan_uuid,
mandatory=True,
)
"""The action plan this action belongs to """
state = wtypes.text
@@ -187,22 +192,32 @@ class Action(base.APIBase):
self.fields.append('action_plan_id')
self.fields.append('description')
setattr(self, 'action_plan_uuid', kwargs.get('action_plan_id',
wtypes.Unset))
setattr(
self,
'action_plan_uuid',
kwargs.get('action_plan_id', wtypes.Unset),
)
@staticmethod
def _convert_with_links(action, url, expand=True):
if not expand:
action.unset_fields_except(['uuid', 'state', 'action_plan_uuid',
'action_plan_id', 'action_type',
'parents'])
action.unset_fields_except(
[
'uuid',
'state',
'action_plan_uuid',
'action_plan_id',
'action_type',
'parents',
]
)
action.links = [link.Link.make_link('self', url,
'actions', action.uuid),
link.Link.make_link('bookmark', url,
'actions', action.uuid,
bookmark=True)
]
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
@@ -210,7 +225,8 @@ class Action(base.APIBase):
action = Action(**action.as_dict())
try:
obj_action_desc = objects.ActionDescription.get_by_type(
pecan.request.context, action.action_type)
pecan.request.context, action.action_type
)
description = obj_action_desc.description
except exception.ActionDescriptionNotFound:
description = ""
@@ -222,13 +238,15 @@ class Action(base.APIBase):
@classmethod
def sample(cls, expand=True):
sample = cls(uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c',
description='action description',
state='PENDING',
created_at=timeutils.utcnow(),
deleted_at=None,
updated_at=timeutils.utcnow(),
parents=[])
sample = cls(
uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c',
description='action description',
state='PENDING',
created_at=timeutils.utcnow(),
deleted_at=None,
updated_at=timeutils.utcnow(),
parents=[],
)
sample._action_plan_uuid = '7ae81bb3-dec3-4289-8d6c-da80bd8001ae'
return cls._convert_with_links(sample, 'http://localhost:9322', expand)
@@ -243,12 +261,11 @@ class ActionCollection(collection.Collection):
self._type = 'actions'
@staticmethod
def convert_with_links(actions, limit, url=None, expand=False,
**kwargs):
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]
collection.actions = [
Action.convert_with_links(p, expand) for p in actions
]
collection.next = collection.get_next(limit, url=url, **kwargs)
return collection
@@ -265,25 +282,32 @@ class ActionsController(rest.RestController):
def __init__(self):
super().__init__()
_custom_actions = {
'detail': ['GET'],
}
_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):
def _get_actions_collection(
self,
marker,
limit,
sort_key,
sort_dir,
expand=False,
resource_url=None,
action_plan_uuid=None,
audit_uuid=None,
):
additional_fields = ['action_plan_uuid']
api_utils.validate_sort_key(sort_key, list(objects.Action.fields) +
additional_fields)
api_utils.validate_sort_key(
sort_key, list(objects.Action.fields) + additional_fields
)
limit = api_utils.validate_limit(limit)
api_utils.validate_sort_dir(sort_dir)
marker_obj = None
if marker:
marker_obj = objects.Action.get_by_uuid(pecan.request.context,
marker)
marker_obj = objects.Action.get_by_uuid(
pecan.request.context, marker
)
filters = {}
if action_plan_uuid:
@@ -292,33 +316,54 @@ class ActionsController(rest.RestController):
if audit_uuid:
filters['audit_uuid'] = audit_uuid
need_api_sort = api_utils.check_need_api_sort(sort_key,
additional_fields)
sort_db_key = (sort_key if not need_api_sort
else None)
need_api_sort = api_utils.check_need_api_sort(
sort_key, additional_fields
)
sort_db_key = sort_key if not need_api_sort else None
actions = objects.Action.list(pecan.request.context,
limit,
marker_obj, sort_key=sort_db_key,
sort_dir=sort_dir,
filters=filters)
actions = objects.Action.list(
pecan.request.context,
limit,
marker_obj,
sort_key=sort_db_key,
sort_dir=sort_dir,
filters=filters,
)
actions_collection = ActionCollection.convert_with_links(
actions, limit, url=resource_url, expand=expand,
sort_key=sort_key, sort_dir=sort_dir)
actions,
limit,
url=resource_url,
expand=expand,
sort_key=sort_key,
sort_dir=sort_dir,
)
if need_api_sort:
api_utils.make_api_sort(actions_collection.actions,
sort_key, sort_dir)
api_utils.make_api_sort(
actions_collection.actions, sort_key, sort_dir
)
return actions_collection
@wsme_pecan.wsexpose(ActionCollection, types.uuid, int,
wtypes.text, wtypes.text, types.uuid,
types.uuid)
def get_all(self, marker=None, limit=None,
sort_key='id', sort_dir='asc', action_plan_uuid=None,
audit_uuid=None):
@wsme_pecan.wsexpose(
ActionCollection,
types.uuid,
int,
wtypes.text,
wtypes.text,
types.uuid,
types.uuid,
)
def get_all(
self,
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.
@@ -331,22 +376,38 @@ class ActionsController(rest.RestController):
to get only actions for that audit.
"""
context = pecan.request.context
policy.enforce(context, 'action:get_all',
action='action:get_all')
policy.enforce(context, 'action:get_all', action='action:get_all')
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)
marker,
limit,
sort_key,
sort_dir,
action_plan_uuid=action_plan_uuid,
audit_uuid=audit_uuid,
)
@wsme_pecan.wsexpose(ActionCollection, types.uuid, int,
wtypes.text, wtypes.text, types.uuid,
types.uuid)
def detail(self, marker=None, limit=None,
sort_key='id', sort_dir='asc', action_plan_uuid=None,
audit_uuid=None):
@wsme_pecan.wsexpose(
ActionCollection,
types.uuid,
int,
wtypes.text,
wtypes.text,
types.uuid,
types.uuid,
)
def detail(
self,
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 marker: pagination marker for large data sets.
@@ -359,8 +420,7 @@ class ActionsController(rest.RestController):
to get only actions for that audit.
"""
context = pecan.request.context
policy.enforce(context, 'action:detail',
action='action:detail')
policy.enforce(context, 'action:detail', action='action:detail')
# NOTE(lucasagomes): /detail should only work against collections
parent = pecan.request.path.split('/')[:-1][-1]
@@ -373,8 +433,15 @@ class ActionsController(rest.RestController):
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)
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):
@@ -398,13 +465,16 @@ class ActionsController(rest.RestController):
"""
if not api_utils.allow_skipped_action():
raise exception.Invalid(
_("API microversion 1.5 or higher is required."))
_("API microversion 1.5 or higher is required.")
)
context = pecan.request.context
action_to_update = api_utils.get_resource(
'Action', action_uuid, eager=True)
policy.enforce(context, 'action:update', action_to_update,
action='action:update')
'Action', action_uuid, eager=True
)
policy.enforce(
context, 'action:update', action_to_update, action='action:update'
)
try:
action_dict = action_to_update.as_dict()
@@ -414,47 +484,59 @@ class ActionsController(rest.RestController):
# Define allowed state transitions for actions
allowed_patch_transitions = [
(objects.action.State.PENDING, objects.action.State.SKIPPED),
(objects.action.State.PENDING, objects.action.State.SKIPPED)
]
# Validate state transitions if state is being modified
if action.state != action_to_update.state:
transition = (action_to_update.state, action.state)
if transition not in allowed_patch_transitions:
error_message = _("State transition not allowed: "
"(%(initial_state)s -> %(new_state)s)")
error_message = _(
"State transition not allowed: "
"(%(initial_state)s -> %(new_state)s)"
)
raise exception.Conflict(
patch=patch,
message=error_message % dict(
message=error_message
% dict(
initial_state=action_to_update.state,
new_state=action.state))
new_state=action.state,
),
)
action_plan = action_to_update.action_plan
if action_plan.state not in [objects.action_plan.State.RECOMMENDED,
objects.action_plan.State.PENDING]:
error_message = _("State update not allowed for actionplan "
"state: %(ap_state)s")
if action_plan.state not in [
objects.action_plan.State.RECOMMENDED,
objects.action_plan.State.PENDING,
]:
error_message = _(
"State update not allowed for actionplan "
"state: %(ap_state)s"
)
raise exception.Conflict(
patch=patch,
message=error_message % dict(
ap_state=action_plan.state))
message=error_message % dict(ap_state=action_plan.state),
)
status_message = _("Action skipped by user.")
# status_message update only allowed with status update or when
# already SKIPPED
# NOTE(dviroel): status_message is an exposed field.
if action.status_message != action_to_update.status_message:
if (action.state == action_to_update.state and
action_to_update.state != objects.action.State.SKIPPED):
if (
action.state == action_to_update.state
and action_to_update.state != objects.action.State.SKIPPED
):
error_message = _(
"status_message update only allowed when action state "
"is SKIPPED")
raise exception.Conflict(
patch=patch,
message=error_message)
"is SKIPPED"
)
raise exception.Conflict(patch=patch, message=error_message)
else:
status_message = (_("%(status_message)s Reason: %(reason)s")
% dict(status_message=status_message,
reason=action.status_message))
status_message = _(
"%(status_message)s Reason: %(reason)s"
) % dict(
status_message=status_message, reason=action.status_message
)
action.status_message = status_message
+232 -122
View File
@@ -94,7 +94,6 @@ def hide_fields_in_newer_versions(obj):
class ActionPlanPatchType(types.JsonPatchType):
@staticmethod
def _validate_state(patch):
serialized_patch = {'path': patch.path, 'op': patch.op}
@@ -105,7 +104,8 @@ class ActionPlanPatchType(types.JsonPatchType):
if state_value and not hasattr(ap_objects.State, state_value):
msg = _("Invalid state: %(state)s")
raise exception.PatchError(
patch=serialized_patch, reason=msg % dict(state=state_value))
patch=serialized_patch, reason=msg % dict(state=state_value)
)
@staticmethod
def validate(patch):
@@ -164,7 +164,8 @@ class ActionPlan(base.APIBase):
try:
_efficacy_indicators = objects.EfficacyIndicator.list(
pecan.request.context,
filters={"action_plan_uuid": self.uuid})
filters={"action_plan_uuid": self.uuid},
)
for indicator in _efficacy_indicators:
efficacy_indicator = efficacyindicator.EfficacyIndicator(
@@ -187,11 +188,11 @@ class ActionPlan(base.APIBase):
strategy = None
try:
if utils.is_uuid_like(value) or utils.is_int_like(value):
strategy = objects.Strategy.get(
pecan.request.context, value)
strategy = objects.Strategy.get(pecan.request.context, value)
else:
strategy = objects.Strategy.get_by_name(
pecan.request.context, value)
pecan.request.context, value
)
except exception.StrategyNotFound:
pass
if strategy:
@@ -221,22 +222,27 @@ class ActionPlan(base.APIBase):
uuid = wtypes.wsattr(types.uuid, readonly=True)
"""Unique UUID for this action plan"""
audit_uuid = wtypes.wsproperty(types.uuid, _get_audit_uuid,
_set_audit_uuid,
mandatory=True)
audit_uuid = wtypes.wsproperty(
types.uuid, _get_audit_uuid, _set_audit_uuid, mandatory=True
)
"""The UUID of the audit this port belongs to"""
strategy_uuid = wtypes.wsproperty(
wtypes.text, _get_strategy_uuid, _set_strategy_uuid, mandatory=False)
wtypes.text, _get_strategy_uuid, _set_strategy_uuid, mandatory=False
)
"""Strategy UUID the action plan refers to"""
strategy_name = wtypes.wsproperty(
wtypes.text, _get_strategy_name, _set_strategy_name, mandatory=False)
wtypes.text, _get_strategy_name, _set_strategy_name, mandatory=False
)
"""The name of the strategy this action plan refers to"""
efficacy_indicators = wtypes.wsproperty(
types.jsontype, _get_efficacy_indicators, _set_efficacy_indicators,
mandatory=True)
types.jsontype,
_get_efficacy_indicators,
_set_efficacy_indicators,
mandatory=True,
)
"""The list of efficacy indicators associated to this action plan"""
global_efficacy = wtypes.wsattr(types.jsontype, readonly=True)
@@ -278,40 +284,60 @@ class ActionPlan(base.APIBase):
def _convert_with_links(action_plan, url, expand=True):
if not expand:
action_plan.unset_fields_except(
['uuid', 'state', 'efficacy_indicators', 'global_efficacy',
'updated_at', 'audit_uuid', 'strategy_uuid', 'strategy_name'])
[
'uuid',
'state',
'efficacy_indicators',
'global_efficacy',
'updated_at',
'audit_uuid',
'strategy_uuid',
'strategy_name',
]
)
action_plan.links = [
link.Link.make_link('self', url, 'action_plans', action_plan.uuid),
link.Link.make_link(
'self', url,
'action_plans', action_plan.uuid),
link.Link.make_link(
'bookmark', url,
'action_plans', action_plan.uuid,
bookmark=True)]
'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())
hide_fields_in_newer_versions(action_plan)
return cls._convert_with_links(action_plan, pecan.request.host_url,
expand)
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=timeutils.utcnow(),
deleted_at=None,
updated_at=timeutils.utcnow())
sample = cls(
uuid='9ef4d84c-41e8-4418-9220-ce55be0436af',
state='ONGOING',
created_at=timeutils.utcnow(),
deleted_at=None,
updated_at=timeutils.utcnow(),
)
sample._audit_uuid = 'abcee106-14d3-4515-b744-5a26885cf6f6'
sample._efficacy_indicators = [{'description': 'Test indicator',
'name': 'test_indicator',
'unit': '%'}]
sample._global_efficacy = {'description': 'Global efficacy',
'name': 'test_global_efficacy',
'unit': '%'}
sample._efficacy_indicators = [
{
'description': 'Test indicator',
'name': 'test_indicator',
'unit': '%',
}
]
sample._global_efficacy = {
'description': 'Global efficacy',
'name': 'test_global_efficacy',
'unit': '%',
}
return cls._convert_with_links(sample, 'http://localhost:9322', expand)
@@ -325,11 +351,13 @@ class ActionPlanCollection(collection.Collection):
self._type = 'action_plans'
@staticmethod
def convert_with_links(rpc_action_plans, limit, url=None, expand=False,
**kwargs):
def convert_with_links(
rpc_action_plans, limit, url=None, expand=False, **kwargs
):
ap_collection = ActionPlanCollection()
ap_collection.action_plans = [ActionPlan.convert_with_links(
p, expand) for p in rpc_action_plans]
ap_collection.action_plans = [
ActionPlan.convert_with_links(p, expand) for p in rpc_action_plans
]
ap_collection.next = ap_collection.get_next(limit, url=url, **kwargs)
return ap_collection
@@ -347,26 +375,32 @@ class ActionPlansController(rest.RestController):
super().__init__()
self.applier_client = rpcapi.ApplierAPI()
_custom_actions = {
'start': ['POST'],
'detail': ['GET']
}
_custom_actions = {'start': ['POST'], 'detail': ['GET']}
def _get_action_plans_collection(self, marker, limit,
sort_key, sort_dir, expand=False,
resource_url=None, audit_uuid=None,
strategy=None):
def _get_action_plans_collection(
self,
marker,
limit,
sort_key,
sort_dir,
expand=False,
resource_url=None,
audit_uuid=None,
strategy=None,
):
additional_fields = ['audit_uuid', 'strategy_uuid', 'strategy_name']
api_utils.validate_sort_key(
sort_key, list(objects.ActionPlan.fields) + additional_fields)
sort_key, list(objects.ActionPlan.fields) + additional_fields
)
limit = api_utils.validate_limit(limit)
api_utils.validate_sort_dir(sort_dir)
marker_obj = None
if marker:
marker_obj = objects.ActionPlan.get_by_uuid(
pecan.request.context, marker)
pecan.request.context, marker
)
filters = {}
if audit_uuid:
@@ -378,31 +412,54 @@ class ActionPlansController(rest.RestController):
else:
filters['strategy_name'] = strategy
need_api_sort = api_utils.check_need_api_sort(sort_key,
additional_fields)
sort_db_key = (sort_key if not need_api_sort
else None)
need_api_sort = api_utils.check_need_api_sort(
sort_key, additional_fields
)
sort_db_key = sort_key if not need_api_sort else None
action_plans = objects.ActionPlan.list(
pecan.request.context,
limit,
marker_obj, sort_key=sort_db_key,
sort_dir=sort_dir, filters=filters)
marker_obj,
sort_key=sort_db_key,
sort_dir=sort_dir,
filters=filters,
)
action_plans_collection = ActionPlanCollection.convert_with_links(
action_plans, limit, url=resource_url, expand=expand,
sort_key=sort_key, sort_dir=sort_dir)
action_plans,
limit,
url=resource_url,
expand=expand,
sort_key=sort_key,
sort_dir=sort_dir,
)
if need_api_sort:
api_utils.make_api_sort(action_plans_collection.action_plans,
sort_key, sort_dir)
api_utils.make_api_sort(
action_plans_collection.action_plans, sort_key, sort_dir
)
return action_plans_collection
@wsme_pecan.wsexpose(ActionPlanCollection, types.uuid, int, wtypes.text,
wtypes.text, types.uuid, wtypes.text)
def get_all(self, marker=None, limit=None,
sort_key='id', sort_dir='asc', audit_uuid=None, strategy=None):
@wsme_pecan.wsexpose(
ActionPlanCollection,
types.uuid,
int,
wtypes.text,
wtypes.text,
types.uuid,
wtypes.text,
)
def get_all(
self,
marker=None,
limit=None,
sort_key='id',
sort_dir='asc',
audit_uuid=None,
strategy=None,
):
"""Retrieve a list of action plans.
:param marker: pagination marker for large data sets.
@@ -414,17 +471,37 @@ class ActionPlansController(rest.RestController):
:param strategy: strategy UUID or name to filter by
"""
context = pecan.request.context
policy.enforce(context, 'action_plan:get_all',
action='action_plan:get_all')
policy.enforce(
context, 'action_plan:get_all', action='action_plan:get_all'
)
return self._get_action_plans_collection(
marker, limit, sort_key, sort_dir,
audit_uuid=audit_uuid, strategy=strategy)
marker,
limit,
sort_key,
sort_dir,
audit_uuid=audit_uuid,
strategy=strategy,
)
@wsme_pecan.wsexpose(ActionPlanCollection, types.uuid, int, wtypes.text,
wtypes.text, types.uuid, wtypes.text)
def detail(self, marker=None, limit=None,
sort_key='id', sort_dir='asc', audit_uuid=None, strategy=None):
@wsme_pecan.wsexpose(
ActionPlanCollection,
types.uuid,
int,
wtypes.text,
wtypes.text,
types.uuid,
wtypes.text,
)
def detail(
self,
marker=None,
limit=None,
sort_key='id',
sort_dir='asc',
audit_uuid=None,
strategy=None,
):
"""Retrieve a list of action_plans with detail.
:param marker: pagination marker for large data sets.
@@ -436,8 +513,9 @@ class ActionPlansController(rest.RestController):
:param strategy: strategy UUID or name to filter by
"""
context = pecan.request.context
policy.enforce(context, 'action_plan:detail',
action='action_plan:detail')
policy.enforce(
context, 'action_plan:detail', action='action_plan:detail'
)
# NOTE(lucasagomes): /detail should only work against collections
parent = pecan.request.path.split('/')[:-1][-1]
@@ -447,8 +525,15 @@ class ActionPlansController(rest.RestController):
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, strategy=strategy)
marker,
limit,
sort_key,
sort_dir,
expand,
resource_url,
audit_uuid=audit_uuid,
strategy=strategy,
)
@wsme_pecan.wsexpose(ActionPlan, types.uuid)
def get_one(self, action_plan_uuid):
@@ -459,7 +544,8 @@ class ActionPlansController(rest.RestController):
context = pecan.request.context
action_plan = api_utils.get_resource('ActionPlan', action_plan_uuid)
policy.enforce(
context, 'action_plan:get', action_plan, action='action_plan:get')
context, 'action_plan:get', action_plan, action='action_plan:get'
)
return ActionPlan.convert_with_links(action_plan)
@@ -471,24 +557,29 @@ class ActionPlansController(rest.RestController):
"""
context = pecan.request.context
action_plan = api_utils.get_resource(
'ActionPlan', action_plan_uuid, eager=True)
policy.enforce(context, 'action_plan:delete', action_plan,
action='action_plan:delete')
'ActionPlan', action_plan_uuid, eager=True
)
policy.enforce(
context,
'action_plan:delete',
action_plan,
action='action_plan:delete',
)
allowed_states = (ap_objects.State.SUCCEEDED,
ap_objects.State.RECOMMENDED,
ap_objects.State.FAILED,
ap_objects.State.SUPERSEDED,
ap_objects.State.CANCELLED)
allowed_states = (
ap_objects.State.SUCCEEDED,
ap_objects.State.RECOMMENDED,
ap_objects.State.FAILED,
ap_objects.State.SUPERSEDED,
ap_objects.State.CANCELLED,
)
if action_plan.state not in allowed_states:
raise exception.DeleteError(
state=action_plan.state)
raise exception.DeleteError(state=action_plan.state)
action_plan.soft_delete()
@wsme.validate(types.uuid, [ActionPlanPatchType])
@wsme_pecan.wsexpose(ActionPlan, types.uuid,
body=[ActionPlanPatchType])
@wsme_pecan.wsexpose(ActionPlan, types.uuid, body=[ActionPlanPatchType])
def patch(self, action_plan_uuid, patch):
"""Update an existing action plan.
@@ -497,14 +588,20 @@ class ActionPlansController(rest.RestController):
"""
context = pecan.request.context
action_plan_to_update = api_utils.get_resource(
'ActionPlan', action_plan_uuid, eager=True)
policy.enforce(context, 'action_plan:update', action_plan_to_update,
action='action_plan:update')
'ActionPlan', action_plan_uuid, eager=True
)
policy.enforce(
context,
'action_plan:update',
action_plan_to_update,
action='action_plan:update',
)
try:
action_plan_dict = action_plan_to_update.as_dict()
action_plan = ActionPlan(**api_utils.apply_jsonpatch(
action_plan_dict, patch))
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)
@@ -513,27 +610,28 @@ class ActionPlansController(rest.RestController):
# transitions that are allowed via PATCH
allowed_patch_transitions = [
(ap_objects.State.RECOMMENDED,
ap_objects.State.PENDING),
(ap_objects.State.RECOMMENDED,
ap_objects.State.CANCELLED),
(ap_objects.State.ONGOING,
ap_objects.State.CANCELLING),
(ap_objects.State.PENDING,
ap_objects.State.CANCELLED),
(ap_objects.State.RECOMMENDED, ap_objects.State.PENDING),
(ap_objects.State.RECOMMENDED, ap_objects.State.CANCELLED),
(ap_objects.State.ONGOING, ap_objects.State.CANCELLING),
(ap_objects.State.PENDING, ap_objects.State.CANCELLED),
]
# todo: improve this in blueprint watcher-api-validation
if hasattr(action_plan, 'state'):
transition = (action_plan_to_update.state, action_plan.state)
if transition not in allowed_patch_transitions:
error_message = _("State transition not allowed: "
"(%(initial_state)s -> %(new_state)s)")
error_message = _(
"State transition not allowed: "
"(%(initial_state)s -> %(new_state)s)"
)
raise exception.PatchError(
patch=patch,
reason=error_message % dict(
reason=error_message
% dict(
initial_state=action_plan_to_update.state,
new_state=action_plan.state))
new_state=action_plan.state,
),
)
if action_plan.state == ap_objects.State.PENDING:
launch_action_plan = True
@@ -552,8 +650,10 @@ class ActionPlansController(rest.RestController):
if action_plan_to_update[field] != patch_val:
action_plan_to_update[field] = patch_val
if (field == 'state' and
patch_val == objects.action_plan.State.PENDING):
if (
field == 'state'
and patch_val == objects.action_plan.State.PENDING
):
launch_action_plan = True
action_plan_to_update.save()
@@ -562,19 +662,21 @@ class ActionPlansController(rest.RestController):
# state update action state here only
if cancel_action_plan:
filters = {'action_plan_uuid': action_plan.uuid}
actions = objects.Action.list(pecan.request.context,
filters=filters, eager=True)
actions = objects.Action.list(
pecan.request.context, filters=filters, eager=True
)
for a in actions:
a.state = objects.action.State.CANCELLED
a.save()
if launch_action_plan:
self.applier_client.launch_action_plan(pecan.request.context,
action_plan.uuid)
self.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)
pecan.request.context, action_plan_uuid
)
return ActionPlan.convert_with_links(action_plan_to_update)
@wsme_pecan.wsexpose(ActionPlan, types.uuid)
@@ -585,23 +687,31 @@ class ActionPlansController(rest.RestController):
"""
action_plan_to_start = api_utils.get_resource(
'ActionPlan', action_plan_uuid, eager=True)
'ActionPlan', action_plan_uuid, eager=True
)
context = pecan.request.context
policy.enforce(context, 'action_plan:start', action_plan_to_start,
action='action_plan:start')
policy.enforce(
context,
'action_plan:start',
action_plan_to_start,
action='action_plan:start',
)
if action_plan_to_start['state'] != \
objects.action_plan.State.RECOMMENDED:
raise exception.StartError(
state=action_plan_to_start.state)
if (
action_plan_to_start['state']
!= objects.action_plan.State.RECOMMENDED
):
raise exception.StartError(state=action_plan_to_start.state)
action_plan_to_start['state'] = objects.action_plan.State.PENDING
action_plan_to_start.save()
self.applier_client.launch_action_plan(pecan.request.context,
action_plan_uuid)
self.applier_client.launch_action_plan(
pecan.request.context, action_plan_uuid
)
action_plan_to_start = objects.ActionPlan.get_by_uuid(
pecan.request.context, action_plan_uuid)
pecan.request.context, action_plan_uuid
)
return ActionPlan.convert_with_links(action_plan_to_start)
+236 -159
View File
@@ -84,7 +84,6 @@ def hide_fields_in_newer_versions(obj):
class AuditPostType(wtypes.Base):
name = wtypes.wsattr(wtypes.text, mandatory=False)
audit_template_uuid = wtypes.wsattr(types.uuid, mandatory=False)
@@ -95,11 +94,13 @@ class AuditPostType(wtypes.Base):
audit_type = wtypes.wsattr(wtypes.text, mandatory=True)
state = wtypes.wsattr(wtypes.text, readonly=True,
default=objects.audit.State.PENDING)
state = wtypes.wsattr(
wtypes.text, readonly=True, default=objects.audit.State.PENDING
)
parameters = wtypes.wsattr({wtypes.text: types.jsontype}, mandatory=False,
default={})
parameters = wtypes.wsattr(
{wtypes.text: types.jsontype}, mandatory=False, default={}
)
interval = wtypes.wsattr(types.interval_or_cron, mandatory=False)
scope = wtypes.wsattr(types.jsontype, readonly=True)
@@ -120,28 +121,35 @@ class AuditPostType(wtypes.Base):
raise exception.AuditTypeNotFound(audit_type=self.audit_type)
if not self.audit_template_uuid and not self.goal:
message = _(
'A valid goal or audit_template_id must be provided')
message = _('A valid goal or audit_template_id must be provided')
raise exception.Invalid(message)
if (self.audit_type == objects.audit.AuditType.ONESHOT.value and
self.interval not in (wtypes.Unset, None)):
if (
self.audit_type == objects.audit.AuditType.ONESHOT.value
and self.interval not in (wtypes.Unset, None)
):
raise exception.AuditIntervalNotAllowed(audit_type=self.audit_type)
if (self.audit_type == objects.audit.AuditType.CONTINUOUS.value and
self.interval in (wtypes.Unset, None)):
if (
self.audit_type == objects.audit.AuditType.CONTINUOUS.value
and self.interval in (wtypes.Unset, None)
):
raise exception.AuditIntervalNotSpecified(
audit_type=self.audit_type)
audit_type=self.audit_type
)
if self.audit_template_uuid and self.goal:
raise exception.Invalid('Either audit_template_uuid '
'or goal should be provided.')
raise exception.Invalid(
'Either audit_template_uuid or goal should be provided.'
)
if (self.audit_type == objects.audit.AuditType.ONESHOT.value and
(self.start_time not in (wtypes.Unset, None) or
self.end_time not in (wtypes.Unset, None))):
if self.audit_type == objects.audit.AuditType.ONESHOT.value and (
self.start_time not in (wtypes.Unset, None)
or self.end_time not in (wtypes.Unset, None)
):
raise exception.AuditStartEndTimeNotAllowed(
audit_type=self.audit_type)
audit_type=self.audit_type
)
if not api_utils.allow_start_end_audit_time():
for field in ('start_time', 'end_time'):
@@ -154,11 +162,14 @@ class AuditPostType(wtypes.Base):
if self.audit_template_uuid:
try:
audit_template = objects.AuditTemplate.get(
context, self.audit_template_uuid)
context, self.audit_template_uuid
)
except exception.AuditTemplateNotFound:
raise exception.Invalid(
message=_('The audit template UUID or name specified is '
'invalid'))
message=_(
'The audit template UUID or name specified is invalid'
)
)
at2a = {
'goal': 'goal_id',
'strategy': 'strategy_id',
@@ -178,12 +189,14 @@ class AuditPostType(wtypes.Base):
# Note: If audit name was not provided, used a default name
if not self.name:
if self.strategy:
strategy = _get_object_by_value(context, objects.Strategy,
self.strategy)
strategy = _get_object_by_value(
context, objects.Strategy, self.strategy
)
self.name = f"{strategy.name}-{timeutils.utcnow().isoformat()}"
elif self.audit_template_uuid:
audit_template = objects.AuditTemplate.get(
context, self.audit_template_uuid)
context, self.audit_template_uuid
)
timestamp = timeutils.utcnow().isoformat()
self.name = f"{audit_template.name}-{timestamp}"
else:
@@ -191,8 +204,7 @@ class AuditPostType(wtypes.Base):
self.name = f"{goal.name}-{timeutils.utcnow().isoformat()}"
# No more than 63 characters
if len(self.name) > 63:
LOG.warning("Audit: %s length exceeds 63 characters",
self.name)
LOG.warning("Audit: %s length exceeds 63 characters", self.name)
self.name = self.name[0:63]
return Audit(
@@ -206,11 +218,11 @@ class AuditPostType(wtypes.Base):
auto_trigger=self.auto_trigger,
start_time=self.start_time,
end_time=self.end_time,
force=self.force)
force=self.force,
)
class AuditPatchType(types.JsonPatchType):
@staticmethod
def mandatory_attrs():
return ['/audit_template_uuid', '/type']
@@ -225,19 +237,21 @@ class AuditPatchType(types.JsonPatchType):
@staticmethod
def validate(patch):
def is_new_state_none(p):
return p.path == '/state' and p.op == 'replace' and p.value is None
serialized_patch = {'path': patch.path,
'op': patch.op,
'value': patch.value}
if (patch.path in AuditPatchType.mandatory_attrs() or
is_new_state_none(patch)):
serialized_patch = {
'path': patch.path,
'op': patch.op,
'value': patch.value,
}
if patch.path in AuditPatchType.mandatory_attrs() or is_new_state_none(
patch
):
msg = _("%(field)s can't be updated.")
raise exception.PatchError(
patch=serialized_patch,
reason=msg % dict(field=patch.path))
patch=serialized_patch, reason=msg % dict(field=patch.path)
)
return types.JsonPatchType.validate(patch)
@@ -247,6 +261,7 @@ class Audit(base.APIBase):
This class enforces type checking and value constraints, and converts
between the internal object model and the API representation of an audit.
"""
_goal_uuid = None
_goal_name = None
_strategy_uuid = None
@@ -258,11 +273,9 @@ class Audit(base.APIBase):
goal = None
try:
if utils.is_uuid_like(value) or utils.is_int_like(value):
goal = objects.Goal.get(
pecan.request.context, value)
goal = objects.Goal.get(pecan.request.context, value)
else:
goal = objects.Goal.get_by_name(
pecan.request.context, value)
goal = objects.Goal.get_by_name(pecan.request.context, value)
except exception.GoalNotFound:
pass
if goal:
@@ -295,11 +308,11 @@ class Audit(base.APIBase):
strategy = None
try:
if utils.is_uuid_like(value) or utils.is_int_like(value):
strategy = objects.Strategy.get(
pecan.request.context, value)
strategy = objects.Strategy.get(pecan.request.context, value)
else:
strategy = objects.Strategy.get_by_name(
pecan.request.context, value)
pecan.request.context, value
)
except exception.StrategyNotFound:
pass
if strategy:
@@ -339,19 +352,23 @@ class Audit(base.APIBase):
"""This audit state"""
goal_uuid = wtypes.wsproperty(
wtypes.text, _get_goal_uuid, _set_goal_uuid, mandatory=True)
wtypes.text, _get_goal_uuid, _set_goal_uuid, mandatory=True
)
"""Goal UUID the audit refers to"""
goal_name = wtypes.wsproperty(
wtypes.text, _get_goal_name, _set_goal_name, mandatory=False)
wtypes.text, _get_goal_name, _set_goal_name, mandatory=False
)
"""The name of the goal this audit refers to"""
strategy_uuid = wtypes.wsproperty(
wtypes.text, _get_strategy_uuid, _set_strategy_uuid, mandatory=False)
wtypes.text, _get_strategy_uuid, _set_strategy_uuid, mandatory=False
)
"""Strategy UUID the audit refers to"""
strategy_name = wtypes.wsproperty(
wtypes.text, _get_strategy_name, _set_strategy_name, mandatory=False)
wtypes.text, _get_strategy_name, _set_strategy_name, mandatory=False
)
"""The name of the strategy this audit refers to"""
parameters = {wtypes.text: types.jsontype}
@@ -401,33 +418,40 @@ class Audit(base.APIBase):
self.fields.append('goal_id')
self.fields.append('strategy_id')
fields.append('goal_uuid')
setattr(self, 'goal_uuid', kwargs.get('goal_id',
wtypes.Unset))
setattr(self, 'goal_uuid', kwargs.get('goal_id', wtypes.Unset))
fields.append('goal_name')
setattr(self, 'goal_name', kwargs.get('goal_id',
wtypes.Unset))
setattr(self, 'goal_name', kwargs.get('goal_id', wtypes.Unset))
fields.append('strategy_uuid')
setattr(self, 'strategy_uuid', kwargs.get('strategy_id',
wtypes.Unset))
setattr(self, 'strategy_uuid', kwargs.get('strategy_id', wtypes.Unset))
fields.append('strategy_name')
setattr(self, 'strategy_name', kwargs.get('strategy_id',
wtypes.Unset))
setattr(self, 'strategy_name', kwargs.get('strategy_id', wtypes.Unset))
@staticmethod
def _convert_with_links(audit, url, expand=True):
if not expand:
audit.unset_fields_except(['uuid', 'name', 'audit_type', 'state',
'goal_uuid', 'interval', 'scope',
'strategy_uuid', 'goal_name',
'strategy_name', 'auto_trigger',
'next_run_time'])
audit.unset_fields_except(
[
'uuid',
'name',
'audit_type',
'state',
'goal_uuid',
'interval',
'scope',
'strategy_uuid',
'goal_name',
'strategy_name',
'auto_trigger',
'next_run_time',
]
)
audit.links = [link.Link.make_link('self', url,
'audits', audit.uuid),
link.Link.make_link('bookmark', url,
'audits', audit.uuid,
bookmark=True)
]
audit.links = [
link.Link.make_link('self', url, 'audits', audit.uuid),
link.Link.make_link(
'bookmark', url, 'audits', audit.uuid, bookmark=True
),
]
return audit
@@ -439,19 +463,21 @@ class Audit(base.APIBase):
@classmethod
def sample(cls, expand=True):
sample = cls(uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c',
name='My Audit',
audit_type='ONESHOT',
state='PENDING',
created_at=timeutils.utcnow(),
deleted_at=None,
updated_at=timeutils.utcnow(),
interval='7200',
scope=[],
auto_trigger=False,
next_run_time=timeutils.utcnow(),
start_time=timeutils.utcnow(),
end_time=timeutils.utcnow())
sample = cls(
uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c',
name='My Audit',
audit_type='ONESHOT',
state='PENDING',
created_at=timeutils.utcnow(),
deleted_at=None,
updated_at=timeutils.utcnow(),
interval='7200',
scope=[],
auto_trigger=False,
next_run_time=timeutils.utcnow(),
start_time=timeutils.utcnow(),
end_time=timeutils.utcnow(),
)
sample.goal_id = '7ae81bb3-dec3-4289-8d6c-da80bd8001ae'
sample.strategy_id = '7ae81bb3-dec3-4289-8d6c-da80bd8001ff'
@@ -470,11 +496,13 @@ class AuditCollection(collection.Collection):
self._type = 'audits'
@staticmethod
def convert_with_links(rpc_audits, limit, url=None, expand=False,
**kwargs):
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]
collection.audits = [
Audit.convert_with_links(p, expand) for p in rpc_audits
]
collection.next = collection.get_next(limit, url=url, **kwargs)
return collection
@@ -492,26 +520,37 @@ class AuditsController(rest.RestController):
super().__init__()
self.dc_client = rpcapi.DecisionEngineAPI()
_custom_actions = {
'detail': ['GET'],
}
_custom_actions = {'detail': ['GET']}
def _get_audits_collection(self, marker, limit,
sort_key, sort_dir, expand=False,
resource_url=None, goal=None,
strategy=None):
additional_fields = ["goal_uuid", "goal_name", "strategy_uuid",
"strategy_name"]
def _get_audits_collection(
self,
marker,
limit,
sort_key,
sort_dir,
expand=False,
resource_url=None,
goal=None,
strategy=None,
):
additional_fields = [
"goal_uuid",
"goal_name",
"strategy_uuid",
"strategy_name",
]
api_utils.validate_sort_key(
sort_key, list(objects.Audit.fields) + additional_fields)
sort_key, list(objects.Audit.fields) + additional_fields
)
limit = api_utils.validate_limit(limit)
api_utils.validate_sort_dir(sort_dir)
marker_obj = None
if marker:
marker_obj = objects.Audit.get_by_uuid(pecan.request.context,
marker)
marker_obj = objects.Audit.get_by_uuid(
pecan.request.context, marker
)
filters = {}
if goal:
@@ -528,30 +567,54 @@ class AuditsController(rest.RestController):
# TODO(michaelgugino): add method to get goal by name.
filters['strategy_name'] = strategy
need_api_sort = api_utils.check_need_api_sort(sort_key,
additional_fields)
sort_db_key = (sort_key if not need_api_sort
else None)
need_api_sort = api_utils.check_need_api_sort(
sort_key, additional_fields
)
sort_db_key = sort_key if not need_api_sort else None
audits = objects.Audit.list(pecan.request.context,
limit,
marker_obj, sort_key=sort_db_key,
sort_dir=sort_dir, filters=filters)
audits = objects.Audit.list(
pecan.request.context,
limit,
marker_obj,
sort_key=sort_db_key,
sort_dir=sort_dir,
filters=filters,
)
audits_collection = AuditCollection.convert_with_links(
audits, limit, url=resource_url, expand=expand,
sort_key=sort_key, sort_dir=sort_dir)
audits,
limit,
url=resource_url,
expand=expand,
sort_key=sort_key,
sort_dir=sort_dir,
)
if need_api_sort:
api_utils.make_api_sort(audits_collection.audits, sort_key,
sort_dir)
api_utils.make_api_sort(
audits_collection.audits, sort_key, sort_dir
)
return audits_collection
@wsme_pecan.wsexpose(AuditCollection, types.uuid, int, wtypes.text,
wtypes.text, wtypes.text, wtypes.text)
def get_all(self, marker=None, limit=None, sort_key='id', sort_dir='asc',
goal=None, strategy=None):
@wsme_pecan.wsexpose(
AuditCollection,
types.uuid,
int,
wtypes.text,
wtypes.text,
wtypes.text,
wtypes.text,
)
def get_all(
self,
marker=None,
limit=None,
sort_key='id',
sort_dir='asc',
goal=None,
strategy=None,
):
"""Retrieve a list of audits.
:param marker: pagination marker for large data sets.
@@ -563,17 +626,18 @@ class AuditsController(rest.RestController):
"""
context = pecan.request.context
policy.enforce(context, 'audit:get_all',
action='audit:get_all')
policy.enforce(context, 'audit:get_all', action='audit:get_all')
return self._get_audits_collection(marker, limit, sort_key,
sort_dir, goal=goal,
strategy=strategy)
return self._get_audits_collection(
marker, limit, sort_key, sort_dir, goal=goal, strategy=strategy
)
@wsme_pecan.wsexpose(AuditCollection, wtypes.text, types.uuid, int,
wtypes.text, wtypes.text)
def detail(self, goal=None, marker=None, limit=None,
sort_key='id', sort_dir='asc'):
@wsme_pecan.wsexpose(
AuditCollection, wtypes.text, types.uuid, int, wtypes.text, wtypes.text
)
def detail(
self, goal=None, marker=None, limit=None, sort_key='id', sort_dir='asc'
):
"""Retrieve a list of audits with detail.
:param goal: goal UUID or name to filter by
@@ -583,8 +647,7 @@ class AuditsController(rest.RestController):
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
"""
context = pecan.request.context
policy.enforce(context, 'audit:detail',
action='audit:detail')
policy.enforce(context, 'audit:detail', action='audit:detail')
# NOTE(lucasagomes): /detail should only work against collections
parent = pecan.request.path.split('/')[:-1][-1]
if parent != "audits":
@@ -592,10 +655,9 @@ class AuditsController(rest.RestController):
expand = True
resource_url = '/'.join(['audits', 'detail'])
return self._get_audits_collection(marker, limit,
sort_key, sort_dir, expand,
resource_url,
goal=goal)
return self._get_audits_collection(
marker, limit, sort_key, sort_dir, expand, resource_url, goal=goal
)
@wsme_pecan.wsexpose(Audit, wtypes.text)
def get_one(self, audit):
@@ -609,39 +671,46 @@ class AuditsController(rest.RestController):
return Audit.convert_with_links(rpc_audit)
@wsme_pecan.wsexpose(Audit, body=AuditPostType,
status_code=HTTPStatus.CREATED)
@wsme_pecan.wsexpose(
Audit, body=AuditPostType, status_code=HTTPStatus.CREATED
)
def post(self, audit_p):
"""Create a new audit.
:param audit_p: an audit within the request body.
"""
context = pecan.request.context
policy.enforce(context, 'audit:create',
action='audit:create')
policy.enforce(context, 'audit:create', action='audit:create')
audit = audit_p.as_audit(context)
strategy_uuid = audit.strategy_uuid
no_schema = True
if strategy_uuid is not None:
# validate parameter when predefined strategy in audit template
strategy = objects.Strategy.get(pecan.request.context,
strategy_uuid)
strategy = objects.Strategy.get(
pecan.request.context, strategy_uuid
)
schema = strategy.parameters_spec
if schema:
# validate input parameter with default value feedback
no_schema = False
try:
utils.StrictDefaultValidatingDraft4Validator(
schema).validate(audit.parameters)
schema
).validate(audit.parameters)
except jsonschema.exceptions.ValidationError as e:
raise exception.Invalid(
_('Invalid parameters for strategy: %s') % e)
_('Invalid parameters for strategy: %s') % e
)
if no_schema and audit.parameters:
raise exception.Invalid(_('Specify parameters but no predefined '
'strategy for audit, or no '
'parameter spec in predefined strategy'))
raise exception.Invalid(
_(
'Specify parameters but no predefined '
'strategy for audit, or no '
'parameter spec in predefined strategy'
)
)
audit_dict = audit.as_dict()
# convert local time to UTC time
@@ -649,10 +718,12 @@ class AuditsController(rest.RestController):
end_time_value = audit_dict.get('end_time')
if start_time_value:
audit_dict['start_time'] = start_time_value.astimezone(
timezone.utc).replace(tzinfo=None)
timezone.utc
).replace(tzinfo=None)
if end_time_value:
audit_dict['end_time'] = end_time_value.astimezone(
timezone.utc).replace(tzinfo=None)
timezone.utc
).replace(tzinfo=None)
new_audit = objects.Audit(context, **audit_dict)
new_audit.create()
@@ -675,10 +746,10 @@ class AuditsController(rest.RestController):
:param patch: a json PATCH document to apply to this audit.
"""
context = pecan.request.context
audit_to_update = api_utils.get_resource(
'Audit', audit, eager=True)
policy.enforce(context, 'audit:update', audit_to_update,
action='audit:update')
audit_to_update = api_utils.get_resource('Audit', audit, eager=True)
policy.enforce(
context, 'audit:update', audit_to_update, action='audit:update'
)
try:
audit_dict = audit_to_update.as_dict()
@@ -686,21 +757,27 @@ class AuditsController(rest.RestController):
initial_state = audit_dict['state']
new_state = api_utils.get_patch_value(patch, 'state')
if not api_utils.check_audit_state_transition(
patch, initial_state):
error_message = _("State transition not allowed: "
"(%(initial_state)s -> %(new_state)s)")
patch, initial_state
):
error_message = _(
"State transition not allowed: "
"(%(initial_state)s -> %(new_state)s)"
)
raise exception.PatchError(
patch=patch,
reason=error_message % dict(
initial_state=initial_state, new_state=new_state))
reason=error_message
% dict(initial_state=initial_state, new_state=new_state),
)
patch_path = api_utils.get_patch_key(patch, 'path')
if patch_path in ('start_time', 'end_time'):
patch_value = api_utils.get_patch_value(patch, patch_path)
# convert string format to UTC time
new_patch_value = wutils.parse_isodatetime(
patch_value).astimezone(
timezone.utc).replace(tzinfo=None)
new_patch_value = (
wutils.parse_isodatetime(patch_value)
.astimezone(timezone.utc)
.replace(tzinfo=None)
)
api_utils.set_patch_value(patch, patch_path, new_patch_value)
audit = Audit(**api_utils.apply_jsonpatch(audit_dict, patch))
@@ -729,16 +806,16 @@ class AuditsController(rest.RestController):
:param audit: UUID or name of an audit.
"""
context = pecan.request.context
audit_to_delete = api_utils.get_resource(
'Audit', audit, eager=True)
policy.enforce(context, 'audit:delete', audit_to_delete,
action='audit:delete')
audit_to_delete = api_utils.get_resource('Audit', audit, eager=True)
policy.enforce(
context, 'audit:delete', audit_to_delete, action='audit:delete'
)
initial_state = audit_to_delete.state
new_state = objects.audit.State.DELETED
if not objects.audit.AuditStateTransitionManager(
).check_transition(initial_state, new_state):
raise exception.DeleteError(
state=initial_state)
if not objects.audit.AuditStateTransitionManager().check_transition(
initial_state, new_state
):
raise exception.DeleteError(state=initial_state)
audit_to_delete.soft_delete()
+247 -137
View File
@@ -113,17 +113,19 @@ class AuditTemplatePostType(wtypes.Base):
"items": {
"type": "object",
"properties": AuditTemplatePostType._get_schemas(),
"additionalProperties": False
}
"additionalProperties": False,
},
}
return SCHEMA
@staticmethod
def _get_schemas():
collectors = default_loading.ClusterDataModelCollectorLoader(
).list_available()
schemas = {k: c.SCHEMA for k, c
in collectors.items() if hasattr(c, "SCHEMA")}
collectors = (
default_loading.ClusterDataModelCollectorLoader().list_available()
)
schemas = {
k: c.SCHEMA for k, c in collectors.items() if hasattr(c, "SCHEMA")
}
return schemas
@staticmethod
@@ -144,7 +146,7 @@ class AuditTemplatePostType(wtypes.Base):
audit_template.scope = [dict(compute=audit_template.scope)]
common_utils.Draft4Validator(
AuditTemplatePostType._build_schema()
).validate(audit_template.scope)
).validate(audit_template.scope)
include_host_aggregates = False
exclude_host_aggregates = False
@@ -159,34 +161,47 @@ class AuditTemplatePostType(wtypes.Base):
raise exception.Invalid(
message=_(
"host_aggregates can't be "
"included and excluded together"))
"included and excluded together"
)
)
if audit_template.strategy:
try:
if (common_utils.is_uuid_like(audit_template.strategy) or
common_utils.is_int_like(audit_template.strategy)):
if common_utils.is_uuid_like(
audit_template.strategy
) or common_utils.is_int_like(audit_template.strategy):
strategy = objects.Strategy.get(
AuditTemplatePostType._ctx, audit_template.strategy)
AuditTemplatePostType._ctx, audit_template.strategy
)
else:
strategy = objects.Strategy.get_by_name(
AuditTemplatePostType._ctx, audit_template.strategy)
AuditTemplatePostType._ctx, audit_template.strategy
)
except Exception:
raise exception.InvalidStrategy(
strategy=audit_template.strategy)
strategy=audit_template.strategy
)
# Check that the strategy we indicate is actually related to the
# specified goal
if strategy.goal_id != goal.id:
available_strategies = objects.Strategy.list(
AuditTemplatePostType._ctx)
choices = [f"'{s.uuid}' ({s.name})"
for s in available_strategies]
AuditTemplatePostType._ctx
)
choices = [
f"'{s.uuid}' ({s.name})" for s in available_strategies
]
raise exception.InvalidStrategy(
message=_(
"'%(strategy)s' strategy does relate to the "
"'%(goal)s' goal. Possible choices: %(choices)s")
% dict(strategy=strategy.name, goal=goal.name,
choices=", ".join(choices)))
"'%(goal)s' goal. Possible choices: %(choices)s"
)
% dict(
strategy=strategy.name,
goal=goal.name,
choices=", ".join(choices),
)
)
audit_template.strategy = strategy.uuid
# We force the UUID so that we do not need to query the DB with the
@@ -197,7 +212,6 @@ class AuditTemplatePostType(wtypes.Base):
class AuditTemplatePatchType(types.JsonPatchType):
_ctx = context_utils.make_context()
@staticmethod
@@ -210,8 +224,8 @@ class AuditTemplatePatchType(types.JsonPatchType):
AuditTemplatePatchType._validate_goal(patch)
elif patch.path == "/goal" and patch.op == "remove":
raise wsme.exc.ClientSideError(
_("Cannot remove 'goal' attribute "
"from an audit template"))
_("Cannot remove 'goal' attribute from an audit template")
)
if patch.path == "/strategy":
AuditTemplatePatchType._validate_strategy(patch)
return types.JsonPatchType.validate(patch)
@@ -222,8 +236,7 @@ class AuditTemplatePatchType(types.JsonPatchType):
goal = patch.value
if goal:
available_goals = objects.Goal.list(
AuditTemplatePatchType._ctx)
available_goals = objects.Goal.list(AuditTemplatePatchType._ctx)
available_goal_uuids_map = {g.uuid: g for g in available_goals}
available_goal_names_map = {g.name: g for g in available_goals}
if goal in available_goal_uuids_map:
@@ -239,11 +252,14 @@ class AuditTemplatePatchType(types.JsonPatchType):
strategy = patch.value
if strategy:
available_strategies = objects.Strategy.list(
AuditTemplatePatchType._ctx)
AuditTemplatePatchType._ctx
)
available_strategy_uuids_map = {
s.uuid: s for s in available_strategies}
s.uuid: s for s in available_strategies
}
available_strategy_names_map = {
s.name: s for s in available_strategies}
s.name: s for s in available_strategies
}
if strategy in available_strategy_uuids_map:
patch.value = available_strategy_uuids_map[strategy].id
elif strategy in available_strategy_names_map:
@@ -271,13 +287,12 @@ class AuditTemplate(base.APIBase):
return None
goal = None
try:
if (common_utils.is_uuid_like(value) or
common_utils.is_int_like(value)):
goal = objects.Goal.get(
pecan.request.context, value)
if common_utils.is_uuid_like(value) or common_utils.is_int_like(
value
):
goal = objects.Goal.get(pecan.request.context, value)
else:
goal = objects.Goal.get_by_name(
pecan.request.context, value)
goal = objects.Goal.get_by_name(pecan.request.context, value)
except exception.GoalNotFound:
pass
if goal:
@@ -289,13 +304,14 @@ class AuditTemplate(base.APIBase):
return None
strategy = None
try:
if (common_utils.is_uuid_like(value) or
common_utils.is_int_like(value)):
strategy = objects.Strategy.get(
pecan.request.context, value)
if common_utils.is_uuid_like(value) or common_utils.is_int_like(
value
):
strategy = objects.Strategy.get(pecan.request.context, value)
else:
strategy = objects.Strategy.get_by_name(
pecan.request.context, value)
pecan.request.context, value
)
except exception.StrategyNotFound:
pass
if strategy:
@@ -352,19 +368,23 @@ class AuditTemplate(base.APIBase):
"""Short description of this audit template"""
goal_uuid = wtypes.wsproperty(
wtypes.text, _get_goal_uuid, _set_goal_uuid, mandatory=True)
wtypes.text, _get_goal_uuid, _set_goal_uuid, mandatory=True
)
"""Goal UUID the audit template refers to"""
goal_name = wtypes.wsproperty(
wtypes.text, _get_goal_name, _set_goal_name, mandatory=False)
wtypes.text, _get_goal_name, _set_goal_name, mandatory=False
)
"""The name of the goal this audit template refers to"""
strategy_uuid = wtypes.wsproperty(
wtypes.text, _get_strategy_uuid, _set_strategy_uuid, mandatory=False)
wtypes.text, _get_strategy_uuid, _set_strategy_uuid, mandatory=False
)
"""Strategy UUID the audit template refers to"""
strategy_name = wtypes.wsproperty(
wtypes.text, _get_strategy_name, _set_strategy_name, mandatory=False)
wtypes.text, _get_strategy_name, _set_strategy_name, mandatory=False
)
"""The name of the strategy this audit template refers to"""
audits = wtypes.wsattr([link.Link], readonly=True)
@@ -400,50 +420,64 @@ class AuditTemplate(base.APIBase):
self.fields.append('strategy_name')
setattr(self, 'goal_uuid', kwargs.get('goal_id', wtypes.Unset))
setattr(self, 'goal_name', kwargs.get('goal_id', wtypes.Unset))
setattr(self, 'strategy_uuid',
kwargs.get('strategy_id', wtypes.Unset))
setattr(self, 'strategy_name',
kwargs.get('strategy_id', wtypes.Unset))
setattr(self, 'strategy_uuid', kwargs.get('strategy_id', wtypes.Unset))
setattr(self, 'strategy_name', kwargs.get('strategy_id', wtypes.Unset))
@staticmethod
def _convert_with_links(audit_template, url, expand=True):
if not expand:
audit_template.unset_fields_except(
['uuid', 'name', 'goal_uuid', 'goal_name',
'scope', 'strategy_uuid', 'strategy_name'])
[
'uuid',
'name',
'goal_uuid',
'goal_name',
'scope',
'strategy_uuid',
'strategy_name',
]
)
# The numeric ID should not be exposed to
# the user, it's internal only.
audit_template.goal_id = wtypes.Unset
audit_template.strategy_id = wtypes.Unset
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)]
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())
hide_fields_in_newer_versions(audit_template)
return cls._convert_with_links(audit_template, pecan.request.host_url,
expand)
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',
goal_uuid='83e44733-b640-40e2-8d8a-7dd3be7134e6',
strategy_uuid='367d826e-b6a4-4b70-bc44-c3f6fe1c9986',
created_at=timeutils.utcnow(),
deleted_at=None,
updated_at=timeutils.utcnow(),
scope=[],)
sample = cls(
uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c',
name='My Audit Template',
description='Description of my audit template',
goal_uuid='83e44733-b640-40e2-8d8a-7dd3be7134e6',
strategy_uuid='367d826e-b6a4-4b70-bc44-c3f6fe1c9986',
created_at=timeutils.utcnow(),
deleted_at=None,
updated_at=timeutils.utcnow(),
scope=[],
)
return cls._convert_with_links(sample, 'http://localhost:9322', expand)
@@ -458,12 +492,14 @@ class AuditTemplateCollection(collection.Collection):
self._type = 'audit_templates'
@staticmethod
def convert_with_links(rpc_audit_templates, limit, url=None, expand=False,
**kwargs):
def convert_with_links(
rpc_audit_templates, limit, url=None, expand=False, **kwargs
):
at_collection = AuditTemplateCollection()
at_collection.audit_templates = [
AuditTemplate.convert_with_links(p, expand)
for p in rpc_audit_templates]
for p in rpc_audit_templates
]
at_collection.next = at_collection.get_next(limit, url=url, **kwargs)
return at_collection
@@ -480,54 +516,90 @@ class AuditTemplatesController(rest.RestController):
def __init__(self):
super().__init__()
_custom_actions = {
'detail': ['GET'],
}
_custom_actions = {'detail': ['GET']}
def _get_audit_templates_collection(self, filters, marker, limit,
sort_key, sort_dir, expand=False,
resource_url=None):
additional_fields = ["goal_uuid", "goal_name", "strategy_uuid",
"strategy_name"]
def _get_audit_templates_collection(
self,
filters,
marker,
limit,
sort_key,
sort_dir,
expand=False,
resource_url=None,
):
additional_fields = [
"goal_uuid",
"goal_name",
"strategy_uuid",
"strategy_name",
]
api_utils.validate_sort_key(
sort_key, list(objects.AuditTemplate.fields) + additional_fields)
sort_key, list(objects.AuditTemplate.fields) + additional_fields
)
api_utils.validate_search_filters(
filters, list(objects.AuditTemplate.fields) + additional_fields)
filters, list(objects.AuditTemplate.fields) + additional_fields
)
limit = api_utils.validate_limit(limit)
api_utils.validate_sort_dir(sort_dir)
marker_obj = None
if marker:
marker_obj = objects.AuditTemplate.get_by_uuid(
pecan.request.context,
marker)
pecan.request.context, marker
)
need_api_sort = api_utils.check_need_api_sort(sort_key,
additional_fields)
sort_db_key = (sort_key if not need_api_sort
else None)
need_api_sort = api_utils.check_need_api_sort(
sort_key, additional_fields
)
sort_db_key = sort_key if not need_api_sort else None
audit_templates = objects.AuditTemplate.list(
pecan.request.context, filters, limit, marker_obj,
sort_key=sort_db_key, sort_dir=sort_dir)
pecan.request.context,
filters,
limit,
marker_obj,
sort_key=sort_db_key,
sort_dir=sort_dir,
)
audit_templates_collection = \
audit_templates_collection = (
AuditTemplateCollection.convert_with_links(
audit_templates, limit, url=resource_url, expand=expand,
sort_key=sort_key, sort_dir=sort_dir)
audit_templates,
limit,
url=resource_url,
expand=expand,
sort_key=sort_key,
sort_dir=sort_dir,
)
)
if need_api_sort:
api_utils.make_api_sort(
audit_templates_collection.audit_templates, sort_key,
sort_dir)
audit_templates_collection.audit_templates, sort_key, sort_dir
)
return audit_templates_collection
@wsme_pecan.wsexpose(AuditTemplateCollection, wtypes.text, wtypes.text,
types.uuid, int, wtypes.text, wtypes.text)
def get_all(self, goal=None, strategy=None, marker=None,
limit=None, sort_key='id', sort_dir='asc'):
@wsme_pecan.wsexpose(
AuditTemplateCollection,
wtypes.text,
wtypes.text,
types.uuid,
int,
wtypes.text,
wtypes.text,
)
def get_all(
self,
goal=None,
strategy=None,
marker=None,
limit=None,
sort_key='id',
sort_dir='asc',
):
"""Retrieve a list of audit templates.
:param goal: goal UUID or name to filter by
@@ -538,8 +610,9 @@ class AuditTemplatesController(rest.RestController):
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
"""
context = pecan.request.context
policy.enforce(context, 'audit_template:get_all',
action='audit_template:get_all')
policy.enforce(
context, 'audit_template:get_all', action='audit_template:get_all'
)
filters = {}
if goal:
if common_utils.is_uuid_like(goal):
@@ -554,12 +627,27 @@ class AuditTemplatesController(rest.RestController):
filters['strategy_name'] = strategy
return self._get_audit_templates_collection(
filters, marker, limit, sort_key, sort_dir)
filters, marker, limit, sort_key, sort_dir
)
@wsme_pecan.wsexpose(AuditTemplateCollection, wtypes.text, wtypes.text,
types.uuid, int, wtypes.text, wtypes.text)
def detail(self, goal=None, strategy=None, marker=None,
limit=None, sort_key='id', sort_dir='asc'):
@wsme_pecan.wsexpose(
AuditTemplateCollection,
wtypes.text,
wtypes.text,
types.uuid,
int,
wtypes.text,
wtypes.text,
)
def detail(
self,
goal=None,
strategy=None,
marker=None,
limit=None,
sort_key='id',
sort_dir='asc',
):
"""Retrieve a list of audit templates with detail.
:param goal: goal UUID or name to filter by
@@ -570,8 +658,9 @@ class AuditTemplatesController(rest.RestController):
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
"""
context = pecan.request.context
policy.enforce(context, 'audit_template:detail',
action='audit_template:detail')
policy.enforce(
context, 'audit_template:detail', action='audit_template:detail'
)
# NOTE(lucasagomes): /detail should only work against collections
parent = pecan.request.path.split('/')[:-1][-1]
@@ -593,9 +682,9 @@ class AuditTemplatesController(rest.RestController):
expand = True
resource_url = '/'.join(['audit_templates', 'detail'])
return self._get_audit_templates_collection(filters, marker, limit,
sort_key, sort_dir, expand,
resource_url)
return self._get_audit_templates_collection(
filters, marker, limit, sort_key, sort_dir, expand, resource_url
)
@wsme_pecan.wsexpose(AuditTemplate, wtypes.text)
def get_one(self, audit_template):
@@ -604,16 +693,24 @@ class AuditTemplatesController(rest.RestController):
:param audit_template: UUID or name of an audit template.
"""
context = pecan.request.context
rpc_audit_template = api_utils.get_resource('AuditTemplate',
audit_template)
policy.enforce(context, 'audit_template:get', rpc_audit_template,
action='audit_template:get')
rpc_audit_template = api_utils.get_resource(
'AuditTemplate', audit_template
)
policy.enforce(
context,
'audit_template:get',
rpc_audit_template,
action='audit_template:get',
)
return AuditTemplate.convert_with_links(rpc_audit_template)
@wsme.validate(types.uuid, AuditTemplatePostType)
@wsme_pecan.wsexpose(AuditTemplate, body=AuditTemplatePostType,
status_code=HTTPStatus.CREATED)
@wsme_pecan.wsexpose(
AuditTemplate,
body=AuditTemplatePostType,
status_code=HTTPStatus.CREATED,
)
def post(self, audit_template_postdata):
"""Create a new audit template.
@@ -621,24 +718,28 @@ class AuditTemplatesController(rest.RestController):
from the request body.
"""
context = pecan.request.context
policy.enforce(context, 'audit_template:create',
action='audit_template:create')
policy.enforce(
context, 'audit_template:create', action='audit_template:create'
)
context = pecan.request.context
audit_template = audit_template_postdata.as_audit_template()
audit_template_dict = audit_template.as_dict()
new_audit_template = objects.AuditTemplate(context,
**audit_template_dict)
new_audit_template = objects.AuditTemplate(
context, **audit_template_dict
)
new_audit_template.create()
# Set the HTTP Location Header
pecan.response.location = link.build_url(
'audit_templates', new_audit_template.uuid)
'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])
@wsme_pecan.wsexpose(
AuditTemplate, wtypes.text, body=[AuditTemplatePatchType]
)
def patch(self, audit_template, patch):
"""Update an existing audit template.
@@ -646,25 +747,30 @@ class AuditTemplatesController(rest.RestController):
:param patch: a json PATCH document to apply to this audit template.
"""
context = pecan.request.context
audit_template_to_update = api_utils.get_resource('AuditTemplate',
audit_template)
policy.enforce(context, 'audit_template:update',
audit_template_to_update,
action='audit_template:update')
audit_template_to_update = api_utils.get_resource(
'AuditTemplate', audit_template
)
policy.enforce(
context,
'audit_template:update',
audit_template_to_update,
action='audit_template:update',
)
if common_utils.is_uuid_like(audit_template):
audit_template_to_update = objects.AuditTemplate.get_by_uuid(
pecan.request.context,
audit_template)
pecan.request.context, audit_template
)
else:
audit_template_to_update = objects.AuditTemplate.get_by_name(
pecan.request.context,
audit_template)
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))
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)
@@ -690,10 +796,14 @@ class AuditTemplatesController(rest.RestController):
:param template_uuid: UUID or name of an audit template.
"""
context = pecan.request.context
audit_template_to_delete = api_utils.get_resource('AuditTemplate',
audit_template)
policy.enforce(context, 'audit_template:delete',
audit_template_to_delete,
action='audit_template:delete')
audit_template_to_delete = api_utils.get_resource(
'AuditTemplate', audit_template
)
policy.enforce(
context,
'audit_template:delete',
audit_template_to_delete,
action='audit_template:delete',
)
audit_template_to_delete.soft_delete()
+3 -3
View File
@@ -23,7 +23,6 @@ from watcher.api.controllers import link
class Collection(base.APIBase):
next = wtypes.text
"""A link to retrieve the next subset of the collection"""
@@ -45,5 +44,6 @@ class Collection(base.APIBase):
marker = getattr(self.collection[-1], marker_field)
next_args = f'?{q_args}limit={limit:d}&marker={marker}'
return link.Link.make_link('next', pecan.request.host_url,
resource_url, next_args).href
return link.Link.make_link(
'next', pecan.request.host_url, resource_url, next_args
).href
+8 -9
View File
@@ -65,19 +65,18 @@ class DataModelController(rest.RestController):
"""
if not utils.allow_list_datamodel():
raise exception.NotAcceptable
allowed_data_model_type = [
'compute',
]
allowed_data_model_type = ['compute']
if data_model_type not in allowed_data_model_type:
raise exception.DataModelTypeNotFound(
data_model_type=data_model_type)
data_model_type=data_model_type
)
context = pecan.request.context
de_client = rpcapi.DecisionEngineAPI()
policy.enforce(context, 'data_model:get_all',
action='data_model:get_all')
policy.enforce(
context, 'data_model:get_all', action='data_model:get_all'
)
rpc_all_data_model = de_client.get_data_model_info(
context,
data_model_type,
audit_uuid)
context, data_model_type, audit_uuid
)
hide_fields_in_newer_versions(rpc_all_data_model)
return rpc_all_data_model
+63 -43
View File
@@ -93,14 +93,16 @@ class Goal(base.APIBase):
@staticmethod
def _convert_with_links(goal, url, expand=True):
if not expand:
goal.unset_fields_except(['uuid', 'name', 'display_name',
'efficacy_specification'])
goal.unset_fields_except(
['uuid', 'name', 'display_name', 'efficacy_specification']
)
goal.links = [link.Link.make_link('self', url,
'goals', goal.uuid),
link.Link.make_link('bookmark', url,
'goals', goal.uuid,
bookmark=True)]
goal.links = [
link.Link.make_link('self', url, 'goals', goal.uuid),
link.Link.make_link(
'bookmark', url, 'goals', goal.uuid, bookmark=True
),
]
return goal
@classmethod
@@ -116,11 +118,15 @@ class Goal(base.APIBase):
name='DUMMY',
display_name='Dummy strategy',
efficacy_specification=[
{'description': 'Dummy indicator', 'name': 'dummy',
'schema': 'Range(min=0, max=100, min_included=True, '
'max_included=True, msg=None)',
'unit': '%'}
])
{
'description': 'Dummy indicator',
'name': 'dummy',
'schema': 'Range(min=0, max=100, min_included=True, '
'max_included=True, msg=None)',
'unit': '%',
}
],
)
return cls._convert_with_links(sample, 'http://localhost:9322', expand)
@@ -135,13 +141,14 @@ class GoalCollection(collection.Collection):
self._type = 'goals'
@staticmethod
def convert_with_links(goals, limit, url=None, expand=False,
**kwargs):
def convert_with_links(goals, limit, url=None, expand=False, **kwargs):
goal_collection = GoalCollection()
goal_collection.goals = [
Goal.convert_with_links(g, expand) for g in goals]
Goal.convert_with_links(g, expand) for g in goals
]
goal_collection.next = goal_collection.get_next(
limit, url=url, **kwargs)
limit, url=url, **kwargs
)
return goal_collection
@classmethod
@@ -157,36 +164,49 @@ class GoalsController(rest.RestController):
def __init__(self):
super().__init__()
_custom_actions = {
'detail': ['GET'],
}
_custom_actions = {'detail': ['GET']}
def _get_goals_collection(self, marker, limit, sort_key, sort_dir,
expand=False, resource_url=None):
api_utils.validate_sort_key(
sort_key, list(objects.Goal.fields))
def _get_goals_collection(
self,
marker,
limit,
sort_key,
sort_dir,
expand=False,
resource_url=None,
):
api_utils.validate_sort_key(sort_key, list(objects.Goal.fields))
limit = api_utils.validate_limit(limit)
api_utils.validate_sort_dir(sort_dir)
marker_obj = None
if marker:
marker_obj = objects.Goal.get_by_uuid(
pecan.request.context, marker)
pecan.request.context, marker
)
sort_db_key = (sort_key if sort_key in objects.Goal.fields
else None)
sort_db_key = sort_key if sort_key in objects.Goal.fields else None
goals = objects.Goal.list(pecan.request.context, limit, marker_obj,
sort_key=sort_db_key, sort_dir=sort_dir)
goals = objects.Goal.list(
pecan.request.context,
limit,
marker_obj,
sort_key=sort_db_key,
sort_dir=sort_dir,
)
return GoalCollection.convert_with_links(goals, limit,
url=resource_url,
expand=expand,
sort_key=sort_key,
sort_dir=sort_dir)
return GoalCollection.convert_with_links(
goals,
limit,
url=resource_url,
expand=expand,
sort_key=sort_key,
sort_dir=sort_dir,
)
@wsme_pecan.wsexpose(GoalCollection, wtypes.text,
int, wtypes.text, wtypes.text)
@wsme_pecan.wsexpose(
GoalCollection, wtypes.text, int, wtypes.text, wtypes.text
)
def get_all(self, marker=None, limit=None, sort_key='id', sort_dir='asc'):
"""Retrieve a list of goals.
@@ -196,12 +216,12 @@ class GoalsController(rest.RestController):
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
"""
context = pecan.request.context
policy.enforce(context, 'goal:get_all',
action='goal:get_all')
policy.enforce(context, 'goal:get_all', action='goal:get_all')
return self._get_goals_collection(marker, limit, sort_key, sort_dir)
@wsme_pecan.wsexpose(GoalCollection, wtypes.text, int,
wtypes.text, wtypes.text)
@wsme_pecan.wsexpose(
GoalCollection, wtypes.text, int, wtypes.text, wtypes.text
)
def detail(self, marker=None, limit=None, sort_key='id', sort_dir='asc'):
"""Retrieve a list of goals with detail.
@@ -211,16 +231,16 @@ class GoalsController(rest.RestController):
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
"""
context = pecan.request.context
policy.enforce(context, 'goal:detail',
action='goal:detail')
policy.enforce(context, 'goal:detail', action='goal:detail')
# NOTE(lucasagomes): /detail should only work against collections
parent = pecan.request.path.split('/')[:-1][-1]
if parent != "goals":
raise exception.HTTPNotFound
expand = True
resource_url = '/'.join(['goals', 'detail'])
return self._get_goals_collection(marker, limit, sort_key, sort_dir,
expand, resource_url)
return self._get_goals_collection(
marker, limit, sort_key, sort_dir, expand, resource_url
)
@wsme_pecan.wsexpose(Goal, wtypes.text)
def get_one(self, goal):
+64 -43
View File
@@ -91,14 +91,14 @@ class ScoringEngine(base.APIBase):
@staticmethod
def _convert_with_links(se, url, expand=True):
if not expand:
se.unset_fields_except(
['uuid', 'name', 'description'])
se.unset_fields_except(['uuid', 'name', 'description'])
se.links = [link.Link.make_link('self', url,
'scoring_engines', se.uuid),
link.Link.make_link('bookmark', url,
'scoring_engines', se.uuid,
bookmark=True)]
se.links = [
link.Link.make_link('self', url, 'scoring_engines', se.uuid),
link.Link.make_link(
'bookmark', url, 'scoring_engines', se.uuid, bookmark=True
),
]
return se
@classmethod
@@ -106,13 +106,16 @@ class ScoringEngine(base.APIBase):
scoring_engine = ScoringEngine(**scoring_engine.as_dict())
hide_fields_in_newer_versions(scoring_engine)
return cls._convert_with_links(
scoring_engine, pecan.request.host_url, expand)
scoring_engine, pecan.request.host_url, expand
)
@classmethod
def sample(cls, expand=True):
sample = cls(uuid='81bbd3c7-3b08-4d12-a268-99354dbf7b71',
name='sample-se-123',
description='Sample Scoring Engine 123 just for testing')
sample = cls(
uuid='81bbd3c7-3b08-4d12-a268-99354dbf7b71',
name='sample-se-123',
description='Sample Scoring Engine 123 just for testing',
)
return cls._convert_with_links(sample, 'http://localhost:9322', expand)
@@ -127,12 +130,14 @@ class ScoringEngineCollection(collection.Collection):
self._type = 'scoring_engines'
@staticmethod
def convert_with_links(scoring_engines, limit, url=None, expand=False,
**kwargs):
def convert_with_links(
scoring_engines, limit, url=None, expand=False, **kwargs
):
collection = ScoringEngineCollection()
collection.scoring_engines = [ScoringEngine.convert_with_links(
se, expand) for se in scoring_engines]
collection.scoring_engines = [
ScoringEngine.convert_with_links(se, expand)
for se in scoring_engines
]
collection.next = collection.get_next(limit, url=url, **kwargs)
return collection
@@ -149,27 +154,34 @@ class ScoringEngineController(rest.RestController):
def __init__(self):
super().__init__()
_custom_actions = {
'detail': ['GET'],
}
_custom_actions = {'detail': ['GET']}
def _get_scoring_engines_collection(self, marker, limit,
sort_key, sort_dir, expand=False,
resource_url=None):
def _get_scoring_engines_collection(
self,
marker,
limit,
sort_key,
sort_dir,
expand=False,
resource_url=None,
):
api_utils.validate_sort_key(
sort_key, list(objects.ScoringEngine.fields))
sort_key, list(objects.ScoringEngine.fields)
)
limit = api_utils.validate_limit(limit)
api_utils.validate_sort_dir(sort_dir)
marker_obj = None
if marker:
marker_obj = objects.ScoringEngine.get_by_uuid(
pecan.request.context, marker)
pecan.request.context, marker
)
filters = {}
sort_db_key = (sort_key if sort_key in objects.ScoringEngine.fields
else None)
sort_db_key = (
sort_key if sort_key in objects.ScoringEngine.fields else None
)
scoring_engines = objects.ScoringEngine.list(
context=pecan.request.context,
@@ -177,7 +189,8 @@ class ScoringEngineController(rest.RestController):
marker=marker_obj,
sort_key=sort_db_key,
sort_dir=sort_dir,
filters=filters)
filters=filters,
)
return ScoringEngineCollection.convert_with_links(
scoring_engines,
@@ -185,12 +198,13 @@ class ScoringEngineController(rest.RestController):
url=resource_url,
expand=expand,
sort_key=sort_key,
sort_dir=sort_dir)
sort_dir=sort_dir,
)
@wsme_pecan.wsexpose(ScoringEngineCollection, wtypes.text,
int, wtypes.text, wtypes.text)
def get_all(self, marker=None, limit=None, sort_key='id',
sort_dir='asc'):
@wsme_pecan.wsexpose(
ScoringEngineCollection, wtypes.text, int, wtypes.text, wtypes.text
)
def get_all(self, marker=None, limit=None, sort_key='id', sort_dir='asc'):
"""Retrieve a list of Scoring Engines.
:param marker: pagination marker for large data sets.
@@ -199,14 +213,17 @@ class ScoringEngineController(rest.RestController):
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
"""
context = pecan.request.context
policy.enforce(context, 'scoring_engine:get_all',
action='scoring_engine:get_all')
policy.enforce(
context, 'scoring_engine:get_all', action='scoring_engine:get_all'
)
return self._get_scoring_engines_collection(
marker, limit, sort_key, sort_dir)
marker, limit, sort_key, sort_dir
)
@wsme_pecan.wsexpose(ScoringEngineCollection, wtypes.text,
int, wtypes.text, wtypes.text)
@wsme_pecan.wsexpose(
ScoringEngineCollection, wtypes.text, int, wtypes.text, wtypes.text
)
def detail(self, marker=None, limit=None, sort_key='id', sort_dir='asc'):
"""Retrieve a list of Scoring Engines with detail.
@@ -216,8 +233,9 @@ class ScoringEngineController(rest.RestController):
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
"""
context = pecan.request.context
policy.enforce(context, 'scoring_engine:detail',
action='scoring_engine:detail')
policy.enforce(
context, 'scoring_engine:detail', action='scoring_engine:detail'
)
parent = pecan.request.path.split('/')[:-1][-1]
if parent != "scoring_engines":
@@ -225,7 +243,8 @@ class ScoringEngineController(rest.RestController):
expand = True
resource_url = '/'.join(['scoring_engines', 'detail'])
return self._get_scoring_engines_collection(
marker, limit, sort_key, sort_dir, expand, resource_url)
marker, limit, sort_key, sort_dir, expand, resource_url
)
@wsme_pecan.wsexpose(ScoringEngine, wtypes.text)
def get_one(self, scoring_engine):
@@ -234,9 +253,11 @@ class ScoringEngineController(rest.RestController):
:param scoring_engine_name: The name of the Scoring Engine.
"""
context = pecan.request.context
policy.enforce(context, 'scoring_engine:get',
action='scoring_engine:get')
policy.enforce(
context, 'scoring_engine:get', action='scoring_engine:get'
)
rpc_scoring_engine = api_utils.get_resource(
'ScoringEngine', scoring_engine)
'ScoringEngine', scoring_engine
)
return ScoringEngine.convert_with_links(rpc_scoring_engine)
+72 -48
View File
@@ -67,8 +67,9 @@ class Service(base.APIBase):
def _set_status(self, id):
service = objects.Service.get(pecan.request.context, id)
last_heartbeat = (service.last_seen_up or service.updated_at or
service.created_at)
last_heartbeat = (
service.last_seen_up or service.updated_at or service.created_at
)
if isinstance(last_heartbeat, str):
# NOTE(russellb) If this service came in over rpc via
# conductor, then the timestamp will be a string and needs to be
@@ -81,12 +82,17 @@ class Service(base.APIBase):
elapsed = timeutils.delta_seconds(last_heartbeat, timeutils.utcnow())
is_up = abs(elapsed) <= CONF.service_down_time
if not is_up:
LOG.warning('Seems service %(name)s on host %(host)s is down. '
'Last heartbeat was %(lhb)s.'
'Elapsed time is %(el)s',
{'name': service.name,
'host': service.host,
'lhb': str(last_heartbeat), 'el': str(elapsed)})
LOG.warning(
'Seems service %(name)s on host %(host)s is down. '
'Last heartbeat was %(lhb)s.'
'Elapsed time is %(el)s',
{
'name': service.name,
'host': service.host,
'lhb': str(last_heartbeat),
'el': str(elapsed),
},
)
self._status = objects.service.ServiceStatus.FAILED
else:
self._status = objects.service.ServiceStatus.ACTIVE
@@ -103,8 +109,9 @@ class Service(base.APIBase):
last_seen_up = wtypes.wsattr(datetime.datetime, readonly=True)
"""Time when Watcher service sent latest heartbeat."""
status = wtypes.wsproperty(wtypes.text, _get_status, _set_status,
mandatory=True)
status = wtypes.wsproperty(
wtypes.text, _get_status, _set_status, mandatory=True
)
links = wtypes.wsattr([link.Link], readonly=True)
"""A list containing a self link."""
@@ -116,34 +123,39 @@ class Service(base.APIBase):
self.fields = []
for field in fields:
self.fields.append(field)
setattr(self, field, kwargs.get(
field if field != 'status' else 'id', wtypes.Unset))
setattr(
self,
field,
kwargs.get(field if field != 'status' else 'id', wtypes.Unset),
)
@staticmethod
def _convert_with_links(service, url, expand=True):
if not expand:
service.unset_fields_except(
['id', 'name', 'host', 'status'])
service.unset_fields_except(['id', 'name', 'host', 'status'])
service.links = [
link.Link.make_link('self', url, 'services', str(service.id)),
link.Link.make_link('bookmark', url, 'services', str(service.id),
bookmark=True)]
link.Link.make_link(
'bookmark', url, 'services', str(service.id), bookmark=True
),
]
return service
@classmethod
def convert_with_links(cls, service, expand=True):
service = Service(**service.as_dict())
hide_fields_in_newer_versions(service)
return cls._convert_with_links(
service, pecan.request.host_url, expand)
return cls._convert_with_links(service, pecan.request.host_url, expand)
@classmethod
def sample(cls, expand=True):
sample = cls(id=1,
name='watcher-applier',
host='Controller',
last_seen_up=datetime.datetime(2016, 1, 1))
sample = cls(
id=1,
name='watcher-applier',
host='Controller',
last_seen_up=datetime.datetime(2016, 1, 1),
)
return cls._convert_with_links(sample, 'http://localhost:9322', expand)
@@ -158,13 +170,14 @@ class ServiceCollection(collection.Collection):
self._type = 'services'
@staticmethod
def convert_with_links(services, limit, url=None, expand=False,
**kwargs):
def convert_with_links(services, limit, url=None, expand=False, **kwargs):
service_collection = ServiceCollection()
service_collection.services = [
Service.convert_with_links(g, expand) for g in services]
Service.convert_with_links(g, expand) for g in services
]
service_collection.next = service_collection.get_next(
limit, url=url, marker_field='id', **kwargs)
limit, url=url, marker_field='id', **kwargs
)
return service_collection
@classmethod
@@ -180,32 +193,43 @@ class ServicesController(rest.RestController):
def __init__(self):
super().__init__()
_custom_actions = {
'detail': ['GET'],
}
_custom_actions = {'detail': ['GET']}
def _get_services_collection(self, marker, limit, sort_key, sort_dir,
expand=False, resource_url=None):
api_utils.validate_sort_key(
sort_key, list(objects.Service.fields))
def _get_services_collection(
self,
marker,
limit,
sort_key,
sort_dir,
expand=False,
resource_url=None,
):
api_utils.validate_sort_key(sort_key, list(objects.Service.fields))
limit = api_utils.validate_limit(limit)
api_utils.validate_sort_dir(sort_dir)
marker_obj = None
if marker:
marker_obj = objects.Service.get(
pecan.request.context, marker)
marker_obj = objects.Service.get(pecan.request.context, marker)
sort_db_key = (sort_key if sort_key in objects.Service.fields
else None)
sort_db_key = sort_key if sort_key in objects.Service.fields else None
services = objects.Service.list(
pecan.request.context, limit, marker_obj,
sort_key=sort_db_key, sort_dir=sort_dir)
pecan.request.context,
limit,
marker_obj,
sort_key=sort_db_key,
sort_dir=sort_dir,
)
return ServiceCollection.convert_with_links(
services, limit, url=resource_url, expand=expand,
sort_key=sort_key, sort_dir=sort_dir)
services,
limit,
url=resource_url,
expand=expand,
sort_key=sort_key,
sort_dir=sort_dir,
)
@wsme_pecan.wsexpose(ServiceCollection, int, int, wtypes.text, wtypes.text)
def get_all(self, marker=None, limit=None, sort_key='id', sort_dir='asc'):
@@ -217,8 +241,7 @@ class ServicesController(rest.RestController):
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
"""
context = pecan.request.context
policy.enforce(context, 'service:get_all',
action='service:get_all')
policy.enforce(context, 'service:get_all', action='service:get_all')
return self._get_services_collection(marker, limit, sort_key, sort_dir)
@@ -232,8 +255,7 @@ class ServicesController(rest.RestController):
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
"""
context = pecan.request.context
policy.enforce(context, 'service:detail',
action='service:detail')
policy.enforce(context, 'service:detail', action='service:detail')
# NOTE(lucasagomes): /detail should only work against collections
parent = pecan.request.path.split('/')[:-1][-1]
if parent != "services":
@@ -242,7 +264,8 @@ class ServicesController(rest.RestController):
resource_url = '/'.join(['services', 'detail'])
return self._get_services_collection(
marker, limit, sort_key, sort_dir, expand, resource_url)
marker, limit, sort_key, sort_dir, expand, resource_url
)
@wsme_pecan.wsexpose(Service, wtypes.text)
def get_one(self, service):
@@ -252,7 +275,8 @@ class ServicesController(rest.RestController):
"""
context = pecan.request.context
rpc_service = api_utils.get_resource('Service', service)
policy.enforce(context, 'service:get', rpc_service,
action='service:get')
policy.enforce(
context, 'service:get', rpc_service, action='service:get'
)
return Service.convert_with_links(rpc_service)
+117 -59
View File
@@ -60,6 +60,7 @@ class Strategy(base.APIBase):
This class enforces type checking and value constraints, and converts
between the internal object model and the API representation of a strategy.
"""
_goal_uuid = None
_goal_name = None
@@ -68,8 +69,9 @@ class Strategy(base.APIBase):
return None
goal = None
try:
if (common_utils.is_uuid_like(value) or
common_utils.is_int_like(value)):
if common_utils.is_uuid_like(value) or common_utils.is_int_like(
value
):
goal = objects.Goal.get(pecan.request.context, value)
else:
goal = objects.Goal.get_by_name(pecan.request.context, value)
@@ -111,12 +113,14 @@ class Strategy(base.APIBase):
links = wtypes.wsattr([link.Link], readonly=True)
"""A list containing a self link and associated goal links"""
goal_uuid = wtypes.wsproperty(wtypes.text, _get_goal_uuid, _set_goal_uuid,
mandatory=True)
goal_uuid = wtypes.wsproperty(
wtypes.text, _get_goal_uuid, _set_goal_uuid, mandatory=True
)
"""The UUID of the goal this audit refers to"""
goal_name = wtypes.wsproperty(wtypes.text, _get_goal_name, _set_goal_name,
mandatory=False)
goal_name = wtypes.wsproperty(
wtypes.text, _get_goal_name, _set_goal_name, mandatory=False
)
"""The name of the goal this audit refers to"""
parameters_spec = {wtypes.text: types.jsontype}
@@ -137,19 +141,25 @@ class Strategy(base.APIBase):
setattr(self, 'display_name', kwargs.get('display_name', wtypes.Unset))
setattr(self, 'goal_uuid', kwargs.get('goal_id', wtypes.Unset))
setattr(self, 'goal_name', kwargs.get('goal_id', wtypes.Unset))
setattr(self, 'parameters_spec', kwargs.get('parameters_spec',
wtypes.Unset))
setattr(
self,
'parameters_spec',
kwargs.get('parameters_spec', wtypes.Unset),
)
@staticmethod
def _convert_with_links(strategy, url, expand=True):
if not expand:
strategy.unset_fields_except(
['uuid', 'name', 'display_name', 'goal_uuid', 'goal_name'])
['uuid', 'name', 'display_name', 'goal_uuid', 'goal_name']
)
strategy.links = [
link.Link.make_link('self', url, 'strategies', strategy.uuid),
link.Link.make_link('bookmark', url, 'strategies', strategy.uuid,
bookmark=True)]
link.Link.make_link(
'bookmark', url, 'strategies', strategy.uuid, bookmark=True
),
]
return strategy
@classmethod
@@ -157,13 +167,16 @@ class Strategy(base.APIBase):
strategy = Strategy(**strategy.as_dict())
hide_fields_in_newer_versions(strategy)
return cls._convert_with_links(
strategy, pecan.request.host_url, expand)
strategy, pecan.request.host_url, expand
)
@classmethod
def sample(cls, expand=True):
sample = cls(uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c',
name='DUMMY',
display_name='Dummy strategy')
sample = cls(
uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c',
name='DUMMY',
display_name='Dummy strategy',
)
return cls._convert_with_links(sample, 'http://localhost:9322', expand)
@@ -178,13 +191,16 @@ class StrategyCollection(collection.Collection):
self._type = 'strategies'
@staticmethod
def convert_with_links(strategies, limit, url=None, expand=False,
**kwargs):
def convert_with_links(
strategies, limit, url=None, expand=False, **kwargs
):
strategy_collection = StrategyCollection()
strategy_collection.strategies = [
Strategy.convert_with_links(g, expand) for g in strategies]
Strategy.convert_with_links(g, expand) for g in strategies
]
strategy_collection.next = strategy_collection.get_next(
limit, url=url, **kwargs)
limit, url=url, **kwargs
)
return strategy_collection
@classmethod
@@ -200,50 +216,76 @@ class StrategiesController(rest.RestController):
def __init__(self):
super().__init__()
_custom_actions = {
'detail': ['GET'],
'state': ['GET'],
}
_custom_actions = {'detail': ['GET'], 'state': ['GET']}
def _get_strategies_collection(self, filters, marker, limit, sort_key,
sort_dir, expand=False, resource_url=None):
def _get_strategies_collection(
self,
filters,
marker,
limit,
sort_key,
sort_dir,
expand=False,
resource_url=None,
):
additional_fields = ["goal_uuid", "goal_name"]
api_utils.validate_sort_key(
sort_key, list(objects.Strategy.fields) + additional_fields)
sort_key, list(objects.Strategy.fields) + additional_fields
)
api_utils.validate_search_filters(
filters, list(objects.Strategy.fields) + additional_fields)
filters, list(objects.Strategy.fields) + additional_fields
)
limit = api_utils.validate_limit(limit)
api_utils.validate_sort_dir(sort_dir)
marker_obj = None
if marker:
marker_obj = objects.Strategy.get_by_uuid(
pecan.request.context, marker)
pecan.request.context, marker
)
need_api_sort = api_utils.check_need_api_sort(sort_key,
additional_fields)
sort_db_key = (sort_key if not need_api_sort
else None)
need_api_sort = api_utils.check_need_api_sort(
sort_key, additional_fields
)
sort_db_key = sort_key if not need_api_sort else None
strategies = objects.Strategy.list(
pecan.request.context, limit, marker_obj, filters=filters,
sort_key=sort_db_key, sort_dir=sort_dir)
pecan.request.context,
limit,
marker_obj,
filters=filters,
sort_key=sort_db_key,
sort_dir=sort_dir,
)
strategies_collection = StrategyCollection.convert_with_links(
strategies, limit, url=resource_url, expand=expand,
sort_key=sort_key, sort_dir=sort_dir)
strategies,
limit,
url=resource_url,
expand=expand,
sort_key=sort_key,
sort_dir=sort_dir,
)
if need_api_sort:
api_utils.make_api_sort(strategies_collection.strategies,
sort_key, sort_dir)
api_utils.make_api_sort(
strategies_collection.strategies, sort_key, sort_dir
)
return strategies_collection
@wsme_pecan.wsexpose(StrategyCollection, wtypes.text, wtypes.text,
int, wtypes.text, wtypes.text)
def get_all(self, goal=None, marker=None, limit=None,
sort_key='id', sort_dir='asc'):
@wsme_pecan.wsexpose(
StrategyCollection,
wtypes.text,
wtypes.text,
int,
wtypes.text,
wtypes.text,
)
def get_all(
self, goal=None, marker=None, limit=None, sort_key='id', sort_dir='asc'
):
"""Retrieve a list of strategies.
:param goal: goal UUID or name to filter by.
@@ -253,8 +295,7 @@ class StrategiesController(rest.RestController):
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
"""
context = pecan.request.context
policy.enforce(context, 'strategy:get_all',
action='strategy:get_all')
policy.enforce(context, 'strategy:get_all', action='strategy:get_all')
filters = {}
if goal:
if common_utils.is_uuid_like(goal):
@@ -263,12 +304,20 @@ class StrategiesController(rest.RestController):
filters['goal_name'] = goal
return self._get_strategies_collection(
filters, marker, limit, sort_key, sort_dir)
filters, marker, limit, sort_key, sort_dir
)
@wsme_pecan.wsexpose(StrategyCollection, wtypes.text, wtypes.text, int,
wtypes.text, wtypes.text)
def detail(self, goal=None, marker=None, limit=None,
sort_key='id', sort_dir='asc'):
@wsme_pecan.wsexpose(
StrategyCollection,
wtypes.text,
wtypes.text,
int,
wtypes.text,
wtypes.text,
)
def detail(
self, goal=None, marker=None, limit=None, sort_key='id', sort_dir='asc'
):
"""Retrieve a list of strategies with detail.
:param goal: goal UUID or name to filter by.
@@ -278,8 +327,7 @@ class StrategiesController(rest.RestController):
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
"""
context = pecan.request.context
policy.enforce(context, 'strategy:detail',
action='strategy:detail')
policy.enforce(context, 'strategy:detail', action='strategy:detail')
# NOTE(lucasagomes): /detail should only work against collections
parent = pecan.request.path.split('/')[:-1][-1]
if parent != "strategies":
@@ -295,7 +343,8 @@ class StrategiesController(rest.RestController):
filters['goal_name'] = goal
return self._get_strategies_collection(
filters, marker, limit, sort_key, sort_dir, expand, resource_url)
filters, marker, limit, sort_key, sort_dir, expand, resource_url
)
@wsme_pecan.wsexpose(wtypes.text, wtypes.text)
def state(self, strategy):
@@ -310,11 +359,19 @@ class StrategiesController(rest.RestController):
raise exception.HTTPNotFound
rpc_strategy = api_utils.get_resource('Strategy', strategy)
de_client = rpcapi.DecisionEngineAPI()
strategy_state = de_client.get_strategy_info(context,
rpc_strategy.name)
strategy_state.extend([{
'type': 'Name', 'state': rpc_strategy.name,
'mandatory': '', 'comment': ''}])
strategy_state = de_client.get_strategy_info(
context, rpc_strategy.name
)
strategy_state.extend(
[
{
'type': 'Name',
'state': rpc_strategy.name,
'mandatory': '',
'comment': '',
}
]
)
return strategy_state
@wsme_pecan.wsexpose(Strategy, wtypes.text)
@@ -325,7 +382,8 @@ class StrategiesController(rest.RestController):
"""
context = pecan.request.context
rpc_strategy = api_utils.get_resource('Strategy', strategy)
policy.enforce(context, 'strategy:get', rpc_strategy,
action='strategy:get')
policy.enforce(
context, 'strategy:get', rpc_strategy, action='strategy:get'
)
return Strategy.convert_with_links(rpc_strategy)
+22 -11
View File
@@ -132,8 +132,9 @@ class JsonType(wtypes.UserType):
def __str__(self):
# These are the json serializable native types
return ' | '.join(map(str, (wtypes.text, int, float,
BooleanType, list, dict, None)))
return ' | '.join(
map(str, (wtypes.text, int, float, BooleanType, list, dict, None))
)
@staticmethod
def validate(value):
@@ -178,17 +179,20 @@ class MultiType(wtypes.UserType):
else:
raise ValueError(
_("Wrong type. Expected '%(type)s', got '%(value)s'"),
type=self.types, value=type(value)
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=r'^(/[\w-]+)+$'),
mandatory=True)
op = wtypes.wsattr(wtypes.Enum(str, 'add', 'replace', 'remove'),
mandatory=True)
path = wtypes.wsattr(
wtypes.StringType(pattern=r'^(/[\w-]+)+$'), mandatory=True
)
op = wtypes.wsattr(
wtypes.Enum(str, 'add', 'replace', 'remove'), mandatory=True
)
value = wsme.wsattr(jsontype, default=wtypes.Unset)
@staticmethod
@@ -199,8 +203,14 @@ class JsonPatchType(wtypes.Base):
method may be overwritten by derived class.
"""
return ['/created_at', '/id', '/links', '/updated_at',
'/deleted_at', '/uuid']
return [
'/created_at',
'/id',
'/links',
'/updated_at',
'/deleted_at',
'/uuid',
]
@staticmethod
def mandatory_attrs():
@@ -222,8 +232,9 @@ class JsonPatchType(wtypes.Base):
_path = '/{}'.format(patch.path.split('/')[1])
if len(patch.allowed_attrs()) > 0:
if _path not in patch.allowed_attrs():
msg = _("'%s' is not an allowed attribute and can not be "
"updated")
msg = _(
"'%s' is not an allowed attribute and can not be updated"
)
raise wsme.exc.ClientSideError(msg % patch.path)
if _path in patch.internal_attrs():
+38 -20
View File
@@ -32,9 +32,11 @@ from watcher.common import utils
CONF = cfg.CONF
JSONPATCH_EXCEPTIONS = (jsonpatch.JsonPatchException,
jsonpatch.JsonPointerException,
KeyError)
JSONPATCH_EXCEPTIONS = (
jsonpatch.JsonPatchException,
jsonpatch.JsonPointerException,
KeyError,
)
def validate_limit(limit):
@@ -54,16 +56,20 @@ def validate_limit(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)
raise wsme.exc.ClientSideError(
_(
"Invalid sort direction: %s. "
"Acceptable values are "
"'asc' or 'desc'"
)
% sort_dir
)
def validate_sort_key(sort_key, allowed_fields):
# Very lightweight validation for now
if sort_key not in allowed_fields:
raise wsme.exc.ClientSideError(
_("Invalid sort key: %s") % sort_key)
raise wsme.exc.ClientSideError(_("Invalid sort key: %s") % sort_key)
def validate_search_filters(filters, allowed_fields):
@@ -72,7 +78,8 @@ def validate_search_filters(filters, allowed_fields):
for filter_name in filters:
if filter_name not in allowed_fields:
raise wsme.exc.ClientSideError(
_("Invalid filter: %s") % filter_name)
_("Invalid filter: %s") % filter_name
)
def check_need_api_sort(sort_key, additional_fields):
@@ -83,7 +90,7 @@ def make_api_sort(sorting_list, sort_key, sort_dir):
# First sort by uuid field, than sort by sort_key
# sort() ensures stable sorting, so we could
# make lexicographical sort
reverse_direction = (sort_dir == 'desc')
reverse_direction = sort_dir == 'desc'
sorting_list.sort(key=attrgetter('uuid'), reverse=reverse_direction)
sorting_list.sort(key=attrgetter(sort_key), reverse=reverse_direction)
@@ -92,8 +99,10 @@ 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')
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))
@@ -120,8 +129,11 @@ def check_audit_state_transition(patch, initial):
is_transition_valid = True
state_value = get_patch_value(patch, "state")
if state_value is not None:
is_transition_valid = objects.audit.AuditStateTransitionManager(
).check_transition(initial, state_value)
is_transition_valid = (
objects.audit.AuditStateTransitionManager().check_transition(
initial, state_value
)
)
return is_transition_valid
@@ -167,7 +179,8 @@ def allow_start_end_audit_time():
audits.
"""
return pecan.request.version.minor >= (
versions.VERSIONS.MINOR_1_START_END_TIMING.value)
versions.VERSIONS.MINOR_1_START_END_TIMING.value
)
def allow_force():
@@ -177,7 +190,8 @@ def allow_force():
launch audit when other action plan is ongoing.
"""
return pecan.request.version.minor >= (
versions.VERSIONS.MINOR_2_FORCE.value)
versions.VERSIONS.MINOR_2_FORCE.value
)
def allow_list_datamodel():
@@ -186,7 +200,8 @@ def allow_list_datamodel():
Version 1.3 of the API added support to list data model.
"""
return pecan.request.version.minor >= (
versions.VERSIONS.MINOR_3_DATAMODEL.value)
versions.VERSIONS.MINOR_3_DATAMODEL.value
)
def allow_webhook_api():
@@ -195,7 +210,8 @@ def allow_webhook_api():
Version 1.4 of the API added support to trigger webhook.
"""
return pecan.request.version.minor >= (
versions.VERSIONS.MINOR_4_WEBHOOK_API.value)
versions.VERSIONS.MINOR_4_WEBHOOK_API.value
)
def allow_skipped_action():
@@ -204,7 +220,8 @@ def allow_skipped_action():
Version 1.5 of the API added support to skipped actions.
"""
return pecan.request.version.minor >= (
versions.VERSIONS.MINOR_5_SKIPPED_ACTION.value)
versions.VERSIONS.MINOR_5_SKIPPED_ACTION.value
)
def allow_list_extend_compute_model():
@@ -214,4 +231,5 @@ def allow_list_extend_compute_model():
to the compute data model.
"""
return pecan.request.version.minor >= (
versions.VERSIONS.MINOR_6_EXT_COMPUTE_MODEL.value)
versions.VERSIONS.MINOR_6_EXT_COMPUTE_MODEL.value
)
+4 -3
View File
@@ -40,8 +40,9 @@ class WebhookController(rest.RestController):
super().__init__()
self.dc_client = rpcapi.DecisionEngineAPI()
@wsme_pecan.wsexpose(None, wtypes.text, body=types.jsontype,
status_code=HTTPStatus.ACCEPTED)
@wsme_pecan.wsexpose(
None, wtypes.text, body=types.jsontype, status_code=HTTPStatus.ACCEPTED
)
def post(self, audit_ident, body):
"""Trigger the given audit.
@@ -59,7 +60,7 @@ class WebhookController(rest.RestController):
allowed_state = (
objects.audit.State.PENDING,
objects.audit.State.SUCCEEDED,
)
)
if audit.state not in allowed_state:
raise exception.AuditStateNotAllowed(state=audit.state)
+7 -5
View File
@@ -56,8 +56,9 @@ class ContextHook(hooks.PecanHook):
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')
roles = (headers.get('X-Roles', None) and
headers.get('X-Roles').split(','))
roles = headers.get('X-Roles', None) and headers.get('X-Roles').split(
','
)
state.request.context = context.make_context(
auth_token=auth_token,
@@ -69,7 +70,8 @@ class ContextHook(hooks.PecanHook):
domain_id=domain_id,
domain_name=domain_name,
show_deleted=show_deleted,
roles=roles)
roles=roles,
)
class NoExceptionTracebackHook(hooks.PecanHook):
@@ -79,6 +81,7 @@ class NoExceptionTracebackHook(hooks.PecanHook):
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
@@ -92,8 +95,7 @@ class NoExceptionTracebackHook(hooks.PecanHook):
# Do nothing if there is no error.
# Status codes in the range 200 (OK) to 399 (400 = BAD_REQUEST) are not
# an error.
if (HTTPStatus.OK <= state.response.status_int <
HTTPStatus.BAD_REQUEST):
if HTTPStatus.OK <= state.response.status_int < HTTPStatus.BAD_REQUEST:
return
json_body = state.response.json
+9 -5
View File
@@ -37,12 +37,15 @@ class AuthTokenMiddleware(auth_token.AuthProtocol):
route_pattern_tpl = r'%s(\.json|\.xml)?$'
try:
self.public_api_routes = [re.compile(route_pattern_tpl % route_tpl)
for route_tpl in public_api_routes]
self.public_api_routes = [
re.compile(route_pattern_tpl % route_tpl)
for route_tpl in public_api_routes
]
except re.error as e:
LOG.exception(e)
raise exception.ConfigInvalid(
error_msg=_('Cannot compile public API routes'))
error_msg=_('Cannot compile public API routes')
)
super().__init__(app, conf)
@@ -52,8 +55,9 @@ class AuthTokenMiddleware(auth_token.AuthProtocol):
# 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(re.match(pattern, path)
for pattern in self.public_api_routes)
env['is_public_api'] = any(
re.match(pattern, path) for pattern in self.public_api_routes
)
if env['is_public_api']:
return self._app(env, start_response)
+23 -14
View File
@@ -49,17 +49,20 @@ class ParsableErrorMiddleware:
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)
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')]
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)
@@ -68,25 +71,31 @@ class ParsableErrorMiddleware:
if (state['status_code'] // 100) not in (2, 3):
req = webob.Request(environ)
if (
req.accept.best_match(
['application/json',
'application/xml']) == 'application/xml'
req.accept.best_match(['application/json', 'application/xml'])
== 'application/xml'
):
try:
# simple check xml is valid
body = [
et.ElementTree.tostring(
et.ElementTree.Element(
'error_message', text='\n'.join(app_iter)))]
'error_message', text='\n'.join(app_iter)
)
)
]
except et.ElementTree.ParseError as err:
LOG.error('Error parsing HTTP response: %s', err)
body = ['<error_message>{}'
'</error_message>'.format(state['status_code'])]
body = [
'<error_message>{}</error_message>'.format(
state['status_code']
)
]
state['headers'].append(('Content-Type', 'application/xml'))
else:
app_iter = [i.decode('utf-8') for i in app_iter]
body = [jsonutils.dumps(
{'error_message': '\n'.join(app_iter)})]
body = [
jsonutils.dumps({'error_message': '\n'.join(app_iter)})
]
body = [item.encode('utf-8') for item in body]
state['headers'].append(('Content-Type', 'application/json'))
state['headers'].append(('Content-Length', str(len(body[0]))))
+5 -3
View File
@@ -35,8 +35,10 @@ def initialize_wsgi_app(show_deprecated=False):
CONF.log_opt_values(LOG, log.DEBUG)
if show_deprecated:
LOG.warning("Using watcher/api/app.wsgi is deprecated and it will "
"be removed in U release. Please use automatically "
"generated watcher-api-wsgi instead.")
LOG.warning(
"Using watcher/api/app.wsgi is deprecated and it will "
"be removed in U release. Please use automatically "
"generated watcher-api-wsgi instead."
)
return app.VersionSelectorApplication()
+42 -25
View File
@@ -32,7 +32,6 @@ LOG = log.getLogger(__name__)
class DefaultActionPlanHandler(base.BaseActionPlanHandler):
def __init__(self, context, service, action_plan_uuid):
super().__init__()
self.ctx = context
@@ -42,16 +41,19 @@ class DefaultActionPlanHandler(base.BaseActionPlanHandler):
def execute(self):
try:
action_plan = objects.ActionPlan.get_by_uuid(
self.ctx, self.action_plan_uuid, eager=True)
self.ctx, self.action_plan_uuid, eager=True
)
if action_plan.state == objects.action_plan.State.CANCELLED:
self._update_action_from_pending_to_cancelled()
return
action_plan.state = objects.action_plan.State.ONGOING
action_plan.save()
notifications.action_plan.send_action_notification(
self.ctx, action_plan,
self.ctx,
action_plan,
action=fields.NotificationAction.EXECUTION,
phase=fields.NotificationPhase.START)
phase=fields.NotificationPhase.START,
)
applier = default.DefaultApplier(self.ctx, self.service)
applier.execute(self.action_plan_uuid)
@@ -59,25 +61,29 @@ class DefaultActionPlanHandler(base.BaseActionPlanHandler):
# If any action has failed the action plan should be FAILED
# Define default values for successful execution
ap_state = objects.action_plan.State.SUCCEEDED
notification_kwargs = {
'phase': fields.NotificationPhase.END
}
notification_kwargs = {'phase': fields.NotificationPhase.END}
failed_filter = {'action_plan_uuid': self.action_plan_uuid,
'state': objects.action.State.FAILED}
failed_filter = {
'action_plan_uuid': self.action_plan_uuid,
'state': objects.action.State.FAILED,
}
failed_actions = objects.Action.list(
self.ctx, filters=failed_filter, eager=True)
self.ctx, filters=failed_filter, eager=True
)
if failed_actions:
ap_state = objects.action_plan.State.FAILED
notification_kwargs = {
'phase': fields.NotificationPhase.ERROR,
'priority': fields.NotificationPriority.ERROR
'priority': fields.NotificationPriority.ERROR,
}
skipped_filter = {'action_plan_uuid': self.action_plan_uuid,
'state': objects.action.State.SKIPPED}
skipped_filter = {
'action_plan_uuid': self.action_plan_uuid,
'state': objects.action.State.SKIPPED,
}
skipped_actions = objects.Action.list(
self.ctx, filters=skipped_filter, eager=True)
self.ctx, filters=skipped_filter, eager=True
)
if skipped_actions:
status_message = _("One or more actions were skipped.")
action_plan.status_message = status_message
@@ -85,9 +91,11 @@ class DefaultActionPlanHandler(base.BaseActionPlanHandler):
action_plan.state = ap_state
action_plan.save()
notifications.action_plan.send_action_notification(
self.ctx, action_plan,
self.ctx,
action_plan,
action=fields.NotificationAction.EXECUTION,
**notification_kwargs)
**notification_kwargs,
)
except exception.ActionPlanCancelled as e:
LOG.exception(e)
@@ -95,34 +103,43 @@ class DefaultActionPlanHandler(base.BaseActionPlanHandler):
self._update_action_from_pending_to_cancelled()
action_plan.save()
notifications.action_plan.send_cancel_notification(
self.ctx, action_plan,
self.ctx,
action_plan,
action=fields.NotificationAction.CANCEL,
phase=fields.NotificationPhase.END)
phase=fields.NotificationPhase.END,
)
except Exception as e:
LOG.exception(e)
action_plan = objects.ActionPlan.get_by_uuid(
self.ctx, self.action_plan_uuid, eager=True)
self.ctx, self.action_plan_uuid, eager=True
)
if action_plan.state == objects.action_plan.State.CANCELLING:
action_plan.state = objects.action_plan.State.FAILED
action_plan.save()
notifications.action_plan.send_cancel_notification(
self.ctx, action_plan,
self.ctx,
action_plan,
action=fields.NotificationAction.CANCEL,
priority=fields.NotificationPriority.ERROR,
phase=fields.NotificationPhase.ERROR)
phase=fields.NotificationPhase.ERROR,
)
else:
action_plan.state = objects.action_plan.State.FAILED
action_plan.save()
notifications.action_plan.send_action_notification(
self.ctx, action_plan,
self.ctx,
action_plan,
action=fields.NotificationAction.EXECUTION,
priority=fields.NotificationPriority.ERROR,
phase=fields.NotificationPhase.ERROR)
phase=fields.NotificationPhase.ERROR,
)
def _update_action_from_pending_to_cancelled(self):
filters = {'action_plan_uuid': self.action_plan_uuid,
'state': objects.action.State.PENDING}
filters = {
'action_plan_uuid': self.action_plan_uuid,
'state': objects.action.State.PENDING,
}
actions = objects.Action.list(self.ctx, filters=filters, eager=True)
if actions:
for a in actions:
@@ -38,10 +38,7 @@ class ChangeNodePowerState(base.BaseAction):
The action schema is::
schema = Schema({
'resource_id': str,
'state': str,
})
schema = Schema({'resource_id': str, 'state': str})
The `resource_id` references a baremetal node id (list of available
ironic nodes is returned by this command: ``ironic node-list``).
@@ -55,19 +52,15 @@ class ChangeNodePowerState(base.BaseAction):
return {
'type': 'object',
'properties': {
'resource_id': {
'type': 'string',
"minlength": 1
},
'resource_name': {
'type': 'string',
"minlength": 1
},
'resource_id': {'type': 'string', "minlength": 1},
'resource_name': {'type': 'string', "minlength": 1},
'state': {
'type': 'string',
'enum': [metal_constants.PowerState.ON.value,
metal_constants.PowerState.OFF.value]
}
'enum': [
metal_constants.PowerState.ON.value,
metal_constants.PowerState.OFF.value,
],
},
},
'required': ['resource_id', 'state'],
'additionalProperties': False,
@@ -95,7 +88,8 @@ class ChangeNodePowerState(base.BaseAction):
def _node_manage_power(self, state, retry=60):
if state is None:
raise exception.IllegalArgumentException(
message=_("The target state is not defined"))
message=_("The target state is not defined")
)
metal_helper = metal_helper_factory.get_helper(self.osc)
node = metal_helper.get_node(self.node_uuid)
@@ -106,14 +100,15 @@ class ChangeNodePowerState(base.BaseAction):
if state == metal_constants.PowerState.OFF.value:
compute_node = node.get_hypervisor_node().to_dict()
if (compute_node['running_vms'] == 0):
if compute_node['running_vms'] == 0:
node.set_power_state(state)
else:
LOG.warning(
"Compute node %s has %s running vms and will "
"NOT be shut off.",
compute_node["hypervisor_hostname"],
compute_node['running_vms'])
compute_node['running_vms'],
)
return False
else:
node.set_power_state(state)
@@ -136,4 +131,4 @@ class ChangeNodePowerState(base.BaseAction):
def get_description(self):
"""Description of the action"""
return ("Compute node power on/off through Ironic or MaaS.")
return "Compute node power on/off through Ironic or MaaS."
@@ -32,11 +32,9 @@ class ChangeNovaServiceState(base.BaseAction):
The action schema is::
schema = Schema({
'resource_id': str,
'state': str,
'disabled_reason': str,
})
schema = Schema(
{'resource_id': str, 'state': str, 'disabled_reason': str}
)
The `resource_id` references a nova-compute service name.
The `state` value should either be `up` or `down`.
@@ -54,25 +52,18 @@ class ChangeNovaServiceState(base.BaseAction):
return {
'type': 'object',
'properties': {
'resource_id': {
'type': 'string',
"minlength": 1
},
'resource_name': {
'type': 'string',
"minlength": 1
},
'resource_id': {'type': 'string', "minlength": 1},
'resource_name': {'type': 'string', "minlength": 1},
'state': {
'type': 'string',
'enum': [element.ServiceState.ONLINE.value,
element.ServiceState.OFFLINE.value,
element.ServiceState.ENABLED.value,
element.ServiceState.DISABLED.value]
'enum': [
element.ServiceState.ONLINE.value,
element.ServiceState.OFFLINE.value,
element.ServiceState.ENABLED.value,
element.ServiceState.DISABLED.value,
],
},
'disabled_reason': {
'type': 'string',
"minlength": 1
}
'disabled_reason': {'type': 'string', "minlength": 1},
},
'required': ['resource_id', 'state'],
'additionalProperties': False,
@@ -109,7 +100,8 @@ class ChangeNovaServiceState(base.BaseAction):
def _nova_manage_service(self, state):
if state is None:
raise exception.IllegalArgumentException(
message=_("The target state is not defined"))
message=_("The target state is not defined")
)
nova = nova_helper.NovaHelper(osc=self.osc)
if state is True:
@@ -129,17 +121,24 @@ class ChangeNovaServiceState(base.BaseAction):
service = next((s for s in services if s.host == self.host), None)
if service is None:
raise exception.ActionSkipped(
_("nova-compute service %s not found") % self.host)
_("nova-compute service %s not found") % self.host
)
if service.status == self.state:
raise exception.ActionSkipped(
_("nova-compute service %s is already in state %s") %
(self.host, self.state))
_(
"nova-compute service %(service)s is already in "
"state %(state)s"
)
% {'service': self.host, 'state': self.state}
)
def post_condition(self):
pass
def get_description(self):
"""Description of the action"""
return ("Disables or enables the nova-compute service."
"A disabled nova-compute service can not be selected "
"by the nova for future deployment of new server.")
return (
"Disables or enables the nova-compute service."
"A disabled nova-compute service can not be selected "
"by the nova for future deployment of new server."
)
+3 -2
View File
@@ -28,8 +28,9 @@ class ActionFactory:
def make_action(self, object_action, osc=None):
LOG.debug("Creating instance of %s", object_action.action_type)
loaded_action = self.action_loader.load(name=object_action.action_type,
osc=osc)
loaded_action = self.action_loader.load(
name=object_action.action_type, osc=osc
)
loaded_action.input_parameters = object_action.input_parameters
LOG.debug("Checking the input parameters")
# NOTE(jed) if we change the schema of an action and we try to reload
+94 -63
View File
@@ -39,12 +39,14 @@ class Migrate(base.BaseAction):
The action schema is::
schema = Schema({
'resource_id': str, # should be a UUID
'migration_type': str, # choices -> "live", "cold"
'destination_node': str,
'source_node': str,
})
schema = Schema(
{
'resource_id': str, # should be a UUID
'migration_type': str, # choices -> "live", "cold"
'destination_node': str,
'source_node': str,
}
)
The `resource_id` is the UUID of the server to migrate.
The `source_node` and `destination_node` parameters are respectively the
@@ -72,28 +74,21 @@ class Migrate(base.BaseAction):
'destination_node': {
"anyof": [
{'type': 'string', "minLength": 1},
{'type': 'None'}
]
},
'migration_type': {
'type': 'string',
"enum": ["live", "cold"]
{'type': 'None'},
]
},
'migration_type': {'type': 'string', "enum": ["live", "cold"]},
'resource_id': {
'type': 'string',
"minlength": 1,
"pattern": ("^([a-fA-F0-9]){8}-([a-fA-F0-9]){4}-"
"([a-fA-F0-9]){4}-([a-fA-F0-9]){4}-"
"([a-fA-F0-9]){12}$")
"pattern": (
"^([a-fA-F0-9]){8}-([a-fA-F0-9]){4}-"
"([a-fA-F0-9]){4}-([a-fA-F0-9]){4}-"
"([a-fA-F0-9]){12}$"
),
},
'resource_name': {
'type': 'string',
"minlength": 1
},
'source_node': {
'type': 'string',
"minLength": 1
}
'resource_name': {'type': 'string', "minlength": 1},
'source_node': {'type': 'string', "minLength": 1},
},
'required': ['migration_type', 'resource_id', 'source_node'],
'additionalProperties': False,
@@ -118,19 +113,25 @@ class Migrate(base.BaseAction):
def _live_migrate_instance(self, nova, destination):
result = None
try:
result = nova.live_migrate_instance(instance_id=self.instance_uuid,
dest_hostname=destination)
result = nova.live_migrate_instance(
instance_id=self.instance_uuid, dest_hostname=destination
)
except exception.NovaClientError as e:
LOG.debug("Nova client exception occurred while live "
"migrating instance "
"%(instance)s.Exception: %(exception)s",
{'instance': self.instance_uuid, 'exception': e})
LOG.debug(
"Nova client exception occurred while live "
"migrating instance "
"%(instance)s.Exception: %(exception)s",
{'instance': self.instance_uuid, 'exception': e},
)
except Exception as e:
LOG.exception(e)
LOG.critical("Unexpected error occurred. Migration failed for "
"instance %s. Leaving instance on previous "
"host.", self.instance_uuid)
LOG.critical(
"Unexpected error occurred. Migration failed for "
"instance %s. Leaving instance on previous "
"host.",
self.instance_uuid,
)
return result
@@ -138,13 +139,16 @@ class Migrate(base.BaseAction):
result = None
try:
result = nova.watcher_non_live_migrate_instance(
instance_id=self.instance_uuid,
dest_hostname=destination)
instance_id=self.instance_uuid, dest_hostname=destination
)
except Exception as exc:
LOG.exception(exc)
LOG.critical("Unexpected error occurred. Migration failed for "
"instance %s. Leaving instance on previous "
"host.", self.instance_uuid)
LOG.critical(
"Unexpected error occurred. Migration failed for "
"instance %s. Leaving instance on previous "
"host.",
self.instance_uuid,
)
return result
def _abort_cold_migrate(self, nova):
@@ -156,17 +160,24 @@ class Migrate(base.BaseAction):
LOG.warning("Abort operation for cold migration is not implemented")
def _abort_live_migrate(self, nova, source, destination):
return nova.abort_live_migrate(instance_id=self.instance_uuid,
source=source, destination=destination)
return nova.abort_live_migrate(
instance_id=self.instance_uuid,
source=source,
destination=destination,
)
def migrate(self, destination=None):
nova = nova_helper.NovaHelper(osc=self.osc)
if destination is None:
LOG.debug("Migrating instance %s, destination node will be "
"determined by nova-scheduler", self.instance_uuid)
LOG.debug(
"Migrating instance %s, destination node will be "
"determined by nova-scheduler",
self.instance_uuid,
)
else:
LOG.debug("Migrate instance %s to %s", self.instance_uuid,
destination)
LOG.debug(
"Migrate instance %s to %s", self.instance_uuid, destination
)
try:
nova.find_instance(self.instance_uuid)
except exception.ComputeResourceNotFound:
@@ -178,9 +189,14 @@ class Migrate(base.BaseAction):
return self._cold_migrate_instance(nova, destination)
else:
raise exception.Invalid(
message=(_("Migration of type '%(migration_type)s' is not "
"supported.") %
{'migration_type': self.migration_type}))
message=(
_(
"Migration of type '%(migration_type)s' is not "
"supported."
)
% {'migration_type': self.migration_type}
)
)
def execute(self):
return self.migrate(destination=self.destination_node)
@@ -199,8 +215,10 @@ class Migrate(base.BaseAction):
return self._abort_cold_migrate(nova)
elif self.migration_type == self.LIVE_MIGRATION:
return self._abort_live_migrate(
nova, source=self.source_node,
destination=self.destination_node)
nova,
source=self.source_node,
destination=self.destination_node,
)
def pre_condition(self):
"""Check migration preconditions
@@ -219,44 +237,57 @@ class Migrate(base.BaseAction):
instance = nova.find_instance(self.instance_uuid)
except exception.ComputeResourceNotFound:
raise exception.ActionSkipped(
_("Instance %s not found") % self.instance_uuid)
_("Instance %s not found") % self.instance_uuid
)
# Check that the instance is running on source_node
instance_host = instance.host
if instance_host != self.source_node:
raise exception.ActionSkipped(
_("Instance %(instance)s is not running on source node "
"%(source)s (currently on %(current)s)") %
{'instance': self.instance_uuid,
'source': self.source_node,
'current': instance_host})
_(
"Instance %(instance)s is not running on source node "
"%(source)s (currently on %(current)s)"
)
% {
'instance': self.instance_uuid,
'source': self.source_node,
'current': instance_host,
}
)
# Check destination node if specified
if self.destination_node:
try:
# Find the compute node and check if service is enabled
dest_node = nova.get_compute_node_by_hostname(
self.destination_node)
self.destination_node
)
# Check if compute service is enabled
if dest_node.status != 'enabled':
raise exception.ActionExecutionFailure(
_("Destination node %s is not in enabled state") %
self.destination_node)
_("Destination node %s is not in enabled state")
% self.destination_node
)
except exception.ComputeNodeNotFound:
raise exception.ActionExecutionFailure(
_("Destination node %s not found") %
self.destination_node)
_("Destination node %s not found") % self.destination_node
)
# Check instance status based on migration type
instance_status = instance.status
if self.migration_type == self.LIVE_MIGRATION:
if instance_status != 'ACTIVE':
raise exception.ActionExecutionFailure(
_("Live migration requires instance %(instance)s to be "
"in ACTIVE status (current status: %(status)s)") %
{'instance': self.instance_uuid,
'status': instance_status})
_(
"Live migration requires instance %(instance)s to be "
"in ACTIVE status (current status: %(status)s)"
)
% {
'instance': self.instance_uuid,
'status': instance_status,
}
)
def post_condition(self):
# TODO(jed): check extra parameters (network response, etc.)
+6 -22
View File
@@ -30,9 +30,7 @@ class Nop(base.BaseAction):
The action schema is::
schema = Schema({
'message': str,
})
schema = Schema({'message': str})
The `message` is the actual message that will be logged.
"""
@@ -44,25 +42,11 @@ class Nop(base.BaseAction):
return {
'type': 'object',
'properties': {
'message': {
'type': ['string', 'null']
},
'skip_pre_condition': {
'type': 'boolean',
'default': False
},
'fail_pre_condition': {
'type': 'boolean',
'default': False
},
'fail_execute': {
'type': 'boolean',
'default': False
},
'fail_post_condition': {
'type': 'boolean',
'default': False
}
'message': {'type': ['string', 'null']},
'skip_pre_condition': {'type': 'boolean', 'default': False},
'fail_pre_condition': {'type': 'boolean', 'default': False},
'fail_execute': {'type': 'boolean', 'default': False},
'fail_post_condition': {'type': 'boolean', 'default': False},
},
'required': ['message'],
'additionalProperties': False,
+27 -20
View File
@@ -34,10 +34,12 @@ class Resize(base.BaseAction):
The action schema is::
schema = Schema({
'resource_id': str, # should be a UUID
'flavor': str, # should be either ID or Name of Flavor
})
schema = Schema(
{
'resource_id': str, # should be a UUID
'flavor': str, # should be either ID or Name of Flavor
}
)
The `resource_id` is the UUID of the server to resize.
The `flavor` is the ID or Name of Flavor (Nova accepts either ID or Name
@@ -55,14 +57,13 @@ class Resize(base.BaseAction):
'resource_id': {
'type': 'string',
'minlength': 1,
'pattern': ('^([a-fA-F0-9]){8}-([a-fA-F0-9]){4}-'
'([a-fA-F0-9]){4}-([a-fA-F0-9]){4}-'
'([a-fA-F0-9]){12}$')
},
'flavor': {
'type': 'string',
'minlength': 1,
'pattern': (
'^([a-fA-F0-9]){8}-([a-fA-F0-9]){4}-'
'([a-fA-F0-9]){4}-([a-fA-F0-9]){4}-'
'([a-fA-F0-9]){12}$'
),
},
'flavor': {'type': 'string', 'minlength': 1},
},
'required': ['resource_id', 'flavor'],
'additionalProperties': False,
@@ -78,24 +79,28 @@ class Resize(base.BaseAction):
def resize(self):
nova = nova_helper.NovaHelper(osc=self.osc)
LOG.debug("Resize instance %s to %s flavor", self.instance_uuid,
self.flavor)
LOG.debug(
"Resize instance %s to %s flavor", self.instance_uuid, self.flavor
)
try:
nova.find_instance(self.instance_uuid)
except exception.ComputeResourceNotFound:
LOG.warning("Instance %s not found, skipping resize",
self.instance_uuid)
LOG.warning(
"Instance %s not found, skipping resize", self.instance_uuid
)
raise exception.InstanceNotFound(name=self.instance_uuid)
result = None
try:
result = nova.resize_instance(
instance_id=self.instance_uuid, flavor=self.flavor)
instance_id=self.instance_uuid, flavor=self.flavor
)
except Exception as exc:
LOG.exception(exc)
LOG.critical(
"Unexpected error occurred. Resizing failed for "
"instance %s.", self.instance_uuid)
"Unexpected error occurred. Resizing failed for instance %s.",
self.instance_uuid,
)
return False
return result
@@ -120,13 +125,15 @@ class Resize(base.BaseAction):
nova.find_instance(self.instance_uuid)
except exception.ComputeResourceNotFound:
raise exception.ActionSkipped(
_("Instance %s not found") % self.instance_uuid)
_("Instance %s not found") % self.instance_uuid
)
try:
nova.get_flavor_id(self.flavor)
except exception.ComputeResourceNotFound:
raise exception.ActionExecutionFailure(
_("Flavor %s not found") % self.flavor)
_("Flavor %s not found") % self.flavor
)
def post_condition(self):
# TODO(jed): check extra parameters (network response, etc.)
+2 -9
View File
@@ -31,9 +31,7 @@ class Sleep(base.BaseAction):
The action schema is::
schema = Schema({
'duration': float,
})
schema = Schema({'duration': float})
The `duration` is expressed in seconds.
"""
@@ -44,12 +42,7 @@ class Sleep(base.BaseAction):
def schema(self):
return {
'type': 'object',
'properties': {
'duration': {
'type': 'number',
'minimum': 0
},
},
'properties': {'duration': {'type': 'number', 'minimum': 0}},
'required': ['duration'],
'additionalProperties': False,
}
+41 -24
View File
@@ -31,9 +31,11 @@ class Stop(base.BaseAction):
The action schema is::
schema = Schema({
'resource_id': str, # should be a UUID
})
schema = Schema(
{
'resource_id': str # should be a UUID
}
)
The `resource_id` is the UUID of the server instance to stop.
The action will check if the instance exists, verify its current state,
@@ -48,10 +50,12 @@ class Stop(base.BaseAction):
'resource_id': {
'type': 'string',
"minlength": 1,
"pattern": ("^([a-fA-F0-9]){8}-([a-fA-F0-9]){4}-"
"([a-fA-F0-9]){4}-([a-fA-F0-9]){4}-"
"([a-fA-F0-9]){12}$")
},
"pattern": (
"^([a-fA-F0-9]){8}-([a-fA-F0-9]){4}-"
"([a-fA-F0-9]){4}-([a-fA-F0-9]){4}-"
"([a-fA-F0-9]){12}$"
),
}
},
'required': ['resource_id'],
'additionalProperties': False,
@@ -68,19 +72,24 @@ class Stop(base.BaseAction):
try:
result = nova.stop_instance(instance_id=self.instance_uuid)
except exception.NovaClientError as e:
LOG.debug("Nova client exception occurred while stopping "
"instance %(instance)s. Exception: %(exception)s",
{'instance': self.instance_uuid, 'exception': e})
LOG.debug(
"Nova client exception occurred while stopping "
"instance %(instance)s. Exception: %(exception)s",
{'instance': self.instance_uuid, 'exception': e},
)
return False
except Exception as e:
LOG.debug("An unexpected error occurred while stopping "
"instance %s: %s", self.instance_uuid, str(e))
LOG.debug(
"An unexpected error occurred while stopping instance %s: %s",
self.instance_uuid,
str(e),
)
return False
if result:
LOG.debug(
"Successfully stopped instance %(uuid)s",
{'uuid': self.instance_uuid}
{'uuid': self.instance_uuid},
)
return True
else:
@@ -91,13 +100,13 @@ class Stop(base.BaseAction):
LOG.info(
"Instance %(uuid)s not found, "
"considering stop operation successful",
{'uuid': self.instance_uuid}
{'uuid': self.instance_uuid},
)
return True
LOG.error(
"Failed to stop instance %(uuid)s",
{'uuid': self.instance_uuid}
{'uuid': self.instance_uuid},
)
return False
@@ -115,20 +124,20 @@ class Stop(base.BaseAction):
LOG.debug(
"Successfully reverted stop action and started instance "
"%(uuid)s",
{'uuid': self.instance_uuid}
{'uuid': self.instance_uuid},
)
return result
else:
LOG.info(
"Failed to start instance %(uuid)s during revert. "
"This may be normal for instances with special configs.",
{'uuid': self.instance_uuid}
{'uuid': self.instance_uuid},
)
except Exception as exc:
LOG.info(
"Could not start instance %(uuid)s during revert: %(error)s. "
"This may be normal for instances with special configs.",
{'uuid': self.instance_uuid, 'error': str(exc)}
{'uuid': self.instance_uuid, 'error': str(exc)},
)
return False
@@ -138,8 +147,11 @@ class Stop(base.BaseAction):
def abort(self):
"""Abort the stop action - not applicable for stop operations"""
LOG.info("Abort operation is not applicable for stop action on "
" instance %s", self.instance_uuid)
LOG.info(
"Abort operation is not applicable for stop action on "
" instance %s",
self.instance_uuid,
)
return False
def pre_condition(self):
@@ -156,14 +168,19 @@ class Stop(base.BaseAction):
instance = nova.find_instance(self.instance_uuid)
except exception.ComputeResourceNotFound:
raise exception.ActionSkipped(
_("Instance %s not found") % self.instance_uuid)
_("Instance %s not found") % self.instance_uuid
)
current_state = instance.status
LOG.debug("Instance %s pre-condition check: state=%s",
self.instance_uuid, current_state)
LOG.debug(
"Instance %s pre-condition check: state=%s",
self.instance_uuid,
current_state,
)
if current_state == 'SHUTOFF':
raise exception.ActionSkipped(
_("Instance %s is already stopped") % self.instance_uuid)
_("Instance %s is already stopped") % self.instance_uuid
)
def post_condition(self):
pass
+65 -43
View File
@@ -42,12 +42,14 @@ class VolumeMigrate(base.BaseAction):
The action schema is::
schema = Schema({
'resource_id': str, # should be a UUID
'migration_type': str, # choices -> "swap", "migrate","retype"
'destination_node': str,
'destination_type': str,
})
schema = Schema(
{
'resource_id': str, # should be a UUID
'migration_type': str, # choices -> "swap", "migrate","retype"
'destination_node': str,
'destination_type': str,
}
)
The `resource_id` is the UUID of cinder volume to migrate.
The `destination_node` is the destination block storage pool name.
@@ -80,30 +82,29 @@ class VolumeMigrate(base.BaseAction):
'resource_id': {
'type': 'string',
"minlength": 1,
"pattern": ("^([a-fA-F0-9]){8}-([a-fA-F0-9]){4}-"
"([a-fA-F0-9]){4}-([a-fA-F0-9]){4}-"
"([a-fA-F0-9]){12}$")
},
'resource_name': {
'type': 'string',
"minlength": 1
"pattern": (
"^([a-fA-F0-9]){8}-([a-fA-F0-9]){4}-"
"([a-fA-F0-9]){4}-([a-fA-F0-9]){4}-"
"([a-fA-F0-9]){12}$"
),
},
'resource_name': {'type': 'string', "minlength": 1},
'migration_type': {
'type': 'string',
"enum": ["swap", "retype", "migrate"]
"enum": ["swap", "retype", "migrate"],
},
'destination_node': {
"anyof": [
{'type': 'string', "minLength": 1},
{'type': 'None'}
]
{'type': 'None'},
]
},
'destination_type': {
"anyof": [
{'type': 'string', "minLength": 1},
{'type': 'None'}
]
}
{'type': 'None'},
]
},
},
'required': ['resource_id', 'migration_type'],
'additionalProperties': False,
@@ -149,21 +150,26 @@ class VolumeMigrate(base.BaseAction):
except exception.ComputeResourceNotFound:
LOG.debug(
"Could not find instance %s, could not determine whether"
"it's safe to migrate", instance_id
"it's safe to migrate",
instance_id,
)
return False
instance_status = instance.status
LOG.debug(
"volume: %s is attached to instance: %s "
"in instance status: %s",
volume.id, instance_id, instance_status)
"volume: %s is attached to instance: %s in instance status: %s",
volume.id,
instance_id,
instance_status,
)
# NOTE(sean-k-mooney): This used to allow RESIZED which
# is the resize_verify task state, that is not an acceptable time
# to migrate volumes, if nova does not block this in the API
# today that is probably a bug. PAUSED is also questionable but
# it should generally be safe.
return (volume.status == 'in-use' and
instance_status in ('ACTIVE', 'PAUSED'))
return volume.status == 'in-use' and instance_status in (
'ACTIVE',
'PAUSED',
)
def _migrate(self, volume_id, dest_node, dest_type):
try:
@@ -172,15 +178,21 @@ class VolumeMigrate(base.BaseAction):
if self.migration_type in (self.SWAP, self.MIGRATE):
if not self._can_swap(volume):
raise exception.Invalid(
message=(_("Invalid state for swapping volume")))
message=(_("Invalid state for swapping volume"))
)
return self.cinder_util.migrate(volume, dest_node)
elif self.migration_type == self.RETYPE:
return self.cinder_util.retype(volume, dest_type)
else:
raise exception.Invalid(
message=(_("Migration of type '%(migration_type)s' is not "
"supported.") %
{'migration_type': self.migration_type}))
message=(
_(
"Migration of type '%(migration_type)s' is not "
"supported."
)
% {'migration_type': self.migration_type}
)
)
except exception.Invalid as ei:
LOG.exception(ei)
return False
@@ -190,9 +202,9 @@ class VolumeMigrate(base.BaseAction):
return False
def execute(self):
return self._migrate(self.volume_id,
self.destination_node,
self.destination_type)
return self._migrate(
self.volume_id, self.destination_node, self.destination_type
)
def revert(self):
LOG.warning("revert not supported")
@@ -215,7 +227,8 @@ class VolumeMigrate(base.BaseAction):
volume = self.cinder_util.get_volume(self.volume_id)
except cinder_exception.NotFound:
raise exception.ActionSkipped(
_("Volume %s not found") % self.volume_id)
_("Volume %s not found") % self.volume_id
)
# Check if destination_type exists (if specified)
if self.destination_type:
@@ -223,31 +236,40 @@ class VolumeMigrate(base.BaseAction):
type_names = [vt.name for vt in volume_types]
if self.destination_type not in type_names:
raise exception.ActionExecutionFailure(
_("Volume type %s not found") % self.destination_type)
_("Volume type %s not found") % self.destination_type
)
# Check if destination_node (pool) exists (if specified)
if self.destination_node:
try:
self.cinder_util.get_storage_pool_by_name(
self.destination_node)
self.destination_node
)
except exception.PoolNotFound:
raise exception.ActionExecutionFailure(
_("Pool %s not found") % self.destination_node)
_("Pool %s not found") % self.destination_node
)
# Check if retype to same type
if (self.migration_type == self.RETYPE and
self.destination_type and
volume.volume_type == self.destination_type):
if (
self.migration_type == self.RETYPE
and self.destination_type
and volume.volume_type == self.destination_type
):
raise exception.ActionSkipped(
_("Volume type is already %s") % self.destination_type)
_("Volume type is already %s") % self.destination_type
)
# Check if migrate to same node
if (self.migration_type in (self.SWAP, self.MIGRATE) and
self.destination_node):
if (
self.migration_type in (self.SWAP, self.MIGRATE)
and self.destination_node
):
current_host = getattr(volume, 'os-vol-host-attr:host')
if current_host == self.destination_node:
raise exception.ActionSkipped(
_("Volume is already on node %s") % self.destination_node)
_("Volume is already on node %s") % self.destination_node
)
def post_condition(self):
pass
+5 -3
View File
@@ -51,13 +51,15 @@ class DefaultApplier(base.BaseApplier):
self._engine = self._loader.load(
name=selected_workflow_engine,
context=self.context,
applier_manager=self.applier_manager)
applier_manager=self.applier_manager,
)
return self._engine
def execute(self, action_plan_uuid):
LOG.debug("Executing action plan %s ", action_plan_uuid)
filters = {'action_plan_uuid': action_plan_uuid}
actions = objects.Action.list(self.context, filters=filters,
eager=True)
actions = objects.Action.list(
self.context, filters=filters, eager=True
)
return self.engine.execute(actions)
+2 -4
View File
@@ -16,11 +16,9 @@ from watcher.common.loader import default
class DefaultWorkFlowEngineLoader(default.DefaultLoader):
def __init__(self):
super().__init__(
namespace='watcher_workflow_engines')
super().__init__(namespace='watcher_workflow_engines')
class DefaultActionLoader(default.DefaultLoader):
def __init__(self):
super().__init__(
namespace='watcher_actions')
super().__init__(namespace='watcher_actions')
-1
View File
@@ -26,7 +26,6 @@ CONF = conf.CONF
class ApplierManager(service_manager.ServiceManager):
@property
def service_name(self):
return 'watcher-applier'
+10 -7
View File
@@ -32,13 +32,14 @@ class TriggerActionPlan:
self.applier_manager = applier_manager
workers = CONF.watcher_applier.workers
self.executor = executor.get_futurist_pool_executor(
max_workers=workers)
max_workers=workers
)
def do_launch_action_plan(self, context, action_plan_uuid):
try:
cmd = default.DefaultActionPlanHandler(context,
self.applier_manager,
action_plan_uuid)
cmd = default.DefaultActionPlanHandler(
context, self.applier_manager, action_plan_uuid
)
cmd.execute()
except Exception as e:
LOG.exception(e)
@@ -46,12 +47,14 @@ class TriggerActionPlan:
def launch_action_plan(self, context, action_plan_uuid):
LOG.debug("Trigger ActionPlan %s", action_plan_uuid)
action_plan = objects.ActionPlan.get_by_uuid(
context, action_plan_uuid, eager=True)
context, action_plan_uuid, eager=True
)
action_plan.hostname = CONF.host
action_plan.save()
# submit
executor.log_executor_stats(self.executor, name="action-plan-pool")
self.executor.submit(self.do_launch_action_plan, context,
action_plan_uuid)
self.executor.submit(
self.do_launch_action_plan, context, action_plan_uuid
)
return action_plan_uuid
+2 -3
View File
@@ -27,7 +27,6 @@ CONF = conf.CONF
class ApplierAPI(service.Service):
def __init__(self):
super().__init__(ApplierAPIManager)
@@ -36,11 +35,11 @@ class ApplierAPI(service.Service):
raise exception.InvalidUuidOrName(name=action_plan_uuid)
self.conductor_client.cast(
context, 'launch_action_plan', action_plan_uuid=action_plan_uuid)
context, 'launch_action_plan', action_plan_uuid=action_plan_uuid
)
class ApplierAPIManager(service_manager.ServiceManager):
@property
def service_name(self):
return None
+21 -12
View File
@@ -44,12 +44,19 @@ class ApplierMonitor(service.ServiceMonitoringBase):
pending_actionplans = objects.ActionPlan.list(
context,
filters={'state': objects.action_plan.State.PENDING,
'hostname': host},
eager=True)
filters={
'state': objects.action_plan.State.PENDING,
'hostname': host,
},
eager=True,
)
for actionplan in pending_actionplans:
LOG.warning("Retriggering action plan %s in Pending state on "
"failed host %s", actionplan.uuid, host)
LOG.warning(
"Retriggering action plan %s in Pending state on "
"failed host %s",
actionplan.uuid,
host,
)
actionplan.hostname = None
actionplan.save()
self.applier_client.launch_action_plan(context, actionplan.uuid)
@@ -75,16 +82,18 @@ class ApplierMonitor(service.ServiceMonitoringBase):
# on services status changes
continue
if changed:
notifications.service.send_service_update(context,
watcher_service,
state=result)
notifications.service.send_service_update(
context, watcher_service, state=result
)
if result == failed_s:
# Cancel ongoing action plans on the failed service using
# the existing startup sync method
syncer = sync.Syncer()
syncer._cancel_ongoing_actionplans(context,
watcher_service.host)
syncer._cancel_ongoing_actionplans(
context, watcher_service.host
)
# Pending action plans should be unassigned and
# re-triggered
self._retrigger_pending_actionplans(context,
watcher_service.host)
self._retrigger_pending_actionplans(
context, watcher_service.host
)
+35 -19
View File
@@ -39,7 +39,8 @@ class Syncer:
load_description = load_action.get_description()
try:
action_desc = objects.ActionDescription.get_by_type(
ctx, action_type)
ctx, action_type
)
if action_desc.description != load_description:
action_desc.description = load_description
action_desc.save()
@@ -54,28 +55,43 @@ class Syncer:
hostname = host or CONF.host
actions_plans = objects.ActionPlan.list(
context,
filters={'state': objects.action_plan.State.ONGOING,
'hostname': hostname},
eager=True)
filters={
'state': objects.action_plan.State.ONGOING,
'hostname': hostname,
},
eager=True,
)
for ap in actions_plans:
ap.state = objects.action_plan.State.CANCELLED
ap.status_message = ("Action plan was cancelled because Applier "
f"{hostname} was stopped while the action "
"plan was ongoing.")
ap.status_message = (
"Action plan was cancelled because Applier "
f"{hostname} was stopped while the action "
"plan was ongoing."
)
ap.save()
filters = {'action_plan_uuid': ap.uuid,
'state__in': (objects.action.State.PENDING,
objects.action.State.ONGOING)}
filters = {
'action_plan_uuid': ap.uuid,
'state__in': (
objects.action.State.PENDING,
objects.action.State.ONGOING,
),
}
actions = objects.Action.list(context, filters=filters, eager=True)
for a in actions:
a.state = objects.action.State.CANCELLED
a.status_message = ("Action was cancelled because Applier "
f"{hostname} was stopped while the "
"action plan was ongoing.")
a.status_message = (
"Action was cancelled because Applier "
f"{hostname} was stopped while the "
"action plan was ongoing."
)
a.save()
LOG.info("Action Plan %(uuid)s along with appropriate Actions "
"has been cancelled because it was in %(state)s state "
"when Applier had been stopped on %(hostname)s host.",
{'uuid': ap.uuid,
'state': objects.action_plan.State.ONGOING,
'hostname': ap.hostname})
LOG.info(
"Action Plan %(uuid)s along with appropriate Actions "
"has been cancelled because it was in %(state)s state "
"when Applier had been stopped on %(hostname)s host.",
{
'uuid': ap.uuid,
'state': objects.action_plan.State.ONGOING,
'hostname': ap.hostname,
},
)
+92 -56
View File
@@ -34,7 +34,6 @@ LOG = log.getLogger(__name__)
class BaseWorkFlowEngine(loadable.Loadable, metaclass=abc.ABCMeta):
def __init__(self, config, context=None, applier_manager=None):
"""Constructor
@@ -80,8 +79,9 @@ class BaseWorkFlowEngine(loadable.Loadable, metaclass=abc.ABCMeta):
return self._action_factory
def notify(self, action, state, status_message=None):
db_action = objects.Action.get_by_uuid(self.context, action.uuid,
eager=True)
db_action = objects.Action.get_by_uuid(
self.context, action.uuid, eager=True
)
db_action.state = state
if status_message:
db_action.status_message = status_message
@@ -89,15 +89,17 @@ class BaseWorkFlowEngine(loadable.Loadable, metaclass=abc.ABCMeta):
return db_action
def notify_cancel_start(self, action_plan_uuid):
action_plan = objects.ActionPlan.get_by_uuid(self.context,
action_plan_uuid,
eager=True)
action_plan = objects.ActionPlan.get_by_uuid(
self.context, action_plan_uuid, eager=True
)
if not self._is_notified:
self._is_notified = True
notifications.action_plan.send_cancel_notification(
self._context, action_plan,
self._context,
action_plan,
action=fields.NotificationAction.CANCEL,
phase=fields.NotificationPhase.START)
phase=fields.NotificationPhase.START,
)
@abc.abstractmethod
def execute(self, actions):
@@ -105,7 +107,6 @@ class BaseWorkFlowEngine(loadable.Loadable, metaclass=abc.ABCMeta):
class BaseTaskFlowActionContainer(flow_task.Task):
def __init__(self, name, db_action, engine, **kwargs):
super().__init__(name=name)
self._db_action = db_action
@@ -120,8 +121,8 @@ class BaseTaskFlowActionContainer(flow_task.Task):
def action(self):
if self.loaded_action is None:
action = self.engine.action_factory.make_action(
self._db_action,
osc=self._engine.osc)
self._db_action, osc=self._engine.osc
)
self.loaded_action = action
return self.loaded_action
@@ -148,20 +149,25 @@ class BaseTaskFlowActionContainer(flow_task.Task):
def _fail_action(self, phase, reason=None):
# Fail action and send notification to the user.
# If a reason is given, it will be used to set the status_message.
LOG.error('The workflow engine has failed '
'to execute the action: %s', self._db_action.uuid)
LOG.error(
'The workflow engine has failed to execute the action: %s',
self._db_action.uuid,
)
kwargs = {}
if reason:
kwargs["status_message"] = (_(
"Action failed in %s: %s") % (phase, reason))
db_action = self.engine.notify(self._db_action,
objects.action.State.FAILED,
**kwargs)
kwargs["status_message"] = _(
"Action failed in %(phase)s: %(reason)s"
) % {'phase': phase, 'reason': reason}
db_action = self.engine.notify(
self._db_action, objects.action.State.FAILED, **kwargs
)
notifications.action.send_execution_notification(
self.engine.context, db_action,
self.engine.context,
db_action,
fields.NotificationAction.EXECUTION,
fields.NotificationPhase.ERROR,
priority=fields.NotificationPriority.ERROR)
priority=fields.NotificationPriority.ERROR,
)
# NOTE(alexchadin): taskflow does 3 method calls (pre_execute, execute,
# post_execute) independently. We want to support notifications in base
@@ -172,33 +178,43 @@ class BaseTaskFlowActionContainer(flow_task.Task):
# next action, if action plan is cancelled raise the exceptions
# so that taskflow does not schedule further actions.
action_plan = objects.ActionPlan.get_by_id(
self.engine.context, self._db_action.action_plan_id)
self.engine.context, self._db_action.action_plan_id
)
if action_plan.state in objects.action_plan.State.CANCEL_STATES:
raise exception.ActionPlanCancelled(uuid=action_plan.uuid)
if self._db_action.state == objects.action.State.SKIPPED:
LOG.debug("Action %s is skipped manually",
self._db_action.uuid)
LOG.debug(
"Action %s is skipped manually", self._db_action.uuid
)
return
db_action = self.do_pre_execute()
notifications.action.send_execution_notification(
self.engine.context, db_action,
self.engine.context,
db_action,
fields.NotificationAction.EXECUTION,
fields.NotificationPhase.START)
fields.NotificationPhase.START,
)
except exception.ActionPlanCancelled as e:
LOG.exception(e)
self.engine.notify_cancel_start(action_plan.uuid)
raise
except exception.ActionSkipped as e:
LOG.info("Action %s was skipped automatically: %s",
self._db_action.uuid, str(e))
status_message = (_(
"Action was skipped automatically: %s") % str(e))
db_action = self.engine.notify(self._db_action,
objects.action.State.SKIPPED,
status_message=status_message)
LOG.info(
"Action %s was skipped automatically: %s",
self._db_action.uuid,
str(e),
)
status_message = _("Action was skipped automatically: %s") % str(e)
db_action = self.engine.notify(
self._db_action,
objects.action.State.SKIPPED,
status_message=status_message,
)
notifications.action.send_update(
self.engine.context, db_action,
old_state=objects.action.State.PENDING)
self.engine.context,
db_action,
old_state=objects.action.State.PENDING,
)
except exception.WatcherException as e:
LOG.exception(e)
self._fail_action("pre_condition", reason=str(e))
@@ -208,19 +224,25 @@ class BaseTaskFlowActionContainer(flow_task.Task):
def execute(self, *args, **kwargs):
action_object = objects.Action.get_by_uuid(
self.engine.context, self._db_action.uuid, eager=True)
if action_object.state in [objects.action.State.SKIPPED,
objects.action.State.FAILED]:
self.engine.context, self._db_action.uuid, eager=True
)
if action_object.state in [
objects.action.State.SKIPPED,
objects.action.State.FAILED,
]:
return True
try:
db_action = self.do_execute(*args, **kwargs)
notifications.action.send_execution_notification(
self.engine.context, db_action,
self.engine.context,
db_action,
fields.NotificationAction.EXECUTION,
fields.NotificationPhase.END)
fields.NotificationPhase.END,
)
action_object = objects.Action.get_by_uuid(
self.engine.context, self._db_action.uuid, eager=True)
self.engine.context, self._db_action.uuid, eager=True
)
if action_object.state == objects.action.State.SUCCEEDED:
return True
else:
@@ -236,7 +258,8 @@ class BaseTaskFlowActionContainer(flow_task.Task):
def post_execute(self):
action_object = objects.Action.get_by_uuid(
self.engine.context, self._db_action.uuid, eager=True)
self.engine.context, self._db_action.uuid, eager=True
)
if action_object.state == objects.action.State.SKIPPED:
return
try:
@@ -264,18 +287,21 @@ class BaseTaskFlowActionContainer(flow_task.Task):
def revert(self, *args, **kwargs):
action_plan = objects.ActionPlan.get_by_id(
self.engine.context, self._db_action.action_plan_id, eager=True)
self.engine.context, self._db_action.action_plan_id, eager=True
)
action_object = objects.Action.get_by_uuid(
self.engine.context, self._db_action.uuid, eager=True)
self.engine.context, self._db_action.uuid, eager=True
)
# NOTE: check if revert cause by cancel action plan or
# some other exception occurred during action plan execution
# if due to some other exception keep the flow intact.
# NOTE(dviroel): If the action was skipped, we should not
# revert it.
if (action_plan.state not in
objects.action_plan.State.CANCEL_STATES and
action_object.state != objects.action.State.SKIPPED):
if (
action_plan.state not in objects.action_plan.State.CANCEL_STATES
and action_object.state != objects.action.State.SKIPPED
):
self.do_revert()
return
@@ -284,37 +310,47 @@ class BaseTaskFlowActionContainer(flow_task.Task):
action_object.state = objects.action.State.CANCELLING
action_object.save()
notifications.action.send_cancel_notification(
self.engine.context, action_object,
self.engine.context,
action_object,
fields.NotificationAction.CANCEL,
fields.NotificationPhase.START)
fields.NotificationPhase.START,
)
action_object = self.abort()
notifications.action.send_cancel_notification(
self.engine.context, action_object,
self.engine.context,
action_object,
fields.NotificationAction.CANCEL,
fields.NotificationPhase.END)
fields.NotificationPhase.END,
)
if action_object.state == objects.action.State.PENDING:
notifications.action.send_cancel_notification(
self.engine.context, action_object,
self.engine.context,
action_object,
fields.NotificationAction.CANCEL,
fields.NotificationPhase.START)
fields.NotificationPhase.START,
)
action_object.state = objects.action.State.CANCELLED
action_object.save()
notifications.action.send_cancel_notification(
self.engine.context, action_object,
self.engine.context,
action_object,
fields.NotificationAction.CANCEL,
fields.NotificationPhase.END)
fields.NotificationPhase.END,
)
except Exception as e:
LOG.exception(e)
action_object.state = objects.action.State.FAILED
action_object.save()
notifications.action.send_cancel_notification(
self.engine.context, action_object,
self.engine.context,
action_object,
fields.NotificationAction.CANCEL,
fields.NotificationPhase.ERROR,
priority=fields.NotificationPriority.ERROR)
priority=fields.NotificationPriority.ERROR,
)
def abort(self, *args, **kwargs):
# NOTE(dviroel): only ONGOING actions are called
+57 -39
View File
@@ -65,26 +65,29 @@ class DefaultWorkFlowEngine(base.BaseWorkFlowEngine):
min=1,
required=True,
help='Number of workers for taskflow engine '
'to execute actions.'),
'to execute actions.',
),
cfg.DictOpt(
'action_execution_rule',
default={},
help='The execution rule for linked actions,'
'the key is strategy name and '
'value ALWAYS means all actions will be executed,'
'value ANY means if previous action executes '
'success, the next action will be ignored.'
'None means ALWAYS.')
]
'the key is strategy name and '
'value ALWAYS means all actions will be executed,'
'value ANY means if previous action executes '
'success, the next action will be ignored.'
'None means ALWAYS.',
),
]
def get_execution_rule(self, actions):
if actions:
actionplan_object = objects.ActionPlan.get_by_id(
self.context, actions[0].action_plan_id)
self.context, actions[0].action_plan_id
)
strategy_object = objects.Strategy.get_by_id(
self.context, actionplan_object.strategy_id)
return self.config.action_execution_rule.get(
strategy_object.name)
self.context, actionplan_object.strategy_id
)
return self.config.action_execution_rule.get(strategy_object.name)
def execute(self, actions):
try:
@@ -106,20 +109,28 @@ class DefaultWorkFlowEngine(base.BaseWorkFlowEngine):
for a in actions:
for parent_id in a.parents:
flow.link(actions_uuid[parent_id], actions_uuid[a.uuid],
decider=self.decider)
flow.link(
actions_uuid[parent_id],
actions_uuid[a.uuid],
decider=self.decider,
)
e = None
engine_type = "parallel"
if eventlet_helper.is_patched():
executor_type = "greenthreaded"
else:
LOG.info("Using Taskflow parallel engine when running "
"in native threading mode.")
LOG.info(
"Using Taskflow parallel engine when running "
"in native threading mode."
)
executor_type = "threaded"
e = engines.load(
flow, executor=executor_type, engine=engine_type,
max_workers=self.config.max_workers)
flow,
executor=executor_type,
engine=engine_type,