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:
@@ -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',
|
||||
)
|
||||
]
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
4
setup.py
4
setup.py
@@ -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)
|
||||
|
||||
3
tox.ini
3
tox.ini
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]))))
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
max_workers=self.config.max_workers,
|
||||
)
|
||||
|
||||
e.run()
|
||||
|
||||
@@ -131,23 +142,23 @@ class DefaultWorkFlowEngine(base.BaseWorkFlowEngine):
|
||||
except tf_exception.WrappedFailure as e:
|
||||
if e.check("watcher.common.exception.ActionPlanCancelled"):
|
||||
raise exception.ActionPlanCancelled(
|
||||
uuid=actions[0].action_plan_id)
|
||||
uuid=actions[0].action_plan_id
|
||||
)
|
||||
else:
|
||||
raise exception.WorkflowExecutionException(
|
||||
error=type(e).__name__)
|
||||
error=type(e).__name__
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
raise exception.WorkflowExecutionException(
|
||||
error=type(e).__name__)
|
||||
raise exception.WorkflowExecutionException(error=type(e).__name__)
|
||||
|
||||
|
||||
class TaskFlowActionContainer(base.BaseTaskFlowActionContainer):
|
||||
def __init__(self, db_action, engine):
|
||||
self.name = (f"action_type:{db_action.action_type} "
|
||||
f"uuid:{db_action.uuid}")
|
||||
super().__init__(self.name,
|
||||
db_action,
|
||||
engine)
|
||||
self.name = (
|
||||
f"action_type:{db_action.action_type} uuid:{db_action.uuid}"
|
||||
)
|
||||
super().__init__(self.name, db_action, engine)
|
||||
|
||||
def do_pre_execute(self):
|
||||
LOG.debug("Pre-condition action: %s", self.name)
|
||||
@@ -162,13 +173,14 @@ class TaskFlowActionContainer(base.BaseTaskFlowActionContainer):
|
||||
# Only when True is returned, the action state is set to SUCCEEDED
|
||||
result = self.action.execute()
|
||||
if result is True:
|
||||
return self.engine.notify(self._db_action,
|
||||
objects.action.State.SUCCEEDED)
|
||||
return self.engine.notify(
|
||||
self._db_action, objects.action.State.SUCCEEDED
|
||||
)
|
||||
else:
|
||||
self.engine.notify(self._db_action,
|
||||
objects.action.State.FAILED)
|
||||
self.engine.notify(self._db_action, objects.action.State.FAILED)
|
||||
raise exception.ActionExecutionFailure(
|
||||
action_id=self._db_action.uuid)
|
||||
action_id=self._db_action.uuid
|
||||
)
|
||||
|
||||
def do_post_execute(self):
|
||||
LOG.debug("Post-condition action: %s", self.name)
|
||||
@@ -177,8 +189,11 @@ class TaskFlowActionContainer(base.BaseTaskFlowActionContainer):
|
||||
def do_revert(self, *args, **kwargs):
|
||||
# NOTE: Not rollback action plan
|
||||
if not CONF.watcher_applier.rollback_when_actionplan_failed:
|
||||
LOG.info("Failed actionplan rollback option is turned off, and "
|
||||
"the following action will be skipped: %s", self.name)
|
||||
LOG.info(
|
||||
"Failed actionplan rollback option is turned off, and "
|
||||
"the following action will be skipped: %s",
|
||||
self.name,
|
||||
)
|
||||
return
|
||||
|
||||
LOG.warning("Revert action: %s", self.name)
|
||||
@@ -195,15 +210,18 @@ class TaskFlowActionContainer(base.BaseTaskFlowActionContainer):
|
||||
result = self.action.abort()
|
||||
if result:
|
||||
# Aborted the action.
|
||||
return self.engine.notify(self._db_action,
|
||||
objects.action.State.CANCELLED)
|
||||
return self.engine.notify(
|
||||
self._db_action, objects.action.State.CANCELLED
|
||||
)
|
||||
else:
|
||||
return self.engine.notify(self._db_action,
|
||||
objects.action.State.SUCCEEDED)
|
||||
return self.engine.notify(
|
||||
self._db_action, objects.action.State.SUCCEEDED
|
||||
)
|
||||
except Exception as e:
|
||||
LOG.exception(e)
|
||||
return self.engine.notify(self._db_action,
|
||||
objects.action.State.FAILED)
|
||||
return self.engine.notify(
|
||||
self._db_action, objects.action.State.FAILED
|
||||
)
|
||||
|
||||
|
||||
class TaskFlowNop(flow_task.Task):
|
||||
|
||||
@@ -32,7 +32,6 @@ CONF = conf.CONF
|
||||
|
||||
|
||||
class DBCommand:
|
||||
|
||||
@staticmethod
|
||||
def upgrade():
|
||||
migration.upgrade(CONF.command.revision)
|
||||
@@ -59,25 +58,31 @@ class DBCommand:
|
||||
|
||||
@staticmethod
|
||||
def purge():
|
||||
purge.purge(CONF.command.age_in_days, CONF.command.max_number,
|
||||
CONF.command.goal, CONF.command.exclude_orphans,
|
||||
CONF.command.dry_run)
|
||||
purge.purge(
|
||||
CONF.command.age_in_days,
|
||||
CONF.command.max_number,
|
||||
CONF.command.goal,
|
||||
CONF.command.exclude_orphans,
|
||||
CONF.command.dry_run,
|
||||
)
|
||||
|
||||
|
||||
def add_command_parsers(subparsers):
|
||||
parser = subparsers.add_parser(
|
||||
'upgrade',
|
||||
help="Upgrade the database schema to the latest version. "
|
||||
"Optionally, use --revision to specify an alembic revision "
|
||||
"string to upgrade to.")
|
||||
"Optionally, use --revision to specify an alembic revision "
|
||||
"string to upgrade to.",
|
||||
)
|
||||
parser.set_defaults(func=DBCommand.upgrade)
|
||||
parser.add_argument('--revision', nargs='?')
|
||||
|
||||
parser = subparsers.add_parser(
|
||||
'downgrade',
|
||||
help="Downgrade the database schema to the oldest revision. "
|
||||
"While optional, one should generally use --revision to "
|
||||
"specify the alembic revision string to downgrade to.")
|
||||
"While optional, one should generally use --revision to "
|
||||
"specify the alembic revision string to downgrade to.",
|
||||
)
|
||||
parser.set_defaults(func=DBCommand.downgrade)
|
||||
parser.add_argument('--revision', nargs='?')
|
||||
|
||||
@@ -88,53 +93,76 @@ def add_command_parsers(subparsers):
|
||||
parser = subparsers.add_parser(
|
||||
'revision',
|
||||
help="Create a new alembic revision. "
|
||||
"Use --message to set the message string.")
|
||||
"Use --message to set the message string.",
|
||||
)
|
||||
parser.add_argument('-m', '--message')
|
||||
parser.add_argument('--autogenerate', action='store_true')
|
||||
parser.set_defaults(func=DBCommand.revision)
|
||||
|
||||
parser = subparsers.add_parser(
|
||||
'version',
|
||||
help="Print the current version information and exit.")
|
||||
'version', help="Print the current version information and exit."
|
||||
)
|
||||
parser.set_defaults(func=DBCommand.version)
|
||||
|
||||
parser = subparsers.add_parser(
|
||||
'create_schema',
|
||||
help="Create the database schema.")
|
||||
'create_schema', help="Create the database schema."
|
||||
)
|
||||
parser.set_defaults(func=DBCommand.create_schema)
|
||||
|
||||
parser = subparsers.add_parser(
|
||||
'purge',
|
||||
help="Purge the database.")
|
||||
parser.add_argument('-d', '--age-in-days',
|
||||
help="Number of days since deletion (from today) "
|
||||
"to exclude from the purge. If None, everything "
|
||||
"will be purged.",
|
||||
type=int, default=None, nargs='?')
|
||||
parser.add_argument('-n', '--max-number',
|
||||
help="Max number of objects expected to be deleted. "
|
||||
"Prevents the deletion if exceeded. No limit if "
|
||||
"set to None.",
|
||||
type=int, default=None, nargs='?')
|
||||
parser.add_argument('-t', '--goal',
|
||||
help="UUID or name of the goal to purge.",
|
||||
type=str, default=None, nargs='?')
|
||||
parser.add_argument('-e', '--exclude-orphans', action='store_true',
|
||||
help="Flag to indicate whether or not you want to "
|
||||
"exclude orphans from deletion (default: False).",
|
||||
default=False)
|
||||
parser.add_argument('--dry-run', action='store_true',
|
||||
help="Flag to indicate whether or not you want to "
|
||||
"perform a dry run (no deletion).",
|
||||
default=False)
|
||||
parser = subparsers.add_parser('purge', help="Purge the database.")
|
||||
parser.add_argument(
|
||||
'-d',
|
||||
'--age-in-days',
|
||||
help="Number of days since deletion (from today) "
|
||||
"to exclude from the purge. If None, everything "
|
||||
"will be purged.",
|
||||
type=int,
|
||||
default=None,
|
||||
nargs='?',
|
||||
)
|
||||
parser.add_argument(
|
||||
'-n',
|
||||
'--max-number',
|
||||
help="Max number of objects expected to be deleted. "
|
||||
"Prevents the deletion if exceeded. No limit if "
|
||||
"set to None.",
|
||||
type=int,
|
||||
default=None,
|
||||
nargs='?',
|
||||
)
|
||||
parser.add_argument(
|
||||
'-t',
|
||||
'--goal',
|
||||
help="UUID or name of the goal to purge.",
|
||||
type=str,
|
||||
default=None,
|
||||
nargs='?',
|
||||
)
|
||||
parser.add_argument(
|
||||
'-e',
|
||||
'--exclude-orphans',
|
||||
action='store_true',
|
||||
help="Flag to indicate whether or not you want to "
|
||||
"exclude orphans from deletion (default: False).",
|
||||
default=False,
|
||||
)
|
||||
parser.add_argument(
|
||||
'--dry-run',
|
||||
action='store_true',
|
||||
help="Flag to indicate whether or not you want to "
|
||||
"perform a dry run (no deletion).",
|
||||
default=False,
|
||||
)
|
||||
|
||||
parser.set_defaults(func=DBCommand.purge)
|
||||
|
||||
|
||||
command_opt = cfg.SubCommandOpt('command',
|
||||
title='Command',
|
||||
help='Available commands',
|
||||
handler=add_command_parsers)
|
||||
command_opt = cfg.SubCommandOpt(
|
||||
'command',
|
||||
title='Command',
|
||||
help='Available commands',
|
||||
handler=add_command_parsers,
|
||||
)
|
||||
|
||||
|
||||
def register_sub_command_opts():
|
||||
@@ -146,8 +174,12 @@ def main():
|
||||
# this is hack to work with previous usage of watcher-dbsync
|
||||
# pls change it to watcher-dbsync upgrade
|
||||
valid_commands = {
|
||||
'upgrade', 'downgrade', 'revision',
|
||||
'version', 'stamp', 'create_schema',
|
||||
'upgrade',
|
||||
'downgrade',
|
||||
'revision',
|
||||
'version',
|
||||
'stamp',
|
||||
'create_schema',
|
||||
'purge',
|
||||
}
|
||||
if not set(sys.argv).intersection(valid_commands):
|
||||
|
||||
@@ -38,12 +38,16 @@ def main():
|
||||
server = service.WSGIService('watcher-api', CONF.api.enable_ssl_api)
|
||||
|
||||
if host == '127.0.0.1':
|
||||
LOG.info('serving on 127.0.0.1:%(port)s, '
|
||||
'view at %(protocol)s://127.0.0.1:%(port)s',
|
||||
dict(protocol=protocol, port=port))
|
||||
LOG.info(
|
||||
'serving on 127.0.0.1:%(port)s, '
|
||||
'view at %(protocol)s://127.0.0.1:%(port)s',
|
||||
dict(protocol=protocol, port=port),
|
||||
)
|
||||
else:
|
||||
LOG.info('serving on %(protocol)s://%(host)s:%(port)s',
|
||||
dict(protocol=protocol, host=host, port=port))
|
||||
LOG.info(
|
||||
'serving on %(protocol)s://%(host)s:%(port)s',
|
||||
dict(protocol=protocol, host=host, port=port),
|
||||
)
|
||||
|
||||
launcher = service.launch(CONF, server, workers=server.workers)
|
||||
launcher.wait()
|
||||
|
||||
@@ -36,8 +36,7 @@ def main():
|
||||
watcher_service.prepare_service(sys.argv, CONF)
|
||||
gmr.register_gmr_plugins()
|
||||
|
||||
LOG.info('Starting Watcher Decision Engine service in PID %s',
|
||||
os.getpid())
|
||||
LOG.info('Starting Watcher Decision Engine service in PID %s', os.getpid())
|
||||
|
||||
syncer = sync.Syncer()
|
||||
syncer.sync()
|
||||
|
||||
@@ -26,7 +26,6 @@ CONF = conf.CONF
|
||||
|
||||
|
||||
class Checks(upgradecheck.UpgradeCommands):
|
||||
|
||||
"""Contains upgrade checks
|
||||
|
||||
Various upgrade checks should be added as separate methods in this class
|
||||
@@ -38,23 +37,22 @@ class Checks(upgradecheck.UpgradeCommands):
|
||||
try:
|
||||
clients.check_min_nova_api_version(CONF.nova.api_version)
|
||||
except ValueError as e:
|
||||
return upgradecheck.Result(
|
||||
upgradecheck.Code.FAILURE, str(e))
|
||||
return upgradecheck.Result(upgradecheck.Code.FAILURE, str(e))
|
||||
return upgradecheck.Result(upgradecheck.Code.SUCCESS)
|
||||
|
||||
_upgrade_checks = (
|
||||
# Added in Train.
|
||||
(_('Minimum Nova API Version'), _minimum_nova_api_version),
|
||||
# Added in Wallaby.
|
||||
(_("Policy File JSON to YAML Migration"),
|
||||
(common_checks.check_policy_json, {'conf': CONF})),
|
||||
|
||||
(
|
||||
_("Policy File JSON to YAML Migration"),
|
||||
(common_checks.check_policy_json, {'conf': CONF}),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
return upgradecheck.main(
|
||||
CONF, project='watcher', upgrade_command=Checks())
|
||||
return upgradecheck.main(CONF, project='watcher', upgrade_command=Checks())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -29,7 +29,6 @@ LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class CinderHelper:
|
||||
|
||||
def __init__(self, osc=None):
|
||||
""":param osc: an OpenStackClients instance"""
|
||||
self.osc = osc if osc else clients.OpenStackClients()
|
||||
@@ -41,8 +40,11 @@ class CinderHelper:
|
||||
def get_storage_node_by_name(self, name):
|
||||
"""Get storage node by name(host@backendname)"""
|
||||
try:
|
||||
storages = [storage for storage in self.get_storage_node_list()
|
||||
if storage.host == name]
|
||||
storages = [
|
||||
storage
|
||||
for storage in self.get_storage_node_list()
|
||||
if storage.host == name
|
||||
]
|
||||
if len(storages) != 1:
|
||||
raise exception.StorageNodeNotFound(name=name)
|
||||
return storages[0]
|
||||
@@ -56,8 +58,11 @@ class CinderHelper:
|
||||
def get_storage_pool_by_name(self, name):
|
||||
"""Get pool by name(host@backend#poolname)"""
|
||||
try:
|
||||
pools = [pool for pool in self.get_storage_pool_list()
|
||||
if pool.name == name]
|
||||
pools = [
|
||||
pool
|
||||
for pool in self.get_storage_pool_list()
|
||||
if pool.name == name
|
||||
]
|
||||
if len(pools) != 1:
|
||||
raise exception.PoolNotFound(name=name)
|
||||
return pools[0]
|
||||
@@ -73,19 +78,22 @@ class CinderHelper:
|
||||
|
||||
def get_volume_snapshots_list(self):
|
||||
return self.cinder.volume_snapshots.list(
|
||||
search_opts={'all_tenants': True})
|
||||
search_opts={'all_tenants': True}
|
||||
)
|
||||
|
||||
def get_volume_type_by_backendname(self, backendname):
|
||||
"""Return a list of volume type"""
|
||||
volume_type_list = self.get_volume_type_list()
|
||||
|
||||
volume_type = [volume_type.name for volume_type in volume_type_list
|
||||
if volume_type.extra_specs.get(
|
||||
'volume_backend_name') == backendname]
|
||||
volume_type = [
|
||||
volume_type.name
|
||||
for volume_type in volume_type_list
|
||||
if volume_type.extra_specs.get('volume_backend_name')
|
||||
== backendname
|
||||
]
|
||||
return volume_type
|
||||
|
||||
def get_volume(self, volume):
|
||||
|
||||
if isinstance(volume, Volume):
|
||||
volume = volume.id
|
||||
|
||||
@@ -141,7 +149,10 @@ class CinderHelper:
|
||||
LOG.debug(
|
||||
"property %s with value %s does not match value "
|
||||
"%s from pool %s",
|
||||
field_name, field_value, pool_value, pool['name']
|
||||
field_name,
|
||||
field_value,
|
||||
pool_value,
|
||||
pool['name'],
|
||||
)
|
||||
return False
|
||||
return True
|
||||
@@ -191,9 +202,11 @@ class CinderHelper:
|
||||
time.sleep(retry_interval)
|
||||
if getattr(volume, 'migration_status') == 'error':
|
||||
host_name = getattr(volume, 'os-vol-host-attr:host')
|
||||
error_msg = ("Volume migration error : "
|
||||
f"volume {volume.id} is now on host "
|
||||
f"'{host_name}'.")
|
||||
error_msg = (
|
||||
"Volume migration error : "
|
||||
f"volume {volume.id} is now on host "
|
||||
f"'{host_name}'."
|
||||
)
|
||||
LOG.error(error_msg)
|
||||
return False
|
||||
|
||||
@@ -207,14 +220,17 @@ class CinderHelper:
|
||||
return False
|
||||
else:
|
||||
host_name = getattr(volume, 'os-vol-host-attr:host')
|
||||
error_msg = ("Volume migration error : "
|
||||
f"volume {volume.id} is now on host '{host_name}'.")
|
||||
error_msg = (
|
||||
"Volume migration error : "
|
||||
f"volume {volume.id} is now on host '{host_name}'."
|
||||
)
|
||||
LOG.error(error_msg)
|
||||
return False
|
||||
LOG.debug(
|
||||
"Volume migration succeeded : "
|
||||
"volume %(volume)s is now on host '%(host)s'.",
|
||||
{'volume': volume.id, 'host': host_name})
|
||||
{'volume': volume.id, 'host': host_name},
|
||||
)
|
||||
return True
|
||||
|
||||
def check_retyped(self, volume, dst_type, retry_interval=10):
|
||||
@@ -223,8 +239,9 @@ class CinderHelper:
|
||||
# A volume retype is correct when the type is the dst_type
|
||||
# and the status is available or in-use. Otherwise, it is
|
||||
# in retyping status or the action failed
|
||||
while (volume.volume_type != dst_type or
|
||||
volume.status not in valid_status):
|
||||
while (
|
||||
volume.volume_type != dst_type or volume.status not in valid_status
|
||||
):
|
||||
# Retype is not finished successfully, checking if the
|
||||
# retype is still ongoing or failed. If status is not
|
||||
# `retyping` it means something went wrong.
|
||||
@@ -233,14 +250,20 @@ class CinderHelper:
|
||||
"Volume retype failed : "
|
||||
"volume %(volume)s has now type '%(type)s' and "
|
||||
"status %(status)s",
|
||||
{'volume': volume.id, 'type': volume.volume_type,
|
||||
'status': volume.status})
|
||||
{
|
||||
'volume': volume.id,
|
||||
'type': volume.volume_type,
|
||||
'status': volume.status,
|
||||
},
|
||||
)
|
||||
# If migration_status is in error, a likely reason why the
|
||||
# retype failed is some problem in the migration. Report it in
|
||||
# the logs if migration_status is error.
|
||||
if volume.migration_status == 'error':
|
||||
LOG.error("Volume migration error on volume %(volume)s.",
|
||||
{'volume': volume.id})
|
||||
LOG.error(
|
||||
"Volume migration error on volume %(volume)s.",
|
||||
{'volume': volume.id},
|
||||
)
|
||||
return False
|
||||
|
||||
LOG.debug('Waiting the retype of %s', volume)
|
||||
@@ -250,7 +273,8 @@ class CinderHelper:
|
||||
LOG.debug(
|
||||
"Volume retype succeeded : "
|
||||
"volume %(volume)s has now type '%(type)s'.",
|
||||
{'volume': volume.id, 'type': dst_type})
|
||||
{'volume': volume.id, 'type': dst_type},
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
@@ -265,19 +289,21 @@ class CinderHelper:
|
||||
_(
|
||||
"Volume type '%(volume_type)s' is not compatible with "
|
||||
"destination pool '%(pool_name)s'"
|
||||
) % {
|
||||
)
|
||||
% {
|
||||
'volume_type': volume.volume_type,
|
||||
'pool_name': dest_node
|
||||
}
|
||||
'pool_name': dest_node,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
source_node = getattr(volume, 'os-vol-host-attr:host')
|
||||
LOG.debug("Volume %(volume)s found on host '%(host)s'.",
|
||||
{'volume': volume.id, 'host': source_node})
|
||||
LOG.debug(
|
||||
"Volume %(volume)s found on host '%(host)s'.",
|
||||
{'volume': volume.id, 'host': source_node},
|
||||
)
|
||||
|
||||
self.cinder.volumes.migrate_volume(
|
||||
volume, dest_node, False, True)
|
||||
self.cinder.volumes.migrate_volume(volume, dest_node, False, True)
|
||||
|
||||
return self.check_migrated(volume)
|
||||
|
||||
@@ -286,20 +312,22 @@ class CinderHelper:
|
||||
volume = self.get_volume(volume)
|
||||
if volume.volume_type == dest_type:
|
||||
raise exception.Invalid(
|
||||
message=(_("Volume type must be different for retyping")))
|
||||
message=(_("Volume type must be different for retyping"))
|
||||
)
|
||||
|
||||
source_node = getattr(volume, 'os-vol-host-attr:host')
|
||||
LOG.debug(
|
||||
"Volume %(volume)s found on host '%(host)s'.",
|
||||
{'volume': volume.id, 'host': source_node})
|
||||
{'volume': volume.id, 'host': source_node},
|
||||
)
|
||||
|
||||
self.cinder.volumes.retype(
|
||||
volume, dest_type, "on-demand")
|
||||
self.cinder.volumes.retype(volume, dest_type, "on-demand")
|
||||
|
||||
return self.check_retyped(volume, dest_type)
|
||||
|
||||
def create_volume(self, cinder, volume,
|
||||
dest_type, retry=120, retry_interval=10):
|
||||
def create_volume(
|
||||
self, cinder, volume, dest_type, retry=120, retry_interval=10
|
||||
):
|
||||
"""Create volume of volume with dest_type using cinder"""
|
||||
volume = self.get_volume(volume)
|
||||
LOG.debug("start creating new volume")
|
||||
@@ -307,7 +335,8 @@ class CinderHelper:
|
||||
getattr(volume, 'size'),
|
||||
name=getattr(volume, 'name'),
|
||||
volume_type=dest_type,
|
||||
availability_zone=getattr(volume, 'availability_zone'))
|
||||
availability_zone=getattr(volume, 'availability_zone'),
|
||||
)
|
||||
while getattr(new_volume, 'status') != 'available' and retry:
|
||||
new_volume = cinder.volumes.get(new_volume.id)
|
||||
LOG.debug('Waiting volume creation of %s', new_volume)
|
||||
@@ -316,8 +345,9 @@ class CinderHelper:
|
||||
LOG.debug("retry count: %s", retry)
|
||||
|
||||
if getattr(new_volume, 'status') != 'available':
|
||||
error_msg = (_("Failed to create volume '%(volume)s. ") %
|
||||
{'volume': new_volume.id})
|
||||
error_msg = _("Failed to create volume '%(volume)s. ") % {
|
||||
'volume': new_volume.id
|
||||
}
|
||||
raise Exception(error_msg)
|
||||
|
||||
LOG.debug("Volume %s was created successfully.", new_volume)
|
||||
@@ -329,6 +359,7 @@ class CinderHelper:
|
||||
self.cinder.volumes.delete(volume)
|
||||
result = self.check_volume_deleted(volume)
|
||||
if not result:
|
||||
error_msg = (_("Failed to delete volume '%(volume)s. ") %
|
||||
{'volume': volume.id})
|
||||
error_msg = _("Failed to delete volume '%(volume)s. ") % {
|
||||
'volume': volume.id
|
||||
}
|
||||
raise Exception(error_msg)
|
||||
|
||||
@@ -50,10 +50,12 @@ warnings.simplefilter("once")
|
||||
|
||||
|
||||
def get_sdk_connection(
|
||||
conf_group: str, session: ka_session.Session | None = None,
|
||||
context: context.RequestContext | None = None,
|
||||
interface: str | None = None, region_name: str | None = None
|
||||
) -> connection.Connection:
|
||||
conf_group: str,
|
||||
session: ka_session.Session | None = None,
|
||||
context: context.RequestContext | None = None,
|
||||
interface: str | None = None,
|
||||
region_name: str | None = None,
|
||||
) -> connection.Connection:
|
||||
"""Create and return an OpenStackSDK Connection object.
|
||||
|
||||
:param conf_group: String name of the conf group to get connection
|
||||
@@ -70,9 +72,7 @@ def get_sdk_connection(
|
||||
# been loaded before. The auth plugin is only used when creating a new
|
||||
# session, but we need to ensure the auth_url config value is set to use
|
||||
# the user token from the context object
|
||||
auth = ka_loading.load_auth_from_conf_options(
|
||||
CONF, conf_group
|
||||
)
|
||||
auth = ka_loading.load_auth_from_conf_options(CONF, conf_group)
|
||||
if context is not None:
|
||||
if interface is None:
|
||||
if "valid_interfaces" in CONF[conf_group]:
|
||||
@@ -90,7 +90,7 @@ def get_sdk_connection(
|
||||
project_domain_id=context.project_domain_id,
|
||||
auth_url=CONF[conf_group].auth_url,
|
||||
region_name=region_name,
|
||||
interface=interface
|
||||
interface=interface,
|
||||
)
|
||||
return conn
|
||||
|
||||
@@ -116,8 +116,10 @@ def check_min_nova_api_version(config_version):
|
||||
)
|
||||
|
||||
if microversion_parse.parse_version_string(config_version) < min_required:
|
||||
raise ValueError(f'Invalid nova.api_version {config_version}. '
|
||||
f'{MIN_NOVA_API_VERSION} or greater is required.')
|
||||
raise ValueError(
|
||||
f'Invalid nova.api_version {config_version}. '
|
||||
f'{MIN_NOVA_API_VERSION} or greater is required.'
|
||||
)
|
||||
|
||||
|
||||
class OpenStackClients:
|
||||
@@ -136,11 +138,12 @@ class OpenStackClients:
|
||||
self._placement = None
|
||||
|
||||
def _get_keystone_session(self):
|
||||
auth = ka_loading.load_auth_from_conf_options(CONF,
|
||||
_CLIENTS_AUTH_GROUP)
|
||||
sess = ka_loading.load_session_from_conf_options(CONF,
|
||||
_CLIENTS_AUTH_GROUP,
|
||||
auth=auth)
|
||||
auth = ka_loading.load_auth_from_conf_options(
|
||||
CONF, _CLIENTS_AUTH_GROUP
|
||||
)
|
||||
sess = ka_loading.load_session_from_conf_options(
|
||||
CONF, _CLIENTS_AUTH_GROUP, auth=auth
|
||||
)
|
||||
return sess
|
||||
|
||||
@property
|
||||
@@ -160,14 +163,15 @@ class OpenStackClients:
|
||||
def keystone(self):
|
||||
if self._keystone:
|
||||
return self._keystone
|
||||
keystone_interface = self._get_client_option('keystone',
|
||||
'interface')
|
||||
keystone_region_name = self._get_client_option('keystone',
|
||||
'region_name')
|
||||
keystone_interface = self._get_client_option('keystone', 'interface')
|
||||
keystone_region_name = self._get_client_option(
|
||||
'keystone', 'region_name'
|
||||
)
|
||||
self._keystone = keyclient.Client(
|
||||
interface=keystone_interface,
|
||||
region_name=keystone_region_name,
|
||||
session=self.session)
|
||||
session=self.session,
|
||||
)
|
||||
|
||||
return self._keystone
|
||||
|
||||
@@ -176,20 +180,25 @@ class OpenStackClients:
|
||||
if self._gnocchi:
|
||||
return self._gnocchi
|
||||
|
||||
gnocchiclient_version = self._get_client_option('gnocchi',
|
||||
'api_version')
|
||||
gnocchiclient_interface = self._get_client_option('gnocchi',
|
||||
'endpoint_type')
|
||||
gnocchiclient_region_name = self._get_client_option('gnocchi',
|
||||
'region_name')
|
||||
gnocchiclient_version = self._get_client_option(
|
||||
'gnocchi', 'api_version'
|
||||
)
|
||||
gnocchiclient_interface = self._get_client_option(
|
||||
'gnocchi', 'endpoint_type'
|
||||
)
|
||||
gnocchiclient_region_name = self._get_client_option(
|
||||
'gnocchi', 'region_name'
|
||||
)
|
||||
adapter_options = {
|
||||
"interface": gnocchiclient_interface,
|
||||
"region_name": gnocchiclient_region_name
|
||||
"region_name": gnocchiclient_region_name,
|
||||
}
|
||||
|
||||
self._gnocchi = gnclient.Client(gnocchiclient_version,
|
||||
adapter_options=adapter_options,
|
||||
session=self.session)
|
||||
self._gnocchi = gnclient.Client(
|
||||
gnocchiclient_version,
|
||||
adapter_options=adapter_options,
|
||||
session=self.session,
|
||||
)
|
||||
return self._gnocchi
|
||||
|
||||
@exception.wrap_keystone_exception
|
||||
@@ -198,13 +207,16 @@ class OpenStackClients:
|
||||
return self._cinder
|
||||
|
||||
cinderclient_version = self._get_client_option('cinder', 'api_version')
|
||||
cinder_endpoint_type = self._get_client_option('cinder',
|
||||
'endpoint_type')
|
||||
cinder_endpoint_type = self._get_client_option(
|
||||
'cinder', 'endpoint_type'
|
||||
)
|
||||
cinder_region_name = self._get_client_option('cinder', 'region_name')
|
||||
self._cinder = ciclient.Client(cinderclient_version,
|
||||
endpoint_type=cinder_endpoint_type,
|
||||
region_name=cinder_region_name,
|
||||
session=self.session)
|
||||
self._cinder = ciclient.Client(
|
||||
cinderclient_version,
|
||||
endpoint_type=cinder_endpoint_type,
|
||||
region_name=cinder_region_name,
|
||||
session=self.session,
|
||||
)
|
||||
return self._cinder
|
||||
|
||||
@exception.wrap_keystone_exception
|
||||
@@ -216,17 +228,23 @@ class OpenStackClients:
|
||||
# the lack of documentation and CI testing. It can be marked as
|
||||
# supported or deprecated in future releases, based on improvements.
|
||||
debtcollector.deprecate(
|
||||
("Ironic is an experimental integration and may be "
|
||||
"deprecated in future releases."),
|
||||
version="2025.2", category=PendingDeprecationWarning)
|
||||
(
|
||||
"Ironic is an experimental integration and may be "
|
||||
"deprecated in future releases."
|
||||
),
|
||||
version="2025.2",
|
||||
category=PendingDeprecationWarning,
|
||||
)
|
||||
|
||||
ironicclient_version = self._get_client_option('ironic', 'api_version')
|
||||
endpoint_type = self._get_client_option('ironic', 'endpoint_type')
|
||||
ironic_region_name = self._get_client_option('ironic', 'region_name')
|
||||
self._ironic = irclient.get_client(ironicclient_version,
|
||||
interface=endpoint_type,
|
||||
region_name=ironic_region_name,
|
||||
session=self.session)
|
||||
self._ironic = irclient.get_client(
|
||||
ironicclient_version,
|
||||
interface=endpoint_type,
|
||||
region_name=ironic_region_name,
|
||||
session=self.session,
|
||||
)
|
||||
return self._ironic
|
||||
|
||||
def maas(self):
|
||||
@@ -237,20 +255,25 @@ class OpenStackClients:
|
||||
# maintenance and support. It has eventlet code that is required to be
|
||||
# removed/replaced in future releases.
|
||||
debtcollector.deprecate(
|
||||
("MAAS integration is deprecated and it will be removed in a "
|
||||
"future release."), version="2026.1", category=DeprecationWarning)
|
||||
(
|
||||
"MAAS integration is deprecated and it will be removed in a "
|
||||
"future release."
|
||||
),
|
||||
version="2026.1",
|
||||
category=DeprecationWarning,
|
||||
)
|
||||
|
||||
if not maas_client:
|
||||
raise exception.UnsupportedError(
|
||||
"MAAS client unavailable. Please install python-libmaas.")
|
||||
"MAAS client unavailable. Please install python-libmaas."
|
||||
)
|
||||
|
||||
url = self._get_client_option('maas', 'url')
|
||||
api_key = self._get_client_option('maas', 'api_key')
|
||||
timeout = self._get_client_option('maas', 'timeout')
|
||||
self._maas = utils.async_compat_call(
|
||||
maas_client.connect,
|
||||
url, apikey=api_key,
|
||||
timeout=timeout)
|
||||
maas_client.connect, url, apikey=api_key, timeout=timeout
|
||||
)
|
||||
return self._maas
|
||||
|
||||
@exception.wrap_keystone_exception
|
||||
@@ -258,12 +281,11 @@ class OpenStackClients:
|
||||
if self._placement:
|
||||
return self._placement
|
||||
|
||||
placement_version = self._get_client_option('placement',
|
||||
'api_version')
|
||||
placement_interface = self._get_client_option('placement',
|
||||
'interface')
|
||||
placement_region_name = self._get_client_option('placement',
|
||||
'region_name')
|
||||
placement_version = self._get_client_option('placement', 'api_version')
|
||||
placement_interface = self._get_client_option('placement', 'interface')
|
||||
placement_region_name = self._get_client_option(
|
||||
'placement', 'region_name'
|
||||
)
|
||||
# Set accept header on every request to ensure we notify placement
|
||||
# service of our response body media type preferences.
|
||||
headers = {'accept': 'application/json'}
|
||||
@@ -273,6 +295,7 @@ class OpenStackClients:
|
||||
default_microversion=placement_version,
|
||||
interface=placement_interface,
|
||||
region_name=placement_region_name,
|
||||
additional_headers=headers)
|
||||
additional_headers=headers,
|
||||
)
|
||||
|
||||
return self._placement
|
||||
|
||||
@@ -24,34 +24,38 @@ from watcher.common import rpc
|
||||
|
||||
def set_lib_defaults():
|
||||
cors.set_defaults(
|
||||
allow_headers=['X-Auth-Token',
|
||||
'X-Identity-Status',
|
||||
'X-Roles',
|
||||
'X-Service-Catalog',
|
||||
'X-User-Id',
|
||||
'X-Tenant-Id',
|
||||
'X-OpenStack-Request-ID'],
|
||||
expose_headers=['X-Auth-Token',
|
||||
'X-Subject-Token',
|
||||
'X-Service-Token',
|
||||
'X-OpenStack-Request-ID'],
|
||||
allow_methods=['GET',
|
||||
'PUT',
|
||||
'POST',
|
||||
'DELETE',
|
||||
'PATCH']
|
||||
allow_headers=[
|
||||
'X-Auth-Token',
|
||||
'X-Identity-Status',
|
||||
'X-Roles',
|
||||
'X-Service-Catalog',
|
||||
'X-User-Id',
|
||||
'X-Tenant-Id',
|
||||
'X-OpenStack-Request-ID',
|
||||
],
|
||||
expose_headers=[
|
||||
'X-Auth-Token',
|
||||
'X-Subject-Token',
|
||||
'X-Service-Token',
|
||||
'X-OpenStack-Request-ID',
|
||||
],
|
||||
allow_methods=['GET', 'PUT', 'POST', 'DELETE', 'PATCH'],
|
||||
)
|
||||
|
||||
|
||||
def parse_args(argv, default_config_files=None, default_config_dirs=None):
|
||||
default_config_files = (default_config_files or
|
||||
cfg.find_config_files(project='watcher'))
|
||||
default_config_dirs = (default_config_dirs or
|
||||
cfg.find_config_dirs(project='watcher'))
|
||||
default_config_files = default_config_files or cfg.find_config_files(
|
||||
project='watcher'
|
||||
)
|
||||
default_config_dirs = default_config_dirs or cfg.find_config_dirs(
|
||||
project='watcher'
|
||||
)
|
||||
rpc.set_defaults(control_exchange='watcher')
|
||||
cfg.CONF(argv[1:],
|
||||
project='watcher',
|
||||
version=version.version_info.release_string(),
|
||||
default_config_dirs=default_config_dirs,
|
||||
default_config_files=default_config_files)
|
||||
cfg.CONF(
|
||||
argv[1:],
|
||||
project='watcher',
|
||||
version=version.version_info.release_string(),
|
||||
default_config_dirs=default_config_dirs,
|
||||
default_config_files=default_config_files,
|
||||
)
|
||||
rpc.init(cfg.CONF)
|
||||
|
||||
@@ -23,11 +23,23 @@ LOG = log.getLogger(__name__)
|
||||
class RequestContext(context.RequestContext):
|
||||
"""Extends security contexts from the OpenStack common library."""
|
||||
|
||||
def __init__(self, user_id=None, project_id=None, is_admin=None,
|
||||
roles=None, timestamp=None, request_id=None, auth_token=None,
|
||||
overwrite=True, user_name=None, project_name=None,
|
||||
domain_name=None, domain_id=None, auth_token_info=None,
|
||||
**kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
user_id=None,
|
||||
project_id=None,
|
||||
is_admin=None,
|
||||
roles=None,
|
||||
timestamp=None,
|
||||
request_id=None,
|
||||
auth_token=None,
|
||||
overwrite=True,
|
||||
user_name=None,
|
||||
project_name=None,
|
||||
domain_name=None,
|
||||
domain_id=None,
|
||||
auth_token_info=None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Stores several additional request parameters:
|
||||
|
||||
:param domain_id: The ID of the domain.
|
||||
@@ -54,7 +66,8 @@ class RequestContext(context.RequestContext):
|
||||
overwrite=overwrite,
|
||||
roles=roles,
|
||||
global_request_id=kwargs.pop('global_request_id', None),
|
||||
system_scope=kwargs.pop('system_scope', None))
|
||||
system_scope=kwargs.pop('system_scope', None),
|
||||
)
|
||||
|
||||
# Note(sean-k-mooney): we should audit what we are using
|
||||
# this for and possibly remove it or document it.
|
||||
@@ -68,10 +81,12 @@ class RequestContext(context.RequestContext):
|
||||
|
||||
def to_dict(self):
|
||||
values = super().to_dict()
|
||||
values.update({
|
||||
'auth_token_info': getattr(self, 'auth_token_info', None),
|
||||
'timestamp': self.timestamp.isoformat(),
|
||||
})
|
||||
values.update(
|
||||
{
|
||||
'auth_token_info': getattr(self, 'auth_token_info', None),
|
||||
'timestamp': self.timestamp.isoformat(),
|
||||
}
|
||||
)
|
||||
return values
|
||||
|
||||
def __str__(self):
|
||||
|
||||
@@ -40,18 +40,24 @@ CONF = cfg.CONF
|
||||
|
||||
def wrap_keystone_exception(func):
|
||||
"""Wrap keystone exceptions and throw Watcher specific exceptions."""
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapped(*args, **kw):
|
||||
try:
|
||||
return func(*args, **kw)
|
||||
except keystone_exceptions.AuthorizationFailure:
|
||||
raise AuthorizationFailure(
|
||||
client=func.__name__, reason=sys.exc_info()[1])
|
||||
client=func.__name__, reason=sys.exc_info()[1]
|
||||
)
|
||||
except keystone_exceptions.ClientException:
|
||||
raise AuthorizationFailure(
|
||||
client=func.__name__,
|
||||
reason=(_('Unexpected keystone client error occurred: %s')
|
||||
% sys.exc_info()[1]))
|
||||
reason=(
|
||||
_('Unexpected keystone client error occurred: %s')
|
||||
% sys.exc_info()[1]
|
||||
),
|
||||
)
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
@@ -63,6 +69,7 @@ class WatcherException(Exception):
|
||||
with the keyword arguments provided to the constructor.
|
||||
|
||||
"""
|
||||
|
||||
msg_fmt = _("An unknown exception occurred")
|
||||
code = HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
headers = {}
|
||||
@@ -85,8 +92,9 @@ class WatcherException(Exception):
|
||||
# log the issue and the kwargs
|
||||
LOG.exception('Exception in string format operation')
|
||||
for name, value in kwargs.items():
|
||||
LOG.error("%(name)s: %(value)s",
|
||||
{'name': name, 'value': value})
|
||||
LOG.error(
|
||||
"%(name)s: %(value)s", {'name': name, 'value': value}
|
||||
)
|
||||
|
||||
if CONF.fatal_exception_format_errors:
|
||||
raise
|
||||
@@ -160,8 +168,9 @@ class InvalidIdentity(Invalid):
|
||||
|
||||
|
||||
class InvalidOperator(Invalid):
|
||||
msg_fmt = _("Filter operator is not valid: %(operator)s not "
|
||||
"in %(valid_operators)s")
|
||||
msg_fmt = _(
|
||||
"Filter operator is not valid: %(operator)s not in %(valid_operators)s"
|
||||
)
|
||||
|
||||
|
||||
class InvalidGoal(Invalid):
|
||||
@@ -233,8 +242,9 @@ class AuditTemplateNotFound(ResourceNotFound):
|
||||
|
||||
|
||||
class AuditTemplateAlreadyExists(Conflict):
|
||||
msg_fmt = _("An audit_template with UUID or name %(audit_template)s "
|
||||
"already exists")
|
||||
msg_fmt = _(
|
||||
"An audit_template with UUID or name %(audit_template)s already exists"
|
||||
)
|
||||
|
||||
|
||||
class AuditTypeNotFound(Invalid):
|
||||
@@ -270,13 +280,15 @@ class AuditIntervalNotAllowed(Invalid):
|
||||
|
||||
|
||||
class AuditStartEndTimeNotAllowed(Invalid):
|
||||
msg_fmt = _("Start or End time of audit must not be set for "
|
||||
"%(audit_type)s.")
|
||||
msg_fmt = _(
|
||||
"Start or End time of audit must not be set for %(audit_type)s."
|
||||
)
|
||||
|
||||
|
||||
class AuditReferenced(Invalid):
|
||||
msg_fmt = _("Audit %(audit)s is referenced by one or multiple action "
|
||||
"plans")
|
||||
msg_fmt = _(
|
||||
"Audit %(audit)s is referenced by one or multiple action plans"
|
||||
)
|
||||
|
||||
|
||||
class AuditCancelled(WatcherException):
|
||||
@@ -296,8 +308,9 @@ class ActionPlanAlreadyExists(Conflict):
|
||||
|
||||
|
||||
class ActionPlanReferenced(Invalid):
|
||||
msg_fmt = _("Action Plan %(action_plan)s is referenced by one or "
|
||||
"multiple actions")
|
||||
msg_fmt = _(
|
||||
"Action Plan %(action_plan)s is referenced by one or multiple actions"
|
||||
)
|
||||
|
||||
|
||||
class ActionPlanCancelled(WatcherException):
|
||||
@@ -317,13 +330,15 @@ class ActionAlreadyExists(Conflict):
|
||||
|
||||
|
||||
class ActionReferenced(Invalid):
|
||||
msg_fmt = _("Action plan %(action_plan)s is referenced by one or "
|
||||
"multiple goals")
|
||||
msg_fmt = _(
|
||||
"Action plan %(action_plan)s is referenced by one or multiple goals"
|
||||
)
|
||||
|
||||
|
||||
class ActionFilterCombinationProhibited(Invalid):
|
||||
msg_fmt = _("Filtering actions on both audit and action-plan is "
|
||||
"prohibited")
|
||||
msg_fmt = _(
|
||||
"Filtering actions on both audit and action-plan is prohibited"
|
||||
)
|
||||
|
||||
|
||||
class UnsupportedActionType(UnsupportedError):
|
||||
@@ -364,6 +379,7 @@ class StartError(Invalid):
|
||||
|
||||
# decision engine
|
||||
|
||||
|
||||
class WorkflowExecutionException(WatcherException):
|
||||
msg_fmt = _('Workflow execution error: %(error)s')
|
||||
|
||||
@@ -393,18 +409,23 @@ class NoAvailableStrategyForGoal(WatcherException):
|
||||
|
||||
|
||||
class InvalidIndicatorValue(WatcherException):
|
||||
msg_fmt = _("The indicator '%(name)s' with value '%(value)s' "
|
||||
"and spec type '%(spec_type)s' is invalid.")
|
||||
msg_fmt = _(
|
||||
"The indicator '%(name)s' with value '%(value)s' "
|
||||
"and spec type '%(spec_type)s' is invalid."
|
||||
)
|
||||
|
||||
|
||||
class GlobalEfficacyComputationError(WatcherException):
|
||||
msg_fmt = _("Could not compute the global efficacy for the '%(goal)s' "
|
||||
"goal using the '%(strategy)s' strategy.")
|
||||
msg_fmt = _(
|
||||
"Could not compute the global efficacy for the '%(goal)s' "
|
||||
"goal using the '%(strategy)s' strategy."
|
||||
)
|
||||
|
||||
|
||||
class UnsupportedDataSource(UnsupportedError):
|
||||
msg_fmt = _("Datasource %(datasource)s is not supported "
|
||||
"by strategy %(strategy)s")
|
||||
msg_fmt = _(
|
||||
"Datasource %(datasource)s is not supported by strategy %(strategy)s"
|
||||
)
|
||||
|
||||
|
||||
class DataSourceNotAvailable(WatcherException):
|
||||
@@ -413,11 +434,13 @@ class DataSourceNotAvailable(WatcherException):
|
||||
|
||||
class MetricNotAvailable(WatcherException):
|
||||
"""Indicate that a metric is not configured or does not exists"""
|
||||
|
||||
msg_fmt = _('Metric: %(metric)s not available')
|
||||
|
||||
|
||||
class NoDatasourceAvailable(WatcherException):
|
||||
"""No datasources have been configured"""
|
||||
|
||||
msg_fmt = _('No datasources available')
|
||||
|
||||
|
||||
@@ -434,8 +457,10 @@ class ServiceNotFound(ResourceNotFound):
|
||||
|
||||
|
||||
class WildcardCharacterIsUsed(WatcherException):
|
||||
msg_fmt = _("You shouldn't use any other IDs of %(resource)s if you use "
|
||||
"wildcard character.")
|
||||
msg_fmt = _(
|
||||
"You shouldn't use any other IDs of %(resource)s if you use "
|
||||
"wildcard character."
|
||||
)
|
||||
|
||||
|
||||
class CronFormatIsInvalid(WatcherException):
|
||||
@@ -443,8 +468,9 @@ class CronFormatIsInvalid(WatcherException):
|
||||
|
||||
|
||||
class ActionDescriptionAlreadyExists(Conflict):
|
||||
msg_fmt = _("An action description with type %(action_type)s is "
|
||||
"already exist.")
|
||||
msg_fmt = _(
|
||||
"An action description with type %(action_type)s is already exist."
|
||||
)
|
||||
|
||||
|
||||
class ActionDescriptionNotFound(ResourceNotFound):
|
||||
@@ -457,6 +483,7 @@ class ActionExecutionFailure(WatcherException):
|
||||
|
||||
# Model
|
||||
|
||||
|
||||
class ComputeResourceNotFound(WatcherException):
|
||||
msg_fmt = _("The compute resource '%(name)s' could not be found")
|
||||
|
||||
@@ -466,8 +493,9 @@ class InstanceNotFound(ComputeResourceNotFound):
|
||||
|
||||
|
||||
class InstanceNotMapped(ComputeResourceNotFound):
|
||||
msg_fmt = _("The mapped compute node for instance '%(uuid)s' "
|
||||
"could not be found.")
|
||||
msg_fmt = _(
|
||||
"The mapped compute node for instance '%(uuid)s' could not be found."
|
||||
)
|
||||
|
||||
|
||||
class ComputeNodeNotFound(ComputeResourceNotFound):
|
||||
@@ -519,8 +547,10 @@ class NegativeLimitError(WatcherException):
|
||||
|
||||
|
||||
class NotificationPayloadError(WatcherException):
|
||||
msg_fmt = _("Payload not populated when trying to send notification "
|
||||
"\"%(class_name)s\"")
|
||||
msg_fmt = _(
|
||||
"Payload not populated when trying to send notification "
|
||||
"\"%(class_name)s\""
|
||||
)
|
||||
|
||||
|
||||
class InvalidPoolAttributeValue(Invalid):
|
||||
@@ -528,13 +558,17 @@ class InvalidPoolAttributeValue(Invalid):
|
||||
|
||||
|
||||
class DataSourceConfigConflict(UnsupportedError):
|
||||
msg_fmt = _("Datasource %(datasource_one)s is not supported "
|
||||
"when datasource %(datasource_two)s is also enabled.")
|
||||
msg_fmt = _(
|
||||
"Datasource %(datasource_one)s is not supported "
|
||||
"when datasource %(datasource_two)s is also enabled."
|
||||
)
|
||||
|
||||
|
||||
class LiveMigrationFailed(WatcherException):
|
||||
msg_fmt = _("Live migration execution and abort both failed "
|
||||
"for the instance %(name)s.")
|
||||
msg_fmt = _(
|
||||
"Live migration execution and abort both failed "
|
||||
"for the instance %(name)s."
|
||||
)
|
||||
|
||||
|
||||
class NovaClientError(WatcherException):
|
||||
|
||||
@@ -55,17 +55,24 @@ def log_executor_stats(executor, name="unknown"):
|
||||
"State of %s ThreadPoolExecutor when submitting a new "
|
||||
"task: max_workers: %d, workers: %d, idle workers: %d, "
|
||||
"queued work: %d, stats: %s",
|
||||
name, executor._max_workers, len(executor._workers),
|
||||
name,
|
||||
executor._max_workers,
|
||||
len(executor._workers),
|
||||
len([w for w in executor._workers if w.idle]),
|
||||
executor._work_queue.qsize(), stats)
|
||||
executor._work_queue.qsize(),
|
||||
stats,
|
||||
)
|
||||
elif isinstance(executor, futurist.GreenThreadPoolExecutor):
|
||||
LOG.debug(
|
||||
"State of %s GreenThreadPoolExecutor when submitting a "
|
||||
"new task: workers: %d, max_workers: %d, "
|
||||
"work queued length: %d, stats: %s",
|
||||
name, len(executor._pool.coroutines_running),
|
||||
name,
|
||||
len(executor._pool.coroutines_running),
|
||||
executor._pool.size,
|
||||
executor._delayed_work.unfinished_tasks, stats)
|
||||
executor._delayed_work.unfinished_tasks,
|
||||
stats,
|
||||
)
|
||||
except Exception as e:
|
||||
LOG.debug("Failed to log executor stats for %s: %s", name, e)
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class IronicHelper:
|
||||
|
||||
def __init__(self, osc=None):
|
||||
""":param osc: an OpenStackClients instance"""
|
||||
self.osc = osc if osc else clients.OpenStackClients()
|
||||
|
||||
@@ -26,7 +26,6 @@ LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class KeystoneHelper:
|
||||
|
||||
def __init__(self, osc=None):
|
||||
""":param osc: an OpenStackClients instance"""
|
||||
self.osc = osc if osc else clients.OpenStackClients()
|
||||
@@ -40,10 +39,12 @@ class KeystoneHelper:
|
||||
roles = self.keystone.roles.list(name=name_or_id)
|
||||
if len(roles) == 0:
|
||||
raise exception.Invalid(
|
||||
message=(_("Role not Found: %s") % name_or_id))
|
||||
message=(_("Role not Found: %s") % name_or_id)
|
||||
)
|
||||
if len(roles) > 1:
|
||||
raise exception.Invalid(
|
||||
message=(_("Role name seems ambiguous: %s") % name_or_id))
|
||||
message=(_("Role name seems ambiguous: %s") % name_or_id)
|
||||
)
|
||||
return roles[0]
|
||||
|
||||
def get_user(self, name_or_id):
|
||||
@@ -54,10 +55,12 @@ class KeystoneHelper:
|
||||
users = self.keystone.users.list(name=name_or_id)
|
||||
if len(users) == 0:
|
||||
raise exception.Invalid(
|
||||
message=(_("User not Found: %s") % name_or_id))
|
||||
message=(_("User not Found: %s") % name_or_id)
|
||||
)
|
||||
if len(users) > 1:
|
||||
raise exception.Invalid(
|
||||
message=(_("User name seems ambiguous: %s") % name_or_id))
|
||||
message=(_("User name seems ambiguous: %s") % name_or_id)
|
||||
)
|
||||
return users[0]
|
||||
|
||||
def get_project(self, name_or_id):
|
||||
@@ -68,11 +71,14 @@ class KeystoneHelper:
|
||||
projects = self.keystone.projects.list(name=name_or_id)
|
||||
if len(projects) == 0:
|
||||
raise exception.Invalid(
|
||||
message=(_("Project not Found: %s") % name_or_id))
|
||||
message=(_("Project not Found: %s") % name_or_id)
|
||||
)
|
||||
if len(projects) > 1:
|
||||
raise exception.Invalid(
|
||||
message=(_("Project name seems ambiguous: %s") %
|
||||
name_or_id))
|
||||
message=(
|
||||
_("Project name seems ambiguous: %s") % name_or_id
|
||||
)
|
||||
)
|
||||
return projects[0]
|
||||
|
||||
def get_domain(self, name_or_id):
|
||||
@@ -83,11 +89,12 @@ class KeystoneHelper:
|
||||
domains = self.keystone.domains.list(name=name_or_id)
|
||||
if len(domains) == 0:
|
||||
raise exception.Invalid(
|
||||
message=(_("Domain not Found: %s") % name_or_id))
|
||||
message=(_("Domain not Found: %s") % name_or_id)
|
||||
)
|
||||
if len(domains) > 1:
|
||||
raise exception.Invalid(
|
||||
message=(_("Domain name seems ambiguous: %s") %
|
||||
name_or_id))
|
||||
message=(_("Domain name seems ambiguous: %s") % name_or_id)
|
||||
)
|
||||
return domains[0]
|
||||
|
||||
def is_service_enabled_by_type(self, svc_type):
|
||||
|
||||
@@ -17,7 +17,6 @@ import abc
|
||||
|
||||
|
||||
class BaseLoader(metaclass=abc.ABCMeta):
|
||||
|
||||
@abc.abstractmethod
|
||||
def list_available(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
@@ -27,7 +27,6 @@ LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class DefaultLoader(base.BaseLoader):
|
||||
|
||||
def __init__(self, namespace, conf=cfg.CONF):
|
||||
"""Entry point loader for Watcher using Stevedore
|
||||
|
||||
@@ -43,9 +42,7 @@ class DefaultLoader(base.BaseLoader):
|
||||
try:
|
||||
LOG.debug("Loading in namespace %s => %s ", self.namespace, name)
|
||||
driver_manager = drivermanager.DriverManager(
|
||||
namespace=self.namespace,
|
||||
name=name,
|
||||
invoke_on_load=False,
|
||||
namespace=self.namespace, name=name, invoke_on_load=False
|
||||
)
|
||||
|
||||
driver_cls = driver_manager.driver
|
||||
@@ -82,13 +79,12 @@ class DefaultLoader(base.BaseLoader):
|
||||
if not config_group:
|
||||
raise exception.LoadingError(name=name)
|
||||
|
||||
config.update({
|
||||
name: value for name, value in config_group.items()
|
||||
})
|
||||
config.update({name: value for name, value in config_group.items()})
|
||||
|
||||
return config
|
||||
|
||||
def list_available(self):
|
||||
extension_manager = extensionmanager.ExtensionManager(
|
||||
namespace=self.namespace)
|
||||
namespace=self.namespace
|
||||
)
|
||||
return {ext.name: ext.plugin for ext in extension_manager.extensions}
|
||||
|
||||
@@ -41,7 +41,8 @@ class Loadable(metaclass=abc.ABCMeta):
|
||||
|
||||
|
||||
LoadableSingletonMeta = type(
|
||||
"LoadableSingletonMeta", (abc.ABCMeta, service.Singleton), {})
|
||||
"LoadableSingletonMeta", (abc.ABCMeta, service.Singleton), {}
|
||||
)
|
||||
|
||||
|
||||
class LoadableSingleton(metaclass=LoadableSingletonMeta):
|
||||
|
||||
@@ -60,7 +60,8 @@ class BaseMetalNode(abc.ABC):
|
||||
self.power_off()
|
||||
else:
|
||||
raise exception.UnsupportedActionType(
|
||||
f"Cannot set power state: {state}")
|
||||
f"Cannot set power state: {state}"
|
||||
)
|
||||
|
||||
|
||||
class BaseMetalHelper(abc.ABC):
|
||||
|
||||
@@ -41,8 +41,9 @@ class IronicNode(base.BaseMetalNode):
|
||||
self._ironic_node = ironic_node
|
||||
|
||||
def get_power_state(self):
|
||||
return POWER_STATES_MAP.get(self._ironic_node.power_state,
|
||||
metal_constants.PowerState.UNKNOWN)
|
||||
return POWER_STATES_MAP.get(
|
||||
self._ironic_node.power_state, metal_constants.PowerState.UNKNOWN
|
||||
)
|
||||
|
||||
def get_id(self):
|
||||
return self._ironic_node.uuid
|
||||
@@ -71,8 +72,10 @@ class IronicHelper(base.BaseMetalHelper):
|
||||
node_info = self._client.node.get(node.uuid)
|
||||
hypervisor_id = node_info.extra.get('compute_node_id', None)
|
||||
if hypervisor_id is None:
|
||||
LOG.warning('Cannot find compute_node_id in extra '
|
||||
'of ironic node %s', node.uuid)
|
||||
LOG.warning(
|
||||
'Cannot find compute_node_id in extra of ironic node %s',
|
||||
node.uuid,
|
||||
)
|
||||
continue
|
||||
|
||||
hypervisor_node = self.nova_client.get_compute_node_by_uuid(
|
||||
|
||||
@@ -42,8 +42,8 @@ class MaasNode(base.BaseMetalNode):
|
||||
|
||||
def get_power_state(self):
|
||||
maas_state = utils.async_compat_call(
|
||||
self._maas_node.query_power_state,
|
||||
timeout=CONF.maas_client.timeout)
|
||||
self._maas_node.query_power_state, timeout=CONF.maas_client.timeout
|
||||
)
|
||||
|
||||
# python-libmaas may not be available, so we'll avoid a global
|
||||
# variable.
|
||||
@@ -53,27 +53,32 @@ class MaasNode(base.BaseMetalNode):
|
||||
maas_enum.PowerState.ERROR: metal_constants.PowerState.ERROR,
|
||||
maas_enum.PowerState.UNKNOWN: metal_constants.PowerState.UNKNOWN,
|
||||
}
|
||||
return power_states_map.get(maas_state,
|
||||
metal_constants.PowerState.UNKNOWN)
|
||||
return power_states_map.get(
|
||||
maas_state, metal_constants.PowerState.UNKNOWN
|
||||
)
|
||||
|
||||
def get_id(self):
|
||||
return self._maas_node.system_id
|
||||
|
||||
def power_on(self):
|
||||
LOG.info("Powering on MAAS node: %s %s",
|
||||
self._maas_node.fqdn,
|
||||
self._maas_node.system_id)
|
||||
LOG.info(
|
||||
"Powering on MAAS node: %s %s",
|
||||
self._maas_node.fqdn,
|
||||
self._maas_node.system_id,
|
||||
)
|
||||
utils.async_compat_call(
|
||||
self._maas_node.power_on,
|
||||
timeout=CONF.maas_client.timeout)
|
||||
self._maas_node.power_on, timeout=CONF.maas_client.timeout
|
||||
)
|
||||
|
||||
def power_off(self):
|
||||
LOG.info("Powering off MAAS node: %s %s",
|
||||
self._maas_node.fqdn,
|
||||
self._maas_node.system_id)
|
||||
LOG.info(
|
||||
"Powering off MAAS node: %s %s",
|
||||
self._maas_node.fqdn,
|
||||
self._maas_node.system_id,
|
||||
)
|
||||
utils.async_compat_call(
|
||||
self._maas_node.power_off,
|
||||
timeout=CONF.maas_client.timeout)
|
||||
self._maas_node.power_off, timeout=CONF.maas_client.timeout
|
||||
)
|
||||
|
||||
|
||||
class MaasHelper(base.BaseMetalHelper):
|
||||
@@ -81,7 +86,8 @@ class MaasHelper(base.BaseMetalHelper):
|
||||
super().__init__(*args, **kwargs)
|
||||
if not maas_enum:
|
||||
raise exception.UnsupportedError(
|
||||
"MAAS client unavailable. Please install python-libmaas.")
|
||||
"MAAS client unavailable. Please install python-libmaas."
|
||||
)
|
||||
|
||||
@property
|
||||
def _client(self):
|
||||
@@ -92,8 +98,8 @@ class MaasHelper(base.BaseMetalHelper):
|
||||
def list_compute_nodes(self):
|
||||
out_list = []
|
||||
node_list = utils.async_compat_call(
|
||||
self._client.machines.list,
|
||||
timeout=CONF.maas_client.timeout)
|
||||
self._client.machines.list, timeout=CONF.maas_client.timeout
|
||||
)
|
||||
|
||||
compute_nodes = self.nova_client.get_compute_node_list(
|
||||
filter_ironic_nodes=False
|
||||
@@ -116,14 +122,17 @@ class MaasHelper(base.BaseMetalHelper):
|
||||
|
||||
def _get_compute_node_by_hostname(self, hostname):
|
||||
compute_nodes = self.nova_client.get_compute_node_by_hostname(
|
||||
hostname, detailed=True)
|
||||
hostname, detailed=True
|
||||
)
|
||||
for compute_node in compute_nodes:
|
||||
if compute_node.hypervisor_hostname == hostname:
|
||||
return compute_node
|
||||
|
||||
def get_node(self, node_id):
|
||||
maas_node = utils.async_compat_call(
|
||||
self._client.machines.get, node_id,
|
||||
timeout=CONF.maas_client.timeout)
|
||||
self._client.machines.get,
|
||||
node_id,
|
||||
timeout=CONF.maas_client.timeout,
|
||||
)
|
||||
compute_node = self._get_compute_node_by_hostname(maas_node.fqdn)
|
||||
return MaasNode(maas_node, compute_node, self._client)
|
||||
|
||||
@@ -55,8 +55,10 @@ def nova_retries(call):
|
||||
else:
|
||||
LOG.error(
|
||||
'Failed to connect to Nova service after %d attempts',
|
||||
retries + 1)
|
||||
retries + 1,
|
||||
)
|
||||
raise
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@@ -76,6 +78,7 @@ def handle_nova_error(resource_type, id_arg_index=1):
|
||||
(default 1, which is the first argument after self)
|
||||
:returns: Decorator function
|
||||
"""
|
||||
|
||||
def decorator(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
@@ -92,7 +95,9 @@ def handle_nova_error(resource_type, id_arg_index=1):
|
||||
except sdk_exc.SDKException as e:
|
||||
LOG.error("Nova client error: %s", e)
|
||||
raise exception.NovaClientError(reason=str(e))
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
@@ -148,7 +153,7 @@ class Server:
|
||||
locked=nova_server.is_locked,
|
||||
metadata=nova_server.metadata,
|
||||
availability_zone=nova_server.availability_zone,
|
||||
pinned_availability_zone=nova_server.pinned_availability_zone
|
||||
pinned_availability_zone=nova_server.pinned_availability_zone,
|
||||
)
|
||||
|
||||
|
||||
@@ -260,7 +265,7 @@ class Flavor:
|
||||
ephemeral=nova_flavor.ephemeral,
|
||||
swap=nova_flavor.swap,
|
||||
is_public=nova_flavor.is_public,
|
||||
extra_specs=nova_flavor.extra_specs
|
||||
extra_specs=nova_flavor.extra_specs,
|
||||
)
|
||||
|
||||
|
||||
@@ -359,13 +364,10 @@ class ServerMigration:
|
||||
:param nova_migration: OpenStackSDK ServerMigration
|
||||
:returns: ServerMigration dataclass instance
|
||||
"""
|
||||
return cls(
|
||||
id=nova_migration.id,
|
||||
)
|
||||
return cls(id=nova_migration.id)
|
||||
|
||||
|
||||
class NovaHelper:
|
||||
|
||||
def __init__(self, osc=None, session=None, context=None):
|
||||
"""Create and return a helper to call the nova service
|
||||
|
||||
@@ -380,9 +382,7 @@ class NovaHelper:
|
||||
clients.check_min_nova_api_version(CONF.nova.api_version)
|
||||
self.osc = osc if osc else clients.OpenStackClients()
|
||||
self.cinder = self.osc.cinder()
|
||||
self._create_sdk_connection(
|
||||
context=context, session=session
|
||||
)
|
||||
self._create_sdk_connection(context=context, session=session)
|
||||
self._is_pinned_az_available = None
|
||||
|
||||
def _override_deprecated_configs(self):
|
||||
@@ -413,14 +413,17 @@ class NovaHelper:
|
||||
# [nova], use [watcher_clients_auth] as fallback
|
||||
LOG.debug(
|
||||
"could not load auth plugin from [nova] section, using %s "
|
||||
"as fallback", clients_auth.WATCHER_CLIENTS_AUTH
|
||||
"as fallback",
|
||||
clients_auth.WATCHER_CLIENTS_AUTH,
|
||||
)
|
||||
auth_group = clients_auth.WATCHER_CLIENTS_AUTH
|
||||
|
||||
self.connection = clients.get_sdk_connection(
|
||||
auth_group, context=context, session=session,
|
||||
auth_group,
|
||||
context=context,
|
||||
session=session,
|
||||
interface=CONF.nova.valid_interfaces,
|
||||
region_name=CONF.nova.region_name
|
||||
region_name=CONF.nova.region_name,
|
||||
)
|
||||
|
||||
def is_pinned_az_available(self):
|
||||
@@ -452,15 +455,17 @@ class NovaHelper:
|
||||
]
|
||||
if filter_ironic_nodes:
|
||||
compute_nodes = [
|
||||
node for node in compute_nodes
|
||||
node
|
||||
for node in compute_nodes
|
||||
if node.hypervisor_type != 'ironic'
|
||||
]
|
||||
return compute_nodes
|
||||
|
||||
@nova_retries
|
||||
@handle_nova_error("Compute node")
|
||||
def get_compute_node_by_name(self, node_name, servers=False,
|
||||
detailed=False):
|
||||
def get_compute_node_by_name(
|
||||
self, node_name, servers=False, detailed=False
|
||||
):
|
||||
"""Search for a hypervisor (compute node) by hypervisor_hostname
|
||||
|
||||
:param node_name: The hypervisor_hostname to search
|
||||
@@ -473,8 +478,10 @@ class NovaHelper:
|
||||
"""
|
||||
# SDK hypervisors() method returns all hypervisors, filter by name
|
||||
hypervisors = self.connection.compute.hypervisors(
|
||||
hypervisor_hostname_pattern=node_name, with_servers=servers,
|
||||
details=detailed)
|
||||
hypervisor_hostname_pattern=node_name,
|
||||
with_servers=servers,
|
||||
details=detailed,
|
||||
)
|
||||
return [Hypervisor.from_openstacksdk(h) for h in hypervisors]
|
||||
|
||||
def get_compute_node_by_hostname(self, node_hostname):
|
||||
@@ -490,7 +497,8 @@ class NovaHelper:
|
||||
# more than one compute node. If so, match on the compute service
|
||||
# hostname.
|
||||
compute_nodes = self.get_compute_node_by_name(
|
||||
node_hostname, detailed=True)
|
||||
node_hostname, detailed=True
|
||||
)
|
||||
if compute_nodes:
|
||||
for cn in compute_nodes:
|
||||
if cn.service_host == node_hostname:
|
||||
@@ -528,9 +536,7 @@ class NovaHelper:
|
||||
https://bugs.launchpad.net/watcher/+bug/1834679
|
||||
:returns: list of Server wrapper objects
|
||||
"""
|
||||
query_params = {
|
||||
'all_projects': True, 'marker': marker
|
||||
}
|
||||
query_params = {'all_projects': True, 'marker': marker}
|
||||
if limit != -1:
|
||||
query_params['limit'] = limit
|
||||
if filters:
|
||||
@@ -540,8 +546,7 @@ class NovaHelper:
|
||||
# passing 'host' is simply ignored
|
||||
filters['compute_host'] = filters.pop('host')
|
||||
query_params.update(filters)
|
||||
servers = self.connection.compute.servers(details=True,
|
||||
**query_params)
|
||||
servers = self.connection.compute.servers(details=True, **query_params)
|
||||
return [Server.from_openstacksdk(s) for s in servers]
|
||||
|
||||
@nova_retries
|
||||
@@ -553,9 +558,9 @@ class NovaHelper:
|
||||
:returns: Server wrapper object matching the UUID
|
||||
:raises: ComputeResourceNotFound if no instance was found
|
||||
"""
|
||||
servers = self.connection.compute.servers(details=True,
|
||||
all_projects=True,
|
||||
uuid=instance_uuid)
|
||||
servers = self.connection.compute.servers(
|
||||
details=True, all_projects=True, uuid=instance_uuid
|
||||
)
|
||||
if servers:
|
||||
return Server.from_openstacksdk(servers[0])
|
||||
else:
|
||||
@@ -586,8 +591,14 @@ class NovaHelper:
|
||||
flavor_obj = self._get_flavor(flavor)
|
||||
return flavor_obj.id
|
||||
except exception.ComputeResourceNotFound:
|
||||
flavor_id = next((f.id for f in self.get_flavor_list() if
|
||||
f.flavor_name == flavor), None)
|
||||
flavor_id = next(
|
||||
(
|
||||
f.id
|
||||
for f in self.get_flavor_list()
|
||||
if f.flavor_name == flavor
|
||||
),
|
||||
None,
|
||||
)
|
||||
if flavor_id:
|
||||
return flavor_id
|
||||
|
||||
@@ -732,7 +743,7 @@ class NovaHelper:
|
||||
except exception.ComputeResourceNotFound:
|
||||
LOG.debug(
|
||||
"Instance %s was not found, could not confirm its resize",
|
||||
instance_id
|
||||
instance_id,
|
||||
)
|
||||
return False
|
||||
|
||||
@@ -742,7 +753,7 @@ class NovaHelper:
|
||||
except exception.ComputeResourceNotFound:
|
||||
LOG.debug(
|
||||
"Instance %s was not found, could not confirm its resize",
|
||||
instance_id
|
||||
instance_id,
|
||||
)
|
||||
return False
|
||||
retry -= 1
|
||||
@@ -750,12 +761,12 @@ class NovaHelper:
|
||||
if instance.status == previous_status:
|
||||
return True
|
||||
else:
|
||||
LOG.debug("confirm resize failed for the "
|
||||
"instance %s", instance_id)
|
||||
LOG.debug("confirm resize failed for the instance %s", instance_id)
|
||||
return False
|
||||
|
||||
def watcher_non_live_migrate_instance(self, instance_id, dest_hostname,
|
||||
retry=None, interval=None):
|
||||
def watcher_non_live_migrate_instance(
|
||||
self, instance_id, dest_hostname, retry=None, interval=None
|
||||
):
|
||||
"""This method migrates a given instance
|
||||
|
||||
This method uses the Nova built-in migrate()
|
||||
@@ -775,8 +786,7 @@ class NovaHelper:
|
||||
:raises: NovaClientError if there is any problem while calling the Nova
|
||||
api
|
||||
"""
|
||||
LOG.debug(
|
||||
"Trying a cold migrate of instance '%s' ", instance_id)
|
||||
LOG.debug("Trying a cold migrate of instance '%s' ", instance_id)
|
||||
|
||||
# Use config defaults if not provided in method parameters
|
||||
retry = retry or CONF.nova.migration_max_retries
|
||||
@@ -790,8 +800,7 @@ class NovaHelper:
|
||||
)
|
||||
return False
|
||||
host_name = instance.host
|
||||
LOG.debug(
|
||||
{'instance': instance_id, 'host': host_name})
|
||||
LOG.debug({'instance': instance_id, 'host': host_name})
|
||||
|
||||
previous_status = instance.status
|
||||
self._instance_migrate(instance_id, dest_hostname)
|
||||
@@ -803,32 +812,30 @@ class NovaHelper:
|
||||
)
|
||||
return False
|
||||
|
||||
while (instance.status not in
|
||||
["VERIFY_RESIZE", "ERROR"] and retry):
|
||||
while instance.status not in ["VERIFY_RESIZE", "ERROR"] and retry:
|
||||
try:
|
||||
instance = self.find_instance(instance_id)
|
||||
except exception.ComputeResourceNotFound:
|
||||
LOG.debug(
|
||||
"Instance %s not found, can't cold migrate it.",
|
||||
instance_id
|
||||
instance_id,
|
||||
)
|
||||
return False
|
||||
time.sleep(interval)
|
||||
retry -= 1
|
||||
new_hostname = instance.host
|
||||
|
||||
if (host_name != new_hostname and
|
||||
instance.status == 'VERIFY_RESIZE'):
|
||||
if host_name != new_hostname and instance.status == 'VERIFY_RESIZE':
|
||||
if not self.confirm_resize(instance, previous_status):
|
||||
return False
|
||||
LOG.debug(
|
||||
"cold migration succeeded : "
|
||||
"instance %(instance)s is now on host '%(host)s'.",
|
||||
{'instance': instance_id, 'host': new_hostname})
|
||||
{'instance': instance_id, 'host': new_hostname},
|
||||
)
|
||||
return True
|
||||
else:
|
||||
LOG.debug(
|
||||
"cold migration for instance %s failed", instance_id)
|
||||
LOG.debug("cold migration for instance %s failed", instance_id)
|
||||
return False
|
||||
|
||||
def resize_instance(self, instance_id, flavor, retry=None, interval=None):
|
||||
@@ -848,9 +855,9 @@ class NovaHelper:
|
||||
api
|
||||
"""
|
||||
LOG.debug(
|
||||
"Trying a resize of instance %(instance)s to "
|
||||
"flavor '%(flavor)s'",
|
||||
{'instance': instance_id, 'flavor': flavor})
|
||||
"Trying a resize of instance %(instance)s to flavor '%(flavor)s'",
|
||||
{'instance': instance_id, 'flavor': flavor},
|
||||
)
|
||||
|
||||
# Use config defaults if not provided in method parameters
|
||||
retry = retry or CONF.nova.migration_max_retries
|
||||
@@ -871,14 +878,19 @@ class NovaHelper:
|
||||
LOG.debug("Flavor not found: %s, could not resize", flavor)
|
||||
return False
|
||||
except exception.NovaClientError as e:
|
||||
LOG.debug("Nova client exception occurred while resizing "
|
||||
"instance %s. Exception: %s", instance_id, e)
|
||||
LOG.debug(
|
||||
"Nova client exception occurred while resizing "
|
||||
"instance %s. Exception: %s",
|
||||
instance_id,
|
||||
e,
|
||||
)
|
||||
return False
|
||||
|
||||
instance_status = instance.vm_state
|
||||
LOG.debug(
|
||||
"Instance %(id)s is in '%(status)s' status.",
|
||||
{'id': instance_id, 'status': instance_status})
|
||||
{'id': instance_id, 'status': instance_status},
|
||||
)
|
||||
|
||||
self._instance_resize(instance_id, flavor_id)
|
||||
while instance.vm_state != 'resized' and retry:
|
||||
@@ -899,13 +911,17 @@ class NovaHelper:
|
||||
|
||||
self._instance_confirm_resize(instance_id)
|
||||
|
||||
LOG.debug("Resizing succeeded : instance %s is now on flavor "
|
||||
"'%s'.", instance_id, flavor_id)
|
||||
LOG.debug(
|
||||
"Resizing succeeded : instance %s is now on flavor '%s'.",
|
||||
instance_id,
|
||||
flavor_id,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def live_migrate_instance(self, instance_id, dest_hostname, retry=None,
|
||||
interval=None):
|
||||
def live_migrate_instance(
|
||||
self, instance_id, dest_hostname, retry=None, interval=None
|
||||
):
|
||||
"""This method does a live migration of a given instance
|
||||
|
||||
This method uses the Nova built-in live_migrate()
|
||||
@@ -925,7 +941,8 @@ class NovaHelper:
|
||||
"""
|
||||
LOG.debug(
|
||||
"Trying a live migrate instance %(instance)s ",
|
||||
{'instance': instance_id})
|
||||
{'instance': instance_id},
|
||||
)
|
||||
|
||||
# Use config defaults if not provided in method parameters
|
||||
retry = retry or CONF.nova.migration_max_retries
|
||||
@@ -940,7 +957,8 @@ class NovaHelper:
|
||||
host_name = instance.host
|
||||
LOG.debug(
|
||||
"Instance %(instance)s found on host '%(host)s'.",
|
||||
{'instance': instance_id, 'host': host_name})
|
||||
{'instance': instance_id, 'host': host_name},
|
||||
)
|
||||
|
||||
self._instance_live_migrate(instance_id, dest_hostname)
|
||||
|
||||
@@ -953,13 +971,13 @@ class NovaHelper:
|
||||
# NOTE: If destination host is not specified for live migration
|
||||
# let nova scheduler choose the destination host.
|
||||
if dest_hostname is None:
|
||||
while (instance.status not in ['ACTIVE', 'ERROR'] and retry):
|
||||
while instance.status not in ['ACTIVE', 'ERROR'] and retry:
|
||||
try:
|
||||
instance = self.find_instance(instance_id)
|
||||
except exception.ComputeResourceNotFound:
|
||||
LOG.debug(
|
||||
"Instance %s not found, can't live migrate",
|
||||
instance_id
|
||||
instance_id,
|
||||
)
|
||||
return False
|
||||
LOG.debug('Waiting the migration of %s', instance_id)
|
||||
@@ -971,7 +989,8 @@ class NovaHelper:
|
||||
LOG.debug(
|
||||
"Live migration succeeded : "
|
||||
"instance %(instance)s is now on host '%(host)s'.",
|
||||
{'instance': instance_id, 'host': new_hostname})
|
||||
{'instance': instance_id, 'host': new_hostname},
|
||||
)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
@@ -987,7 +1006,7 @@ class NovaHelper:
|
||||
break
|
||||
LOG.debug(
|
||||
'Waiting the migration of %s to %s', instance, instance.host
|
||||
)
|
||||
)
|
||||
time.sleep(interval)
|
||||
retry -= 1
|
||||
|
||||
@@ -998,7 +1017,8 @@ class NovaHelper:
|
||||
LOG.debug(
|
||||
"Live migration succeeded : "
|
||||
"instance %(instance)s is now on host '%(host)s'.",
|
||||
{'instance': instance_id, 'host': host_name})
|
||||
{'instance': instance_id, 'host': host_name},
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
@@ -1022,13 +1042,16 @@ class NovaHelper:
|
||||
except exception.ComputeResourceNotFound:
|
||||
# failed to abort the migration since the migration does not exist
|
||||
LOG.debug(
|
||||
"No running migrations found for instance %s", instance_id)
|
||||
"No running migrations found for instance %s", instance_id
|
||||
)
|
||||
if migration:
|
||||
migration_id = migration[0].id
|
||||
try:
|
||||
self._live_migration_abort(instance_id, migration_id)
|
||||
except (exception.ComputeResourceNotFound,
|
||||
exception.NovaClientError) as e:
|
||||
except (
|
||||
exception.ComputeResourceNotFound,
|
||||
exception.NovaClientError,
|
||||
) as e:
|
||||
# Note: Does not return from here, as abort request can't be
|
||||
# accepted but migration still going on.
|
||||
LOG.exception(e)
|
||||
@@ -1039,11 +1062,13 @@ class NovaHelper:
|
||||
except exception.ComputeResourceNotFound:
|
||||
LOG.debug(
|
||||
"Instance %s not found, can't abort live migrate",
|
||||
instance_id
|
||||
instance_id,
|
||||
)
|
||||
return False
|
||||
if (instance.task_state is None and
|
||||
instance.status in ['ACTIVE', 'ERROR']):
|
||||
if instance.task_state is None and instance.status in [
|
||||
'ACTIVE',
|
||||
'ERROR',
|
||||
]:
|
||||
break
|
||||
time.sleep(2)
|
||||
retry -= 1
|
||||
|
||||
@@ -24,10 +24,10 @@ LOG = log.getLogger(__name__)
|
||||
def init_oslo_service_backend():
|
||||
if eventlet_helper.is_patched():
|
||||
backend.init_backend(backend.BackendType.EVENTLET)
|
||||
LOG.warning(
|
||||
"Service is starting with Eventlet based service backend.")
|
||||
LOG.warning("Service is starting with Eventlet based service backend.")
|
||||
else:
|
||||
backend.init_backend(backend.BackendType.THREADING)
|
||||
LOG.warning(
|
||||
"Service is starting with Threading based service backend. "
|
||||
"This is an experimental feature, do not use it in production.")
|
||||
"This is an experimental feature, do not use it in production."
|
||||
)
|
||||
|
||||
@@ -24,7 +24,6 @@ LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PlacementHelper:
|
||||
|
||||
def __init__(self, osc=None):
|
||||
""":param osc: an OpenStackClients instance"""
|
||||
self.osc = osc if osc else clients.OpenStackClients()
|
||||
@@ -83,8 +82,10 @@ class PlacementHelper:
|
||||
if resp.status_code == HTTPStatus.OK:
|
||||
json = resp.json()
|
||||
return json['inventories']
|
||||
msg = ("Failed to get resource provider %(rp_uuid)s inventories. "
|
||||
"Got %(status_code)d: %(err_text)s.")
|
||||
msg = (
|
||||
"Failed to get resource provider %(rp_uuid)s inventories. "
|
||||
"Got %(status_code)d: %(err_text)s."
|
||||
)
|
||||
args = {
|
||||
'rp_uuid': rp_uuid,
|
||||
'status_code': resp.status_code,
|
||||
@@ -103,8 +104,10 @@ class PlacementHelper:
|
||||
if resp.status_code == HTTPStatus.OK:
|
||||
json = resp.json()
|
||||
return json['traits']
|
||||
msg = ("Failed to get resource provider %(rp_uuid)s traits. "
|
||||
"Got %(status_code)d: %(err_text)s.")
|
||||
msg = (
|
||||
"Failed to get resource provider %(rp_uuid)s traits. "
|
||||
"Got %(status_code)d: %(err_text)s."
|
||||
)
|
||||
args = {
|
||||
'rp_uuid': rp_uuid,
|
||||
'status_code': resp.status_code,
|
||||
@@ -124,8 +127,10 @@ class PlacementHelper:
|
||||
if resp.status_code == HTTPStatus.OK:
|
||||
json = resp.json()
|
||||
return json['allocations']
|
||||
msg = ("Failed to get allocations for consumer %(c_uuid)s. "
|
||||
"Got %(status_code)d: %(err_text)s.")
|
||||
msg = (
|
||||
"Failed to get allocations for consumer %(c_uuid)s. "
|
||||
"Got %(status_code)d: %(err_text)s."
|
||||
)
|
||||
args = {
|
||||
'c_uuid': consumer_uuid,
|
||||
'status_code': resp.status_code,
|
||||
@@ -145,8 +150,10 @@ class PlacementHelper:
|
||||
if resp.status_code == HTTPStatus.OK:
|
||||
json = resp.json()
|
||||
return json['usages']
|
||||
msg = ("Failed to get resource provider %(rp_uuid)s usages. "
|
||||
"Got %(status_code)d: %(err_text)s.")
|
||||
msg = (
|
||||
"Failed to get resource provider %(rp_uuid)s usages. "
|
||||
"Got %(status_code)d: %(err_text)s."
|
||||
)
|
||||
args = {
|
||||
'rp_uuid': rp_uuid,
|
||||
'status_code': resp.status_code,
|
||||
@@ -176,7 +183,9 @@ class PlacementHelper:
|
||||
'status_code': resp.status_code,
|
||||
'err_text': self.get_error_msg(resp),
|
||||
}
|
||||
msg = ("Failed to get allocation candidates from placement "
|
||||
"API for resources: %(resource_request)s\n"
|
||||
"Got %(status_code)d: %(err_text)s.")
|
||||
msg = (
|
||||
"Failed to get allocation candidates from placement "
|
||||
"API for resources: %(resource_request)s\n"
|
||||
"Got %(status_code)d: %(err_text)s."
|
||||
)
|
||||
LOG.error(msg, args)
|
||||
|
||||
@@ -22,46 +22,26 @@ rules = [
|
||||
name=ACTION % 'detail',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description='Retrieve a list of actions with detail.',
|
||||
operations=[
|
||||
{
|
||||
'path': '/v1/actions/detail',
|
||||
'method': 'GET'
|
||||
}
|
||||
]
|
||||
operations=[{'path': '/v1/actions/detail', 'method': 'GET'}],
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=ACTION % 'get',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description='Retrieve information about a given action.',
|
||||
operations=[
|
||||
{
|
||||
'path': '/v1/actions/{action_id}',
|
||||
'method': 'GET'
|
||||
}
|
||||
]
|
||||
operations=[{'path': '/v1/actions/{action_id}', 'method': 'GET'}],
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=ACTION % 'get_all',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description='Retrieve a list of all actions.',
|
||||
operations=[
|
||||
{
|
||||
'path': '/v1/actions',
|
||||
'method': 'GET'
|
||||
}
|
||||
]
|
||||
operations=[{'path': '/v1/actions', 'method': 'GET'}],
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=ACTION % 'update',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description='Update an action.',
|
||||
operations=[
|
||||
{
|
||||
'path': '/v1/actions/{action_id}',
|
||||
'method': 'PATCH'
|
||||
}
|
||||
]
|
||||
)
|
||||
operations=[{'path': '/v1/actions/{action_id}', 'method': 'PATCH'}],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -23,55 +23,36 @@ rules = [
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description='Delete an action plan.',
|
||||
operations=[
|
||||
{
|
||||
'path': '/v1/action_plans/{action_plan_uuid}',
|
||||
'method': 'DELETE'
|
||||
}
|
||||
]
|
||||
{'path': '/v1/action_plans/{action_plan_uuid}', 'method': 'DELETE'}
|
||||
],
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=ACTION_PLAN % 'detail',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description='Retrieve a list of action plans with detail.',
|
||||
operations=[
|
||||
{
|
||||
'path': '/v1/action_plans/detail',
|
||||
'method': 'GET'
|
||||
}
|
||||
]
|
||||
operations=[{'path': '/v1/action_plans/detail', 'method': 'GET'}],
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=ACTION_PLAN % 'get',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description='Get an action plan.',
|
||||
operations=[
|
||||
{
|
||||
'path': '/v1/action_plans/{action_plan_id}',
|
||||
'method': 'GET'
|
||||
}
|
||||
]
|
||||
{'path': '/v1/action_plans/{action_plan_id}', 'method': 'GET'}
|
||||
],
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=ACTION_PLAN % 'get_all',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description='Get all action plans.',
|
||||
operations=[
|
||||
{
|
||||
'path': '/v1/action_plans',
|
||||
'method': 'GET'
|
||||
}
|
||||
]
|
||||
operations=[{'path': '/v1/action_plans', 'method': 'GET'}],
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=ACTION_PLAN % 'update',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description='Update an action plans.',
|
||||
operations=[
|
||||
{
|
||||
'path': '/v1/action_plans/{action_plan_uuid}',
|
||||
'method': 'PATCH'
|
||||
}
|
||||
]
|
||||
{'path': '/v1/action_plans/{action_plan_uuid}', 'method': 'PATCH'}
|
||||
],
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=ACTION_PLAN % 'start',
|
||||
@@ -80,10 +61,10 @@ rules = [
|
||||
operations=[
|
||||
{
|
||||
'path': '/v1/action_plans/{action_plan_uuid}/start',
|
||||
'method': 'POST'
|
||||
'method': 'POST',
|
||||
}
|
||||
]
|
||||
)
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -22,68 +22,38 @@ rules = [
|
||||
name=AUDIT % 'create',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description='Create a new audit.',
|
||||
operations=[
|
||||
{
|
||||
'path': '/v1/audits',
|
||||
'method': 'POST'
|
||||
}
|
||||
]
|
||||
operations=[{'path': '/v1/audits', 'method': 'POST'}],
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=AUDIT % 'delete',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description='Delete an audit.',
|
||||
operations=[
|
||||
{
|
||||
'path': '/v1/audits/{audit_uuid}',
|
||||
'method': 'DELETE'
|
||||
}
|
||||
]
|
||||
operations=[{'path': '/v1/audits/{audit_uuid}', 'method': 'DELETE'}],
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=AUDIT % 'detail',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description='Retrieve audit list with details.',
|
||||
operations=[
|
||||
{
|
||||
'path': '/v1/audits/detail',
|
||||
'method': 'GET'
|
||||
}
|
||||
]
|
||||
operations=[{'path': '/v1/audits/detail', 'method': 'GET'}],
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=AUDIT % 'get',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description='Get an audit.',
|
||||
operations=[
|
||||
{
|
||||
'path': '/v1/audits/{audit_uuid}',
|
||||
'method': 'GET'
|
||||
}
|
||||
]
|
||||
operations=[{'path': '/v1/audits/{audit_uuid}', 'method': 'GET'}],
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=AUDIT % 'get_all',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description='Get all audits.',
|
||||
operations=[
|
||||
{
|
||||
'path': '/v1/audits',
|
||||
'method': 'GET'
|
||||
}
|
||||
]
|
||||
operations=[{'path': '/v1/audits', 'method': 'GET'}],
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=AUDIT % 'update',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description='Update an audit.',
|
||||
operations=[
|
||||
{
|
||||
'path': '/v1/audits/{audit_uuid}',
|
||||
'method': 'PATCH'
|
||||
}
|
||||
]
|
||||
)
|
||||
operations=[{'path': '/v1/audits/{audit_uuid}', 'method': 'PATCH'}],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -22,12 +22,7 @@ rules = [
|
||||
name=AUDIT_TEMPLATE % 'create',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description='Create an audit template.',
|
||||
operations=[
|
||||
{
|
||||
'path': '/v1/audit_templates',
|
||||
'method': 'POST'
|
||||
}
|
||||
]
|
||||
operations=[{'path': '/v1/audit_templates', 'method': 'POST'}],
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=AUDIT_TEMPLATE % 'delete',
|
||||
@@ -36,20 +31,15 @@ rules = [
|
||||
operations=[
|
||||
{
|
||||
'path': '/v1/audit_templates/{audit_template_uuid}',
|
||||
'method': 'DELETE'
|
||||
'method': 'DELETE',
|
||||
}
|
||||
]
|
||||
],
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=AUDIT_TEMPLATE % 'detail',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description='Retrieve a list of audit templates with details.',
|
||||
operations=[
|
||||
{
|
||||
'path': '/v1/audit_templates/detail',
|
||||
'method': 'GET'
|
||||
}
|
||||
]
|
||||
operations=[{'path': '/v1/audit_templates/detail', 'method': 'GET'}],
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=AUDIT_TEMPLATE % 'get',
|
||||
@@ -58,20 +48,15 @@ rules = [
|
||||
operations=[
|
||||
{
|
||||
'path': '/v1/audit_templates/{audit_template_uuid}',
|
||||
'method': 'GET'
|
||||
'method': 'GET',
|
||||
}
|
||||
]
|
||||
],
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=AUDIT_TEMPLATE % 'get_all',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description='Get a list of all audit templates.',
|
||||
operations=[
|
||||
{
|
||||
'path': '/v1/audit_templates',
|
||||
'method': 'GET'
|
||||
}
|
||||
]
|
||||
operations=[{'path': '/v1/audit_templates', 'method': 'GET'}],
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=AUDIT_TEMPLATE % 'update',
|
||||
@@ -80,10 +65,10 @@ rules = [
|
||||
operations=[
|
||||
{
|
||||
'path': '/v1/audit_templates/{audit_template_uuid}',
|
||||
'method': 'PATCH'
|
||||
'method': 'PATCH',
|
||||
}
|
||||
]
|
||||
)
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -19,13 +19,9 @@ ALWAYS_DENY = '!'
|
||||
|
||||
rules = [
|
||||
policy.RuleDefault(
|
||||
name='admin_api',
|
||||
check_str=ROLE_ADMIN_OR_ADMINISTRATOR
|
||||
name='admin_api', check_str=ROLE_ADMIN_OR_ADMINISTRATOR
|
||||
),
|
||||
policy.RuleDefault(
|
||||
name='show_password',
|
||||
check_str=ALWAYS_DENY
|
||||
)
|
||||
policy.RuleDefault(name='show_password', check_str=ALWAYS_DENY),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -24,13 +24,8 @@ rules = [
|
||||
name=DATA_MODEL % 'get_all',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description='List data model.',
|
||||
operations=[
|
||||
{
|
||||
'path': '/v1/data_model',
|
||||
'method': 'GET'
|
||||
}
|
||||
]
|
||||
),
|
||||
operations=[{'path': '/v1/data_model', 'method': 'GET'}],
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -22,35 +22,20 @@ rules = [
|
||||
name=GOAL % 'detail',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description='Retrieve a list of goals with detail.',
|
||||
operations=[
|
||||
{
|
||||
'path': '/v1/goals/detail',
|
||||
'method': 'GET'
|
||||
}
|
||||
]
|
||||
operations=[{'path': '/v1/goals/detail', 'method': 'GET'}],
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=GOAL % 'get',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description='Get a goal.',
|
||||
operations=[
|
||||
{
|
||||
'path': '/v1/goals/{goal_uuid}',
|
||||
'method': 'GET'
|
||||
}
|
||||
]
|
||||
operations=[{'path': '/v1/goals/{goal_uuid}', 'method': 'GET'}],
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=GOAL % 'get_all',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description='Get all goals.',
|
||||
operations=[
|
||||
{
|
||||
'path': '/v1/goals',
|
||||
'method': 'GET'
|
||||
}
|
||||
]
|
||||
)
|
||||
operations=[{'path': '/v1/goals', 'method': 'GET'}],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -25,12 +25,7 @@ rules = [
|
||||
name=SCORING_ENGINE % 'detail',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description='List scoring engines with details.',
|
||||
operations=[
|
||||
{
|
||||
'path': '/v1/scoring_engines/detail',
|
||||
'method': 'GET'
|
||||
}
|
||||
]
|
||||
operations=[{'path': '/v1/scoring_engines/detail', 'method': 'GET'}],
|
||||
),
|
||||
# FIXME(lbragstad): Find someone from watcher to double check this
|
||||
# information. This API isn't listed in watcher's API reference
|
||||
@@ -42,9 +37,9 @@ rules = [
|
||||
operations=[
|
||||
{
|
||||
'path': '/v1/scoring_engines/{scoring_engine_id}',
|
||||
'method': 'GET'
|
||||
'method': 'GET',
|
||||
}
|
||||
]
|
||||
],
|
||||
),
|
||||
# FIXME(lbragstad): Find someone from watcher to double check this
|
||||
# information. This API isn't listed in watcher's API reference
|
||||
@@ -53,13 +48,8 @@ rules = [
|
||||
name=SCORING_ENGINE % 'get_all',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description='Get all scoring engines.',
|
||||
operations=[
|
||||
{
|
||||
'path': '/v1/scoring_engines',
|
||||
'method': 'GET'
|
||||
}
|
||||
]
|
||||
)
|
||||
operations=[{'path': '/v1/scoring_engines', 'method': 'GET'}],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -22,34 +22,19 @@ rules = [
|
||||
name=SERVICE % 'detail',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description='List services with detail.',
|
||||
operations=[
|
||||
{
|
||||
'path': '/v1/services/',
|
||||
'method': 'GET'
|
||||
}
|
||||
]
|
||||
operations=[{'path': '/v1/services/', 'method': 'GET'}],
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=SERVICE % 'get',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description='Get a specific service.',
|
||||
operations=[
|
||||
{
|
||||
'path': '/v1/services/{service_id}',
|
||||
'method': 'GET'
|
||||
}
|
||||
]
|
||||
operations=[{'path': '/v1/services/{service_id}', 'method': 'GET'}],
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=SERVICE % 'get_all',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description='List all services.',
|
||||
operations=[
|
||||
{
|
||||
'path': '/v1/services/',
|
||||
'method': 'GET'
|
||||
}
|
||||
]
|
||||
operations=[{'path': '/v1/services/', 'method': 'GET'}],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@@ -22,46 +22,30 @@ rules = [
|
||||
name=STRATEGY % 'detail',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description='List strategies with detail.',
|
||||
operations=[
|
||||
{
|
||||
'path': '/v1/strategies/detail',
|
||||
'method': 'GET'
|
||||
}
|
||||
]
|
||||
operations=[{'path': '/v1/strategies/detail', 'method': 'GET'}],
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=STRATEGY % 'get',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description='Get a strategy.',
|
||||
operations=[
|
||||
{
|
||||
'path': '/v1/strategies/{strategy_uuid}',
|
||||
'method': 'GET'
|
||||
}
|
||||
]
|
||||
{'path': '/v1/strategies/{strategy_uuid}', 'method': 'GET'}
|
||||
],
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=STRATEGY % 'get_all',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description='List all strategies.',
|
||||
operations=[
|
||||
{
|
||||
'path': '/v1/strategies',
|
||||
'method': 'GET'
|
||||
}
|
||||
]
|
||||
operations=[{'path': '/v1/strategies', 'method': 'GET'}],
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=STRATEGY % 'state',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description='Get state of strategy.',
|
||||
operations=[
|
||||
{
|
||||
'path': '/v1/strategies{strategy_uuid}/state',
|
||||
'method': 'GET'
|
||||
}
|
||||
]
|
||||
)
|
||||
{'path': '/v1/strategies{strategy_uuid}/state', 'method': 'GET'}
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -32,69 +32,77 @@ CONF = cfg.CONF
|
||||
# oslo policy support change policy rule dynamically.
|
||||
# at present, policy.enforce will reload the policy rules when it checks
|
||||
# the policy files have been touched.
|
||||
def init(policy_file=None, rules=None,
|
||||
default_rule=None, use_conf=True, overwrite=True):
|
||||
def init(
|
||||
policy_file=None,
|
||||
rules=None,
|
||||
default_rule=None,
|
||||
use_conf=True,
|
||||
overwrite=True,
|
||||
):
|
||||
"""Init an Enforcer class.
|
||||
|
||||
:param policy_file: Custom policy file to use, if none is
|
||||
specified, ``conf.policy_file`` will be
|
||||
used.
|
||||
:param rules: Default dictionary / Rules to use. It will be
|
||||
considered just in the first instantiation. If
|
||||
:meth:`load_rules` with ``force_reload=True``,
|
||||
:meth:`clear` or :meth:`set_rules` with
|
||||
``overwrite=True`` is called this will be overwritten.
|
||||
:param default_rule: Default rule to use, conf.default_rule will
|
||||
be used if none is specified.
|
||||
:param use_conf: Whether to load rules from cache or config file.
|
||||
:param overwrite: Whether to overwrite existing rules when reload rules
|
||||
from config file.
|
||||
:param policy_file: Custom policy file to use, if none is
|
||||
specified, ``conf.policy_file`` will be
|
||||
used.
|
||||
:param rules: Default dictionary / Rules to use. It will be
|
||||
considered just in the first instantiation. If
|
||||
:meth:`load_rules` with ``force_reload=True``,
|
||||
:meth:`clear` or :meth:`set_rules` with
|
||||
``overwrite=True`` is called this will be overwritten.
|
||||
:param default_rule: Default rule to use, conf.default_rule will
|
||||
be used if none is specified.
|
||||
:param use_conf: Whether to load rules from cache or config file.
|
||||
:param overwrite: Whether to overwrite existing rules when reload rules
|
||||
from config file.
|
||||
"""
|
||||
global _ENFORCER
|
||||
if not _ENFORCER:
|
||||
# https://docs.openstack.org/oslo.policy/latest/admin/index.html
|
||||
_ENFORCER = policy.Enforcer(CONF,
|
||||
policy_file=policy_file,
|
||||
rules=rules,
|
||||
default_rule=default_rule,
|
||||
use_conf=use_conf,
|
||||
overwrite=overwrite)
|
||||
_ENFORCER = policy.Enforcer(
|
||||
CONF,
|
||||
policy_file=policy_file,
|
||||
rules=rules,
|
||||
default_rule=default_rule,
|
||||
use_conf=use_conf,
|
||||
overwrite=overwrite,
|
||||
)
|
||||
_ENFORCER.register_defaults(policies.list_rules())
|
||||
return _ENFORCER
|
||||
|
||||
|
||||
def enforce(context, rule=None, target=None,
|
||||
do_raise=True, exc=None, *args, **kwargs):
|
||||
def enforce(
|
||||
context, rule=None, target=None, do_raise=True, exc=None, *args, **kwargs
|
||||
):
|
||||
"""Checks authorization of a rule against the target and credentials.
|
||||
|
||||
:param dict context: As much information about the user performing the
|
||||
action as possible.
|
||||
:param rule: The rule to evaluate.
|
||||
:param dict target: As much information about the object being operated
|
||||
on as possible.
|
||||
:param do_raise: Whether to raise an exception or not if check
|
||||
fails.
|
||||
:param exc: Class of the exception to raise if the check fails.
|
||||
Any remaining arguments passed to :meth:`enforce` (both
|
||||
positional and keyword arguments) will be passed to
|
||||
the exception class. If not specified,
|
||||
:class:`PolicyNotAuthorized` will be used.
|
||||
:param dict context: As much information about the user performing the
|
||||
action as possible.
|
||||
:param rule: The rule to evaluate.
|
||||
:param dict target: As much information about the object being operated
|
||||
on as possible.
|
||||
:param do_raise: Whether to raise an exception or not if check
|
||||
fails.
|
||||
:param exc: Class of the exception to raise if the check fails.
|
||||
Any remaining arguments passed to :meth:`enforce` (both
|
||||
positional and keyword arguments) will be passed to
|
||||
the exception class. If not specified,
|
||||
:class:`PolicyNotAuthorized` will be used.
|
||||
|
||||
:return: ``False`` if the policy does not allow the action and `exc` is
|
||||
not provided; otherwise, returns a value that evaluates to
|
||||
``True``. Note: for rules using the "case" expression, this
|
||||
``True`` value will be the specified string from the
|
||||
expression.
|
||||
:return: ``False`` if the policy does not allow the action and `exc` is
|
||||
not provided; otherwise, returns a value that evaluates to
|
||||
``True``. Note: for rules using the "case" expression, this
|
||||
``True`` value will be the specified string from the
|
||||
expression.
|
||||
"""
|
||||
enforcer = init()
|
||||
credentials = context.to_dict()
|
||||
if not exc:
|
||||
exc = exception.PolicyNotAuthorized
|
||||
if target is None:
|
||||
target = {'project_id': context.project_id,
|
||||
'user_id': context.user_id}
|
||||
return enforcer.enforce(rule, target, credentials,
|
||||
do_raise=do_raise, exc=exc, *args, **kwargs)
|
||||
target = {'project_id': context.project_id, 'user_id': context.user_id}
|
||||
return enforcer.enforce(
|
||||
rule, target, credentials, do_raise=do_raise, exc=exc, *args, **kwargs
|
||||
)
|
||||
|
||||
|
||||
def get_enforcer():
|
||||
|
||||
@@ -42,9 +42,7 @@ TRANSPORT = None
|
||||
NOTIFICATION_TRANSPORT = None
|
||||
NOTIFIER = None
|
||||
|
||||
ALLOWED_EXMODS = [
|
||||
exception.__name__,
|
||||
]
|
||||
ALLOWED_EXMODS = [exception.__name__]
|
||||
EXTRA_EXMODS = []
|
||||
|
||||
|
||||
@@ -54,18 +52,20 @@ JsonPayloadSerializer = messaging.JsonPayloadSerializer
|
||||
def init(conf):
|
||||
global TRANSPORT, NOTIFICATION_TRANSPORT, NOTIFIER
|
||||
exmods = get_allowed_exmods()
|
||||
TRANSPORT = messaging.get_rpc_transport(
|
||||
conf, allowed_remote_exmods=exmods)
|
||||
TRANSPORT = messaging.get_rpc_transport(conf, allowed_remote_exmods=exmods)
|
||||
NOTIFICATION_TRANSPORT = messaging.get_notification_transport(
|
||||
conf, allowed_remote_exmods=exmods)
|
||||
conf, allowed_remote_exmods=exmods
|
||||
)
|
||||
|
||||
serializer = RequestContextSerializer(JsonPayloadSerializer())
|
||||
if not conf.notification_level:
|
||||
NOTIFIER = messaging.Notifier(
|
||||
NOTIFICATION_TRANSPORT, serializer=serializer, driver='noop')
|
||||
NOTIFICATION_TRANSPORT, serializer=serializer, driver='noop'
|
||||
)
|
||||
else:
|
||||
NOTIFIER = messaging.Notifier(NOTIFICATION_TRANSPORT,
|
||||
serializer=serializer)
|
||||
NOTIFIER = messaging.Notifier(
|
||||
NOTIFICATION_TRANSPORT, serializer=serializer
|
||||
)
|
||||
|
||||
|
||||
def initialized():
|
||||
@@ -98,7 +98,6 @@ def get_allowed_exmods():
|
||||
|
||||
|
||||
class RequestContextSerializer(messaging.Serializer):
|
||||
|
||||
def __init__(self, base):
|
||||
self._base = base
|
||||
|
||||
@@ -123,10 +122,7 @@ def get_client(target, version_cap=None, serializer=None):
|
||||
assert TRANSPORT is not None
|
||||
serializer = RequestContextSerializer(serializer)
|
||||
return messaging.get_rpc_client(
|
||||
TRANSPORT,
|
||||
target,
|
||||
version_cap=version_cap,
|
||||
serializer=serializer
|
||||
TRANSPORT, target, version_cap=version_cap, serializer=serializer
|
||||
)
|
||||
|
||||
|
||||
@@ -139,7 +135,7 @@ def get_server(target, endpoints, serializer=None):
|
||||
target,
|
||||
endpoints,
|
||||
serializer=serializer,
|
||||
access_policy=access_policy
|
||||
access_policy=access_policy,
|
||||
)
|
||||
|
||||
|
||||
@@ -152,7 +148,7 @@ def get_notification_listener(targets, endpoints, serializer=None, pool=None):
|
||||
endpoints,
|
||||
allow_requeue=False,
|
||||
pool=pool,
|
||||
serializer=serializer
|
||||
serializer=serializer,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -25,13 +25,12 @@ from watcher.common import executor
|
||||
|
||||
job_events = events
|
||||
|
||||
executors = {
|
||||
'default': executor.APSchedulerThreadPoolExecutor(),
|
||||
}
|
||||
executors = {'default': executor.APSchedulerThreadPoolExecutor()}
|
||||
|
||||
|
||||
class BackgroundSchedulerService(
|
||||
service.ServiceBase, background.BackgroundScheduler):
|
||||
service.ServiceBase, background.BackgroundScheduler
|
||||
):
|
||||
def __init__(self, gconfig=None, **options):
|
||||
if options is None:
|
||||
options = {'executors': executors}
|
||||
@@ -48,8 +47,9 @@ class BackgroundSchedulerService(
|
||||
super()._main_loop()
|
||||
|
||||
def add_job(self, *args, **kwargs):
|
||||
executor.log_executor_stats(executors['default'].executor,
|
||||
name="background-scheduler-pool")
|
||||
executor.log_executor_stats(
|
||||
executors['default'].executor, name="background-scheduler-pool"
|
||||
)
|
||||
return super().add_job(*args, **kwargs)
|
||||
|
||||
def start(self):
|
||||
|
||||
@@ -43,13 +43,17 @@ from watcher.objects import fields as wfields
|
||||
|
||||
|
||||
NOTIFICATION_OPTS = [
|
||||
cfg.StrOpt('notification_level',
|
||||
choices=[''] + list(wfields.NotificationPriority.ALL),
|
||||
default=wfields.NotificationPriority.INFO,
|
||||
help=_('Specifies the minimum level for which to send '
|
||||
'notifications. If not set, no notifications will '
|
||||
'be sent. The default is for this option to be at the '
|
||||
'`INFO` level.'))
|
||||
cfg.StrOpt(
|
||||
'notification_level',
|
||||
choices=[''] + list(wfields.NotificationPriority.ALL),
|
||||
default=wfields.NotificationPriority.INFO,
|
||||
help=_(
|
||||
'Specifies the minimum level for which to send '
|
||||
'notifications. If not set, no notifications will '
|
||||
'be sent. The default is for this option to be at the '
|
||||
'`INFO` level.'
|
||||
),
|
||||
)
|
||||
]
|
||||
cfg.CONF.register_opts(NOTIFICATION_OPTS)
|
||||
|
||||
@@ -57,12 +61,20 @@ cfg.CONF.register_opts(NOTIFICATION_OPTS)
|
||||
CONF = cfg.CONF
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
_DEFAULT_LOG_LEVELS = ['amqp=WARN', 'amqplib=WARN', 'qpid.messaging=INFO',
|
||||
'oslo.messaging=INFO', 'sqlalchemy=WARN',
|
||||
'keystoneclient=INFO', 'stevedore=INFO',
|
||||
'eventlet.wsgi.server=WARN', 'iso8601=WARN',
|
||||
'requests=WARN', 'neutronclient=WARN',
|
||||
'apscheduler=WARN']
|
||||
_DEFAULT_LOG_LEVELS = [
|
||||
'amqp=WARN',
|
||||
'amqplib=WARN',
|
||||
'qpid.messaging=INFO',
|
||||
'oslo.messaging=INFO',
|
||||
'sqlalchemy=WARN',
|
||||
'keystoneclient=INFO',
|
||||
'stevedore=INFO',
|
||||
'eventlet.wsgi.server=WARN',
|
||||
'iso8601=WARN',
|
||||
'requests=WARN',
|
||||
'neutronclient=WARN',
|
||||
'apscheduler=WARN',
|
||||
]
|
||||
|
||||
Singleton = service.Singleton
|
||||
|
||||
@@ -78,13 +90,16 @@ class WSGIService(service.ServiceBase):
|
||||
"""
|
||||
self.service_name = service_name
|
||||
self.app = app.VersionSelectorApplication()
|
||||
self.workers = (CONF.api.workers or
|
||||
processutils.get_worker_count())
|
||||
self.server = wsgi.Server(CONF, self.service_name, self.app,
|
||||
host=CONF.api.host,
|
||||
port=CONF.api.port,
|
||||
use_ssl=use_ssl,
|
||||
logger_name=self.service_name)
|
||||
self.workers = CONF.api.workers or processutils.get_worker_count()
|
||||
self.server = wsgi.Server(
|
||||
CONF,
|
||||
self.service_name,
|
||||
self.app,
|
||||
host=CONF.api.host,
|
||||
port=CONF.api.port,
|
||||
use_ssl=use_ssl,
|
||||
logger_name=self.service_name,
|
||||
)
|
||||
|
||||
def start(self):
|
||||
"""Start serving this service using loaded configuration"""
|
||||
@@ -104,7 +119,6 @@ class WSGIService(service.ServiceBase):
|
||||
|
||||
|
||||
class ServiceHeartbeat(scheduling.BackgroundSchedulerService):
|
||||
|
||||
service_name = None
|
||||
|
||||
def __init__(self, gconfig=None, service_name=None, **kwargs):
|
||||
@@ -117,8 +131,9 @@ class ServiceHeartbeat(scheduling.BackgroundSchedulerService):
|
||||
def send_beat(self):
|
||||
host = CONF.host
|
||||
watcher_list = objects.Service.list(
|
||||
self.context, filters={'name': ServiceHeartbeat.service_name,
|
||||
'host': host})
|
||||
self.context,
|
||||
filters={'name': ServiceHeartbeat.service_name, 'host': host},
|
||||
)
|
||||
if watcher_list:
|
||||
watcher_service = watcher_list[0]
|
||||
watcher_service.last_seen_up = timeutils.utcnow()
|
||||
@@ -130,8 +145,12 @@ class ServiceHeartbeat(scheduling.BackgroundSchedulerService):
|
||||
watcher_service.create()
|
||||
|
||||
def add_heartbeat_job(self):
|
||||
self.add_job(self.send_beat, 'interval', seconds=60,
|
||||
next_run_time=datetime.datetime.now())
|
||||
self.add_job(
|
||||
self.send_beat,
|
||||
'interval',
|
||||
seconds=60,
|
||||
next_run_time=datetime.datetime.now(),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_service_name(cls):
|
||||
@@ -157,7 +176,6 @@ class ServiceHeartbeat(scheduling.BackgroundSchedulerService):
|
||||
|
||||
|
||||
class Service(service.ServiceBase):
|
||||
|
||||
API_VERSION = '1.0'
|
||||
|
||||
def __init__(self, manager_class):
|
||||
@@ -175,7 +193,8 @@ class Service(service.ServiceBase):
|
||||
self.service_name = self.manager.service_name
|
||||
if self.service_name:
|
||||
self.heartbeat = ServiceHeartbeat(
|
||||
service_name=self.manager.service_name)
|
||||
service_name=self.manager.service_name
|
||||
)
|
||||
|
||||
self.conductor_endpoints = [
|
||||
ep(self) for ep in self.manager.conductor_endpoints
|
||||
@@ -189,7 +208,8 @@ class Service(service.ServiceBase):
|
||||
|
||||
if self.conductor_topic and self.conductor_endpoints:
|
||||
self.conductor_topic_handler = self.build_topic_handler(
|
||||
self.conductor_topic, self.conductor_endpoints)
|
||||
self.conductor_topic, self.conductor_endpoints
|
||||
)
|
||||
if self.notification_topics and self.notification_endpoints:
|
||||
self.notification_handler = self.build_notification_handler(
|
||||
self.notification_topics, self.notification_endpoints
|
||||
@@ -199,12 +219,10 @@ class Service(service.ServiceBase):
|
||||
def conductor_client(self):
|
||||
if self._conductor_client is None:
|
||||
target = messaging.Target(
|
||||
topic=self.conductor_topic,
|
||||
version=self.API_VERSION,
|
||||
topic=self.conductor_topic, version=self.API_VERSION
|
||||
)
|
||||
self._conductor_client = rpc.get_client(
|
||||
target,
|
||||
serializer=base.WatcherObjectSerializer()
|
||||
target, serializer=base.WatcherObjectSerializer()
|
||||
)
|
||||
return self._conductor_client
|
||||
|
||||
@@ -220,8 +238,7 @@ class Service(service.ServiceBase):
|
||||
version=self.api_version,
|
||||
)
|
||||
return rpc.get_server(
|
||||
target, endpoints,
|
||||
serializer=rpc.JsonPayloadSerializer()
|
||||
target, endpoints, serializer=rpc.JsonPayloadSerializer()
|
||||
)
|
||||
|
||||
def build_notification_handler(self, topic_names, endpoints=()):
|
||||
@@ -235,9 +252,10 @@ class Service(service.ServiceBase):
|
||||
targets.append(messaging.Target(**kwargs))
|
||||
|
||||
return rpc.get_notification_listener(
|
||||
targets, endpoints,
|
||||
targets,
|
||||
endpoints,
|
||||
serializer=rpc.JsonPayloadSerializer(),
|
||||
pool=CONF.host
|
||||
pool=CONF.host,
|
||||
)
|
||||
|
||||
def start(self):
|
||||
@@ -266,12 +284,14 @@ class Service(service.ServiceBase):
|
||||
|
||||
def check_api_version(self, ctx):
|
||||
api_manager_version = self.conductor_client.call(
|
||||
ctx, 'check_api_version', api_version=self.api_version)
|
||||
ctx, 'check_api_version', api_version=self.api_version
|
||||
)
|
||||
return api_manager_version
|
||||
|
||||
|
||||
class ServiceMonitoringBase(scheduling.BackgroundSchedulerService,
|
||||
metaclass=abc.ABCMeta):
|
||||
class ServiceMonitoringBase(
|
||||
scheduling.BackgroundSchedulerService, metaclass=abc.ABCMeta
|
||||
):
|
||||
"""Base Service to monitor the status of Watcher services.
|
||||
|
||||
This class is intended to be used as a base class to monitore the
|
||||
@@ -297,9 +317,15 @@ class ServiceMonitoringBase(scheduling.BackgroundSchedulerService,
|
||||
|
||||
def _am_i_leader(self, services):
|
||||
active_hosts = sorted(
|
||||
[s.host for s in services
|
||||
if (s.state == objects.service.ServiceStatus.ACTIVE and
|
||||
s.name == self.service_name)])
|
||||
[
|
||||
s.host
|
||||
for s in services
|
||||
if (
|
||||
s.state == objects.service.ServiceStatus.ACTIVE
|
||||
and s.name == self.service_name
|
||||
)
|
||||
]
|
||||
)
|
||||
if not active_hosts:
|
||||
LOG.info("No active services found for %s", self.service_name)
|
||||
self.last_leader = None
|
||||
@@ -309,10 +335,14 @@ class ServiceMonitoringBase(scheduling.BackgroundSchedulerService,
|
||||
if leader != self.last_leader:
|
||||
LOG.info(
|
||||
"Leader election completed for %s: %s -> %s. "
|
||||
"Selected as leader: %s", self.service_name, self.last_leader,
|
||||
leader, CONF.host == leader)
|
||||
"Selected as leader: %s",
|
||||
self.service_name,
|
||||
self.last_leader,
|
||||
leader,
|
||||
CONF.host == leader,
|
||||
)
|
||||
self.last_leader = leader
|
||||
return (CONF.host == leader)
|
||||
return CONF.host == leader
|
||||
|
||||
@abc.abstractmethod
|
||||
def monitor_services_status(self, context):
|
||||
@@ -320,9 +350,11 @@ class ServiceMonitoringBase(scheduling.BackgroundSchedulerService,
|
||||
|
||||
def get_service_status(self, context, service_id):
|
||||
watcher_service = objects.Service.get(context, service_id)
|
||||
last_heartbeat = (watcher_service.last_seen_up or
|
||||
watcher_service.updated_at or
|
||||
watcher_service.created_at)
|
||||
last_heartbeat = (
|
||||
watcher_service.last_seen_up
|
||||
or watcher_service.updated_at
|
||||
or watcher_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
|
||||
@@ -335,11 +367,16 @@ class ServiceMonitoringBase(scheduling.BackgroundSchedulerService,
|
||||
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': watcher_service.name,
|
||||
'host': watcher_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': watcher_service.name,
|
||||
'host': watcher_service.host,
|
||||
'lhb': str(last_heartbeat),
|
||||
'el': str(elapsed),
|
||||
},
|
||||
)
|
||||
return objects.service.ServiceStatus.FAILED
|
||||
|
||||
return objects.service.ServiceStatus.ACTIVE
|
||||
@@ -347,13 +384,18 @@ class ServiceMonitoringBase(scheduling.BackgroundSchedulerService,
|
||||
def start(self):
|
||||
"""Start service."""
|
||||
admin_context = context.make_context(is_admin=True)
|
||||
LOG.info('Starting service monitoring service for %s',
|
||||
self.service_name)
|
||||
self.add_job(self.monitor_services_status,
|
||||
name='service_status', trigger='interval',
|
||||
jobstore='default', args=[admin_context],
|
||||
next_run_time=datetime.datetime.now(),
|
||||
seconds=CONF.periodic_interval)
|
||||
LOG.info(
|
||||
'Starting service monitoring service for %s', self.service_name
|
||||
)
|
||||
self.add_job(
|
||||
self.monitor_services_status,
|
||||
name='service_status',
|
||||
trigger='interval',
|
||||
jobstore='default',
|
||||
args=[admin_context],
|
||||
next_run_time=datetime.datetime.now(),
|
||||
seconds=CONF.periodic_interval,
|
||||
)
|
||||
super().start()
|
||||
|
||||
def stop(self):
|
||||
@@ -379,13 +421,13 @@ def prepare_service(argv=(), conf=cfg.CONF):
|
||||
gmr_opts.set_defaults(conf)
|
||||
|
||||
config.parse_args(argv)
|
||||
cfg.set_defaults(_options.log_opts,
|
||||
default_log_levels=_DEFAULT_LOG_LEVELS)
|
||||
cfg.set_defaults(_options.log_opts, default_log_levels=_DEFAULT_LOG_LEVELS)
|
||||
config.set_lib_defaults()
|
||||
log.setup(conf, 'python-watcher')
|
||||
conf.log_opt_values(LOG, log.DEBUG)
|
||||
objects.register_all()
|
||||
|
||||
gmr.TextGuruMeditation.register_section(
|
||||
_('Plugins'), plugins_conf.show_plugins)
|
||||
_('Plugins'), plugins_conf.show_plugins
|
||||
)
|
||||
gmr.TextGuruMeditation.setup_autorun(version, conf=conf)
|
||||
|
||||
@@ -17,7 +17,6 @@ import abc
|
||||
|
||||
|
||||
class ServiceManager(metaclass=abc.ABCMeta):
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def service_name(self):
|
||||
|
||||
@@ -86,7 +86,9 @@ def safe_rstrip(value, chars=None):
|
||||
if not isinstance(value, str):
|
||||
LOG.warning(
|
||||
"Failed to remove trailing character. Returning original object."
|
||||
"Supplied object is not a string: %s,", value)
|
||||
"Supplied object is not a string: %s,",
|
||||
value,
|
||||
)
|
||||
return value
|
||||
|
||||
return value.rstrip(chars) or value
|
||||
@@ -105,8 +107,7 @@ def is_hostname_safe(hostname):
|
||||
|
||||
"""
|
||||
m = r'^[a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?$'
|
||||
return (isinstance(hostname, str) and
|
||||
(re.match(m, hostname) is not None))
|
||||
return isinstance(hostname, str) and (re.match(m, hostname) is not None)
|
||||
|
||||
|
||||
def get_cls_import_path(cls):
|
||||
@@ -130,8 +131,7 @@ def extend_with_default(validator_class):
|
||||
validator, properties, instance, schema
|
||||
)
|
||||
|
||||
return validators.extend(validator_class,
|
||||
{"properties": set_defaults})
|
||||
return validators.extend(validator_class, {"properties": set_defaults})
|
||||
|
||||
|
||||
# Parameter strict check extension as jsonschema doesn't support it
|
||||
@@ -154,7 +154,8 @@ def extend_with_strict_schema(validator_class):
|
||||
|
||||
|
||||
StrictDefaultValidatingDraft4Validator = extend_with_default(
|
||||
extend_with_strict_schema(validators.Draft4Validator))
|
||||
extend_with_strict_schema(validators.Draft4Validator)
|
||||
)
|
||||
|
||||
|
||||
Draft4Validator = validators.Draft4Validator
|
||||
@@ -189,6 +190,6 @@ def async_compat_call(f, *args, **kwargs):
|
||||
# to avoid lingering threads. For consistency, we'll convert eventlet
|
||||
# timeout exceptions to asyncio timeout errors.
|
||||
with eventlet.timeout.Timeout(
|
||||
seconds=timeout,
|
||||
exception=asyncio.TimeoutError(f"Timeout: {timeout}s")):
|
||||
seconds=timeout, exception=asyncio.TimeoutError(f"Timeout: {timeout}s")
|
||||
):
|
||||
return tpool.execute(tpool_wrapper)
|
||||
|
||||
@@ -16,29 +16,45 @@
|
||||
from oslo_config import cfg
|
||||
|
||||
|
||||
aetos_client = cfg.OptGroup(name='aetos_client',
|
||||
title='Configuration Options for Aetos',
|
||||
help="See https://docs.openstack.org/watcher/"
|
||||
"latest/datasources/aetos.html for "
|
||||
"details on how these options are used.")
|
||||
aetos_client = cfg.OptGroup(
|
||||
name='aetos_client',
|
||||
title='Configuration Options for Aetos',
|
||||
help="See https://docs.openstack.org/watcher/"
|
||||
"latest/datasources/aetos.html for "
|
||||
"details on how these options are used.",
|
||||
)
|
||||
|
||||
AETOS_CLIENT_OPTS = [
|
||||
cfg.StrOpt('interface',
|
||||
default='public',
|
||||
choices=['public', 'internal', 'admin',
|
||||
'publicURL', 'internalURL', 'adminURL'],
|
||||
help="Type of endpoint to use in keystoneclient."),
|
||||
cfg.StrOpt('region_name',
|
||||
help="Region in Identity service catalog to use for "
|
||||
"communication with the OpenStack service."),
|
||||
cfg.StrOpt('fqdn_label',
|
||||
default='fqdn',
|
||||
help="The label that Prometheus uses to store the fqdn of "
|
||||
"exporters. Defaults to 'fqdn'."),
|
||||
cfg.StrOpt('instance_uuid_label',
|
||||
default='resource',
|
||||
help="The label that Prometheus uses to store the uuid of "
|
||||
"OpenStack instances. Defaults to 'resource'."),
|
||||
cfg.StrOpt(
|
||||
'interface',
|
||||
default='public',
|
||||
choices=[
|
||||
'public',
|
||||
'internal',
|
||||
'admin',
|
||||
'publicURL',
|
||||
'internalURL',
|
||||
'adminURL',
|
||||
],
|
||||
help="Type of endpoint to use in keystoneclient.",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
'region_name',
|
||||
help="Region in Identity service catalog to use for "
|
||||
"communication with the OpenStack service.",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
'fqdn_label',
|
||||
default='fqdn',
|
||||
help="The label that Prometheus uses to store the fqdn of "
|
||||
"exporters. Defaults to 'fqdn'.",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
'instance_uuid_label',
|
||||
default='resource',
|
||||
help="The label that Prometheus uses to store the uuid of "
|
||||
"OpenStack instances. Defaults to 'resource'.",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -18,48 +18,56 @@
|
||||
from oslo_config import cfg
|
||||
|
||||
|
||||
api = cfg.OptGroup(name='api',
|
||||
title='Options for the Watcher API service')
|
||||
api = cfg.OptGroup(name='api', title='Options for the Watcher API service')
|
||||
|
||||
AUTH_OPTS = [
|
||||
cfg.BoolOpt('enable_authentication',
|
||||
default=True,
|
||||
help='This option enables or disables user authentication '
|
||||
'via keystone. Default value is True.'),
|
||||
cfg.BoolOpt(
|
||||
'enable_authentication',
|
||||
default=True,
|
||||
help='This option enables or disables user authentication '
|
||||
'via keystone. Default value is True.',
|
||||
)
|
||||
]
|
||||
|
||||
API_SERVICE_OPTS = [
|
||||
cfg.PortOpt('port',
|
||||
default=9322,
|
||||
help='The port for the watcher API server'),
|
||||
cfg.HostAddressOpt('host',
|
||||
default='127.0.0.1',
|
||||
help='The listen IP address for the watcher API server'
|
||||
),
|
||||
cfg.IntOpt('max_limit',
|
||||
default=1000,
|
||||
help='The maximum number of items returned in a single '
|
||||
'response from a collection resource'),
|
||||
cfg.IntOpt('workers',
|
||||
min=1,
|
||||
help='Number of workers for Watcher API service. '
|
||||
'The default is equal to the number of CPUs available '
|
||||
'if that can be determined, else a default worker '
|
||||
'count of 1 is returned.'),
|
||||
|
||||
cfg.BoolOpt('enable_ssl_api',
|
||||
default=False,
|
||||
help="Enable the integrated stand-alone API to service "
|
||||
"requests via HTTPS instead of HTTP. If there is a "
|
||||
"front-end service performing HTTPS offloading from "
|
||||
"the service, this option should be False; note, you "
|
||||
"will want to change public API endpoint to represent "
|
||||
"SSL termination URL with 'public_endpoint' option."),
|
||||
|
||||
cfg.BoolOpt('enable_webhooks_auth',
|
||||
default=True,
|
||||
help='This option enables or disables webhook request '
|
||||
'authentication via keystone. Default value is True.'),
|
||||
cfg.PortOpt(
|
||||
'port', default=9322, help='The port for the watcher API server'
|
||||
),
|
||||
cfg.HostAddressOpt(
|
||||
'host',
|
||||
default='127.0.0.1',
|
||||
help='The listen IP address for the watcher API server',
|
||||
),
|
||||
cfg.IntOpt(
|
||||
'max_limit',
|
||||
default=1000,
|
||||
help='The maximum number of items returned in a single '
|
||||
'response from a collection resource',
|
||||
),
|
||||
cfg.IntOpt(
|
||||
'workers',
|
||||
min=1,
|
||||
help='Number of workers for Watcher API service. '
|
||||
'The default is equal to the number of CPUs available '
|
||||
'if that can be determined, else a default worker '
|
||||
'count of 1 is returned.',
|
||||
),
|
||||
cfg.BoolOpt(
|
||||
'enable_ssl_api',
|
||||
default=False,
|
||||
help="Enable the integrated stand-alone API to service "
|
||||
"requests via HTTPS instead of HTTP. If there is a "
|
||||
"front-end service performing HTTPS offloading from "
|
||||
"the service, this option should be False; note, you "
|
||||
"will want to change public API endpoint to represent "
|
||||
"SSL termination URL with 'public_endpoint' option.",
|
||||
),
|
||||
cfg.BoolOpt(
|
||||
'enable_webhooks_auth',
|
||||
default=True,
|
||||
help='This option enables or disables webhook request '
|
||||
'authentication via keystone. Default value is True.',
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -18,40 +18,49 @@
|
||||
from oslo_config import cfg
|
||||
|
||||
|
||||
watcher_applier = cfg.OptGroup(name='watcher_applier',
|
||||
title='Options for the Applier messaging '
|
||||
'core')
|
||||
watcher_applier = cfg.OptGroup(
|
||||
name='watcher_applier', title='Options for the Applier messaging core'
|
||||
)
|
||||
|
||||
APPLIER_MANAGER_OPTS = [
|
||||
cfg.IntOpt('workers',
|
||||
default=1,
|
||||
min=1,
|
||||
required=True,
|
||||
help='Number of workers for applier, default value is 1.'),
|
||||
cfg.StrOpt('conductor_topic',
|
||||
default='watcher.applier.control',
|
||||
help='The topic name used for '
|
||||
'control events, this topic '
|
||||
'used for rpc call '),
|
||||
cfg.StrOpt('publisher_id',
|
||||
default='watcher.applier.api',
|
||||
help='The identifier used by watcher '
|
||||
'module on the message broker'),
|
||||
cfg.StrOpt('workflow_engine',
|
||||
default='taskflow',
|
||||
required=True,
|
||||
help='Select the engine to use to execute the workflow'),
|
||||
cfg.IntOpt(
|
||||
'workers',
|
||||
default=1,
|
||||
min=1,
|
||||
required=True,
|
||||
help='Number of workers for applier, default value is 1.',
|
||||
),
|
||||
cfg.StrOpt(
|
||||
'conductor_topic',
|
||||
default='watcher.applier.control',
|
||||
help='The topic name used for '
|
||||
'control events, this topic '
|
||||
'used for rpc call ',
|
||||
),
|
||||
cfg.StrOpt(
|
||||
'publisher_id',
|
||||
default='watcher.applier.api',
|
||||
help='The identifier used by watcher module on the message broker',
|
||||
),
|
||||
cfg.StrOpt(
|
||||
'workflow_engine',
|
||||
default='taskflow',
|
||||
required=True,
|
||||
help='Select the engine to use to execute the workflow',
|
||||
),
|
||||
]
|
||||
|
||||
APPLIER_OPTS = [
|
||||
cfg.BoolOpt('rollback_when_actionplan_failed',
|
||||
default=False,
|
||||
help='If set True, the failed actionplan will rollback '
|
||||
'when executing. Default value is False.',
|
||||
deprecated_for_removal=True,
|
||||
deprecated_since='2026.1',
|
||||
deprecated_reason='This feature does not work and is planned '
|
||||
'to be removed in future releases.'),
|
||||
cfg.BoolOpt(
|
||||
'rollback_when_actionplan_failed',
|
||||
default=False,
|
||||
help='If set True, the failed actionplan will rollback '
|
||||
'when executing. Default value is False.',
|
||||
deprecated_for_removal=True,
|
||||
deprecated_since='2026.1',
|
||||
deprecated_reason='This feature does not work and is planned '
|
||||
'to be removed in future releases.',
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@@ -62,5 +71,7 @@ def register_opts(conf):
|
||||
|
||||
|
||||
def list_opts():
|
||||
return [(watcher_applier, APPLIER_MANAGER_OPTS),
|
||||
(watcher_applier, APPLIER_OPTS)]
|
||||
return [
|
||||
(watcher_applier, APPLIER_MANAGER_OPTS),
|
||||
(watcher_applier, APPLIER_OPTS),
|
||||
]
|
||||
|
||||
@@ -18,21 +18,35 @@
|
||||
from oslo_config import cfg
|
||||
|
||||
|
||||
cinder_client = cfg.OptGroup(name='cinder_client',
|
||||
title='Configuration Options for Cinder')
|
||||
cinder_client = cfg.OptGroup(
|
||||
name='cinder_client', title='Configuration Options for Cinder'
|
||||
)
|
||||
|
||||
CINDER_CLIENT_OPTS = [
|
||||
cfg.StrOpt('api_version',
|
||||
default='3',
|
||||
help='Version of Cinder API to use in cinderclient.'),
|
||||
cfg.StrOpt('endpoint_type',
|
||||
default='publicURL',
|
||||
choices=['public', 'internal', 'admin',
|
||||
'publicURL', 'internalURL', 'adminURL'],
|
||||
help='Type of endpoint to use in cinderclient.'),
|
||||
cfg.StrOpt('region_name',
|
||||
help='Region in Identity service catalog to use for '
|
||||
'communication with the OpenStack service.')]
|
||||
cfg.StrOpt(
|
||||
'api_version',
|
||||
default='3',
|
||||
help='Version of Cinder API to use in cinderclient.',
|
||||
),
|
||||
cfg.StrOpt(
|
||||
'endpoint_type',
|
||||
default='publicURL',
|
||||
choices=[
|
||||
'public',
|
||||
'internal',
|
||||
'admin',
|
||||
'publicURL',
|
||||
'internalURL',
|
||||
'adminURL',
|
||||
],
|
||||
help='Type of endpoint to use in cinderclient.',
|
||||
),
|
||||
cfg.StrOpt(
|
||||
'region_name',
|
||||
help='Region in Identity service catalog to use for '
|
||||
'communication with the OpenStack service.',
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def register_opts(conf):
|
||||
|
||||
@@ -27,5 +27,10 @@ def register_opts(conf):
|
||||
|
||||
|
||||
def list_opts():
|
||||
return [(WATCHER_CLIENTS_AUTH, ka_loading.get_session_conf_options() +
|
||||
ka_loading.get_auth_common_conf_options())]
|
||||
return [
|
||||
(
|
||||
WATCHER_CLIENTS_AUTH,
|
||||
ka_loading.get_session_conf_options()
|
||||
+ ka_loading.get_auth_common_conf_options(),
|
||||
)
|
||||
]
|
||||
|
||||
@@ -16,14 +16,16 @@
|
||||
from oslo_config import cfg
|
||||
|
||||
|
||||
collector = cfg.OptGroup(name='collector',
|
||||
title='Defines the parameters of '
|
||||
'the module model collectors')
|
||||
collector = cfg.OptGroup(
|
||||
name='collector',
|
||||
title='Defines the parameters of the module model collectors',
|
||||
)
|
||||
|
||||
COLLECTOR_OPTS = [
|
||||
cfg.ListOpt('collector_plugins',
|
||||
default=['compute', 'storage'],
|
||||
help="""
|
||||
cfg.ListOpt(
|
||||
'collector_plugins',
|
||||
default=['compute', 'storage'],
|
||||
help="""
|
||||
The cluster data model plugin names.
|
||||
|
||||
Supported in-tree collectors include:
|
||||
@@ -34,34 +36,39 @@ Supported in-tree collectors include:
|
||||
|
||||
Custom data model collector plugins can be defined with the
|
||||
``watcher_cluster_data_model_collectors`` extension point.
|
||||
"""),
|
||||
cfg.IntOpt('api_query_max_retries',
|
||||
min=1,
|
||||
default=10,
|
||||
help="Number of retries before giving up on query to "
|
||||
"external service.",
|
||||
deprecated_name="api_call_retries"),
|
||||
cfg.IntOpt('api_query_interval',
|
||||
min=0,
|
||||
default=1,
|
||||
help="Time before retry after failed query to "
|
||||
"external service.",
|
||||
deprecated_name="api_query_timeout"),
|
||||
cfg.IntOpt("compute_resources_collector_timeout",
|
||||
min=30,
|
||||
default=600,
|
||||
help="Timeout in seconds for collecting multiple compute "
|
||||
"resources from nova. Note that this timeout does not "
|
||||
"represent the total time for collecting all resources. "
|
||||
"Setting this value to 0 or small values will cause the "
|
||||
"collector to abort and stop the collection process."),
|
||||
""",
|
||||
),
|
||||
cfg.IntOpt(
|
||||
'api_query_max_retries',
|
||||
min=1,
|
||||
default=10,
|
||||
help="Number of retries before giving up on query to "
|
||||
"external service.",
|
||||
deprecated_name="api_call_retries",
|
||||
),
|
||||
cfg.IntOpt(
|
||||
'api_query_interval',
|
||||
min=0,
|
||||
default=1,
|
||||
help="Time before retry after failed query to external service.",
|
||||
deprecated_name="api_query_timeout",
|
||||
),
|
||||
cfg.IntOpt(
|
||||
"compute_resources_collector_timeout",
|
||||
min=30,
|
||||
default=600,
|
||||
help="Timeout in seconds for collecting multiple compute "
|
||||
"resources from nova. Note that this timeout does not "
|
||||
"represent the total time for collecting all resources. "
|
||||
"Setting this value to 0 or small values will cause the "
|
||||
"collector to abort and stop the collection process.",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def register_opts(conf):
|
||||
conf.register_group(collector)
|
||||
conf.register_opts(COLLECTOR_OPTS,
|
||||
group=collector)
|
||||
conf.register_opts(COLLECTOR_OPTS, group=collector)
|
||||
|
||||
|
||||
def list_opts():
|
||||
|
||||
@@ -21,9 +21,10 @@ from watcher.decision_engine.datasources import manager
|
||||
from watcher.decision_engine.datasources import prometheus
|
||||
|
||||
|
||||
datasources = cfg.OptGroup(name='watcher_datasources',
|
||||
title='Configuration Options for watcher'
|
||||
' datasources')
|
||||
datasources = cfg.OptGroup(
|
||||
name='watcher_datasources',
|
||||
title='Configuration Options for watcher datasources',
|
||||
)
|
||||
|
||||
possible_datasources = list(manager.DataSourceManager.metric_map.keys())
|
||||
|
||||
@@ -35,26 +36,32 @@ default_datasources = list(possible_datasources)
|
||||
default_datasources.remove(prometheus.PrometheusHelper.NAME)
|
||||
|
||||
DATASOURCES_OPTS = [
|
||||
cfg.ListOpt("datasources",
|
||||
help="Datasources to use in order to query the needed metrics."
|
||||
" If one of strategy metric is not available in the first"
|
||||
" datasource, the next datasource will be chosen. This is"
|
||||
" the default for all strategies unless a strategy has a"
|
||||
" specific override.",
|
||||
item_type=cfg.types.String(choices=possible_datasources),
|
||||
default=default_datasources),
|
||||
cfg.IntOpt('query_max_retries',
|
||||
min=1,
|
||||
default=10,
|
||||
mutable=True,
|
||||
help='How many times Watcher is trying to query again'),
|
||||
cfg.IntOpt('query_interval',
|
||||
min=0,
|
||||
default=1,
|
||||
mutable=True,
|
||||
help='How many seconds Watcher should wait to do query again',
|
||||
deprecated_name="query_timeout")
|
||||
]
|
||||
cfg.ListOpt(
|
||||
"datasources",
|
||||
help="Datasources to use in order to query the needed metrics."
|
||||
" If one of strategy metric is not available in the first"
|
||||
" datasource, the next datasource will be chosen. This is"
|
||||
" the default for all strategies unless a strategy has a"
|
||||
" specific override.",
|
||||
item_type=cfg.types.String(choices=possible_datasources),
|
||||
default=default_datasources,
|
||||
),
|
||||
cfg.IntOpt(
|
||||
'query_max_retries',
|
||||
min=1,
|
||||
default=10,
|
||||
mutable=True,
|
||||
help='How many times Watcher is trying to query again',
|
||||
),
|
||||
cfg.IntOpt(
|
||||
'query_interval',
|
||||
min=0,
|
||||
default=1,
|
||||
mutable=True,
|
||||
help='How many seconds Watcher should wait to do query again',
|
||||
deprecated_name="query_timeout",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def register_opts(conf):
|
||||
|
||||
@@ -22,15 +22,15 @@ from watcher.conf import paths
|
||||
|
||||
|
||||
_DEFAULT_SQL_CONNECTION = 'sqlite:///{}'.format(
|
||||
paths.state_path_def('watcher.sqlite'))
|
||||
paths.state_path_def('watcher.sqlite')
|
||||
)
|
||||
|
||||
database = cfg.OptGroup(name='database',
|
||||
title='Configuration Options for database')
|
||||
database = cfg.OptGroup(
|
||||
name='database', title='Configuration Options for database'
|
||||
)
|
||||
|
||||
SQL_OPTS = [
|
||||
cfg.StrOpt('mysql_engine',
|
||||
default='InnoDB',
|
||||
help='MySQL engine to use.')
|
||||
cfg.StrOpt('mysql_engine', default='InnoDB', help='MySQL engine to use.')
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -18,74 +18,93 @@
|
||||
from oslo_config import cfg
|
||||
|
||||
|
||||
watcher_decision_engine = cfg.OptGroup(name='watcher_decision_engine',
|
||||
title='Defines the parameters of '
|
||||
'the module decision engine')
|
||||
watcher_decision_engine = cfg.OptGroup(
|
||||
name='watcher_decision_engine',
|
||||
title='Defines the parameters of the module decision engine',
|
||||
)
|
||||
|
||||
WATCHER_DECISION_ENGINE_OPTS = [
|
||||
cfg.StrOpt('conductor_topic',
|
||||
default='watcher.decision.control',
|
||||
help='The topic name used for '
|
||||
'control events, this topic '
|
||||
'used for RPC calls'),
|
||||
cfg.ListOpt('notification_topics',
|
||||
default=['nova.versioned_notifications',
|
||||
'openstack.notifications'],
|
||||
help='The exchange and topic names from which '
|
||||
'notification events will be listened to. '
|
||||
'The exchange should be specified to get '
|
||||
'an ability to use pools.'),
|
||||
cfg.StrOpt('publisher_id',
|
||||
default='watcher.decision.api',
|
||||
help='The identifier used by the Watcher '
|
||||
'module on the message broker'),
|
||||
cfg.IntOpt('max_audit_workers',
|
||||
default=2,
|
||||
required=True,
|
||||
help='The maximum number of threads that can be used to '
|
||||
'execute audits in parallel.'),
|
||||
cfg.IntOpt('max_general_workers',
|
||||
default=4,
|
||||
required=True,
|
||||
help='The maximum number of threads that can be used to '
|
||||
'execute general tasks in parallel. The number of general '
|
||||
'workers will not increase depending on the number of '
|
||||
'audit workers!'),
|
||||
cfg.IntOpt('action_plan_expiry',
|
||||
default=24,
|
||||
mutable=True,
|
||||
help='An expiry timespan(hours). Watcher invalidates any '
|
||||
'action plan for which its creation time '
|
||||
'-whose number of hours has been offset by this value-'
|
||||
' is older that the current time.'),
|
||||
cfg.IntOpt('check_periodic_interval',
|
||||
default=30 * 60,
|
||||
mutable=True,
|
||||
help='Interval (in seconds) for checking action plan expiry.'),
|
||||
cfg.StrOpt('metric_map_path',
|
||||
default='/etc/watcher/metric_map.yaml',
|
||||
help='Path to metric map yaml formatted file. '
|
||||
' '
|
||||
'The file contains a map per datasource whose keys '
|
||||
'are the metric names as recognized by watcher and the '
|
||||
'value is the real name of the metric in the datasource. '
|
||||
'For example:: \n\n'
|
||||
' gnocchi:\n'
|
||||
' instance_cpu_usage: cpu_vm_util\n'
|
||||
' aetos:\n'
|
||||
' instance_cpu_usage: ceilometer_cpu\n\n'
|
||||
'This file is optional.'),
|
||||
cfg.IntOpt('continuous_audit_interval',
|
||||
default=10,
|
||||
mutable=True,
|
||||
help='Interval (in seconds) for checking newly created '
|
||||
'continuous audits.')]
|
||||
cfg.StrOpt(
|
||||
'conductor_topic',
|
||||
default='watcher.decision.control',
|
||||
help='The topic name used for '
|
||||
'control events, this topic '
|
||||
'used for RPC calls',
|
||||
),
|
||||
cfg.ListOpt(
|
||||
'notification_topics',
|
||||
default=['nova.versioned_notifications', 'openstack.notifications'],
|
||||
help='The exchange and topic names from which '
|
||||
'notification events will be listened to. '
|
||||
'The exchange should be specified to get '
|
||||
'an ability to use pools.',
|
||||
),
|
||||
cfg.StrOpt(
|
||||
'publisher_id',
|
||||
default='watcher.decision.api',
|
||||
help='The identifier used by the Watcher module on the message broker',
|
||||
),
|
||||
cfg.IntOpt(
|
||||
'max_audit_workers',
|
||||
default=2,
|
||||
required=True,
|
||||
help='The maximum number of threads that can be used to '
|
||||
'execute audits in parallel.',
|
||||
),
|
||||
cfg.IntOpt(
|
||||
'max_general_workers',
|
||||
default=4,
|
||||
required=True,
|
||||
help='The maximum number of threads that can be used to '
|
||||
'execute general tasks in parallel. The number of general '
|
||||
'workers will not increase depending on the number of '
|
||||
'audit workers!',
|
||||
),
|
||||
cfg.IntOpt(
|
||||
'action_plan_expiry',
|
||||
default=24,
|
||||
mutable=True,
|
||||
help='An expiry timespan(hours). Watcher invalidates any '
|
||||
'action plan for which its creation time '
|
||||
'-whose number of hours has been offset by this value-'
|
||||
' is older that the current time.',
|
||||
),
|
||||
cfg.IntOpt(
|
||||
'check_periodic_interval',
|
||||
default=30 * 60,
|
||||
mutable=True,
|
||||
help='Interval (in seconds) for checking action plan expiry.',
|
||||
),
|
||||
cfg.StrOpt(
|
||||
'metric_map_path',
|
||||
default='/etc/watcher/metric_map.yaml',
|
||||
help='Path to metric map yaml formatted file. '
|
||||
' '
|
||||
'The file contains a map per datasource whose keys '
|
||||
'are the metric names as recognized by watcher and the '
|
||||
'value is the real name of the metric in the datasource. '
|
||||
'For example:: \n\n'
|
||||
' gnocchi:\n'
|
||||
' instance_cpu_usage: cpu_vm_util\n'
|
||||
' aetos:\n'
|
||||
' instance_cpu_usage: ceilometer_cpu\n\n'
|
||||
'This file is optional.',
|
||||
),
|
||||
cfg.IntOpt(
|
||||
'continuous_audit_interval',
|
||||
default=10,
|
||||
mutable=True,
|
||||
help='Interval (in seconds) for checking newly created '
|
||||
'continuous audits.',
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def register_opts(conf):
|
||||
conf.register_group(watcher_decision_engine)
|
||||
conf.register_opts(WATCHER_DECISION_ENGINE_OPTS,
|
||||
group=watcher_decision_engine)
|
||||
conf.register_opts(
|
||||
WATCHER_DECISION_ENGINE_OPTS, group=watcher_decision_engine
|
||||
)
|
||||
|
||||
|
||||
def list_opts():
|
||||
|
||||
@@ -19,9 +19,11 @@ from oslo_config import cfg
|
||||
|
||||
|
||||
EXC_LOG_OPTS = [
|
||||
cfg.BoolOpt('fatal_exception_format_errors',
|
||||
default=False,
|
||||
help='Make exception message format errors fatal.'),
|
||||
cfg.BoolOpt(
|
||||
'fatal_exception_format_errors',
|
||||
default=False,
|
||||
help='Make exception message format errors fatal.',
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -18,21 +18,34 @@
|
||||
from oslo_config import cfg
|
||||
|
||||
|
||||
gnocchi_client = cfg.OptGroup(name='gnocchi_client',
|
||||
title='Configuration Options for Gnocchi')
|
||||
gnocchi_client = cfg.OptGroup(
|
||||
name='gnocchi_client', title='Configuration Options for Gnocchi'
|
||||
)
|
||||
|
||||
GNOCCHI_CLIENT_OPTS = [
|
||||
cfg.StrOpt('api_version',
|
||||
default='1',
|
||||
help='Version of Gnocchi API to use in gnocchiclient.'),
|
||||
cfg.StrOpt('endpoint_type',
|
||||
default='public',
|
||||
choices=['public', 'internal', 'admin',
|
||||
'publicURL', 'internalURL', 'adminURL'],
|
||||
help='Type of endpoint to use in gnocchi client.'),
|
||||
cfg.StrOpt('region_name',
|
||||
help='Region in Identity service catalog to use for '
|
||||
'communication with the OpenStack service.')
|
||||
cfg.StrOpt(
|
||||
'api_version',
|
||||
default='1',
|
||||
help='Version of Gnocchi API to use in gnocchiclient.',
|
||||
),
|
||||
cfg.StrOpt(
|
||||
'endpoint_type',
|
||||
default='public',
|
||||
choices=[
|
||||
'public',
|
||||
'internal',
|
||||
'admin',
|
||||
'publicURL',
|
||||
'internalURL',
|
||||
'adminURL',
|
||||
],
|
||||
help='Type of endpoint to use in gnocchi client.',
|
||||
),
|
||||
cfg.StrOpt(
|
||||
'region_name',
|
||||
help='Region in Identity service catalog to use for '
|
||||
'communication with the OpenStack service.',
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user