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:
+6
-10
@@ -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:
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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}
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"""
|
||||
|
||||
|
||||
@@ -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",)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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."
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -26,7 +26,6 @@ CONF = conf.CONF
|
||||
|
||||
|
||||
class ApplierManager(service_manager.ServiceManager):
|
||||
|
||||
@property
|
||||
def service_name(self):
|
||||
return 'watcher-applier'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||