adopt ruff-format

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

Assisted-By: claude-code sonnet 4.6
Change-Id: If237599df59552d7aff9726be8af5d780f077e9a
Signed-off-by: Sean Mooney <work@seanmooney.info>
This commit is contained in:
Sean Mooney
2026-03-20 19:31:34 +00:00
parent c66dceb705
commit e19327e780
401 changed files with 24071 additions and 15629 deletions

View File

@@ -29,6 +29,12 @@ repos:
hooks:
- id: remove-tabs
exclude: '.*\.(svg)$'
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.1
hooks:
- id: ruff-check
args: ['--fix', '--unsafe-fixes']
- id: ruff-format
- repo: https://opendev.org/openstack/hacking
rev: 7.0.0
hooks:
@@ -40,16 +46,6 @@ repos:
hooks:
- id: bandit
args: ['-c', 'pyproject.toml']
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.1
hooks:
- id: ruff-check
args: ['--fix', '--unsafe-fixes']
- repo: https://github.com/hhatto/autopep8
rev: v2.3.2
hooks:
- id: autopep8
files: '^.*\.py$'
- repo: https://github.com/codespell-project/codespell
rev: v2.4.1
hooks:

View File

@@ -22,10 +22,7 @@
# All configuration values have a default; values that are commented out
# serve to show the default.
extensions = [
'openstackdocstheme',
'os_api_ref',
]
extensions = ['openstackdocstheme', 'os_api_ref']
# -- General configuration ----------------------------------------------------
@@ -60,9 +57,7 @@ html_theme = 'openstackdocs'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
html_theme_options = {
"sidebar_mode": "toc",
}
html_theme_options = {"sidebar_mode": "toc"}
# -- Options for LaTeX output -------------------------------------------------
@@ -70,6 +65,11 @@ html_theme_options = {
# (source start file, target name, title, author, documentclass
# [howto/manual]).
latex_documents = [
('index', 'Watcher.tex', 'Infrastructure Optimization API Reference',
'OpenStack Foundation', 'manual'),
(
'index',
'Watcher.tex',
'Infrastructure Optimization API Reference',
'OpenStack Foundation',
'manual',
)
]

View File

@@ -24,12 +24,29 @@ from watcher.version import version_string
class BaseWatcherDirective(rst.Directive):
def __init__(self, name, arguments, options, content, lineno,
content_offset, block_text, state, state_machine):
def __init__(
self,
name,
arguments,
options,
content,
lineno,
content_offset,
block_text,
state,
state_machine,
):
super().__init__(
name, arguments, options, content, lineno,
content_offset, block_text, state, state_machine)
name,
arguments,
options,
content,
lineno,
content_offset,
block_text,
state,
state_machine,
)
self.result = statemachine.ViewList()
def run(self):
@@ -143,7 +160,8 @@ class WatcherFunc(BaseWatcherDirective):
error = self.state_machine.reporter.error(
f'The "{self.name}" directive is empty; content required.',
nodes.literal_block(self.block_text, self.block_text),
line=self.lineno)
line=self.lineno,
)
return [error]
func_path = self.content[0]
@@ -164,8 +182,9 @@ class WatcherFunc(BaseWatcherDirective):
self.add_textblock(textblock)
try:
node_class = getattr(nodes,
self.options.get('format', 'paragraph'))
node_class = getattr(
nodes, self.options.get('format', 'paragraph')
)
except Exception as exc:
raise self.error(exc)

View File

@@ -28,7 +28,6 @@ from watcher.objects import base
class VersionedNotificationDirective(Directive):
SAMPLE_ROOT = 'doc/notification_samples/'
TOGGLE_SCRIPT = """
<script>
@@ -51,15 +50,16 @@ jQuery(document).ready(function(){
ovos = base.WatcherObjectRegistry.obj_classes()
for name, cls in ovos.items():
cls = cls[0]
if (issubclass(cls, notification.NotificationBase) and
cls != notification.NotificationBase):
if (
issubclass(cls, notification.NotificationBase)
and cls != notification.NotificationBase
):
payload_name = cls.fields['payload'].objname
payload_cls = ovos[payload_name][0]
for sample in cls.samples:
notifications.append((cls.__name__,
payload_cls.__name__,
sample))
notifications.append(
(cls.__name__, payload_cls.__name__, sample)
)
return sorted(notifications)
def _build_markup(self, notifications):
@@ -90,7 +90,7 @@ jQuery(document).ready(function(){
# fill the table content, one notification per row
for name, payload, sample_file in notifications:
event_type = sample_file[0: -5].replace('-', '.')
event_type = sample_file[0:-5].replace('-', '.')
row = nodes.row()
body.append(row)
@@ -115,11 +115,15 @@ jQuery(document).ready(function(){
with open(self.SAMPLE_ROOT + sample_file) as f:
sample_content = f.read()
event_type = sample_file[0: -5]
html_str = self.TOGGLE_SCRIPT % ((event_type, ) * 3)
html_str += (f"<input type='button' id='{event_type}-hideshow' "
"value='hide/show sample'>")
html_str += (f"<div id='{event_type}-div'><pre>{sample_content}</pre></div>")
event_type = sample_file[0:-5]
html_str = self.TOGGLE_SCRIPT % ((event_type,) * 3)
html_str += (
f"<input type='button' id='{event_type}-hideshow' "
"value='hide/show sample'>"
)
html_str += (
f"<div id='{event_type}-div'><pre>{sample_content}</pre></div>"
)
raw = nodes.raw('', html_str, format="html")
col.append(raw)
@@ -128,5 +132,6 @@ jQuery(document).ready(function(){
def setup(app):
app.add_directive('versioned_notifications',
VersionedNotificationDirective)
app.add_directive(
'versioned_notifications', VersionedNotificationDirective
)

View File

@@ -45,9 +45,9 @@ extensions = [
]
wsme_protocols = ['restjson']
config_generator_config_file = [(
'../../etc/watcher/oslo-config-generator/watcher.conf',
'_static/watcher')]
config_generator_config_file = [
('../../etc/watcher/oslo-config-generator/watcher.conf', '_static/watcher')
]
sample_config_basename = 'watcher'
# The suffix of source filenames.
@@ -92,14 +92,28 @@ pygments_style = 'native'
# List of tuples 'sourcefile', 'target', u'title', u'Authors name', 'manual'
man_pages = [
('man/watcher-api', 'watcher-api', 'Watcher API Server',
['OpenStack'], 1),
('man/watcher-applier', 'watcher-applier', 'Watcher Applier',
['OpenStack'], 1),
('man/watcher-db-manage', 'watcher-db-manage',
'Watcher Db Management Utility', ['OpenStack'], 1),
('man/watcher-decision-engine', 'watcher-decision-engine',
'Watcher Decision Engine', ['OpenStack'], 1),
('man/watcher-api', 'watcher-api', 'Watcher API Server', ['OpenStack'], 1),
(
'man/watcher-applier',
'watcher-applier',
'Watcher Applier',
['OpenStack'],
1,
),
(
'man/watcher-db-manage',
'watcher-db-manage',
'Watcher Db Management Utility',
['OpenStack'],
1,
),
(
'man/watcher-decision-engine',
'watcher-decision-engine',
'Watcher Decision Engine',
['OpenStack'],
1,
),
]
# -- Options for HTML output --------------------------------------------------
@@ -127,10 +141,13 @@ openstackdocs_bug_tag = ''
# (source start file, target name, title, author, documentclass
# [howto/manual]).
latex_documents = [
('index',
'doc-watcher.tex',
'Watcher Documentation',
'OpenStack Foundation', 'manual'),
(
'index',
'doc-watcher.tex',
'Watcher Documentation',
'OpenStack Foundation',
'manual',
)
]
# If false, no module index is generated.

View File

@@ -35,8 +35,7 @@
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['reno.sphinxext',
'openstackdocstheme']
extensions = ['reno.sphinxext', 'openstackdocstheme']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
@@ -183,10 +182,8 @@ htmlhelp_basename = 'watcherdoc'
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
# 'preamble': '',
}
@@ -194,8 +191,13 @@ latex_elements = {
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual])
latex_documents = [
('index', 'watcher.tex', 'Watcher Documentation',
'Watcher developers', 'manual'),
(
'index',
'watcher.tex',
'Watcher Documentation',
'Watcher developers',
'manual',
)
]
# The name of an image file (relative to this directory) to place at the top of
@@ -224,8 +226,7 @@ latex_documents = [
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'watcher', 'Watcher Documentation',
['Watcher developers'], 1)
('index', 'watcher', 'Watcher Documentation', ['Watcher developers'], 1)
]
# If true, show URL addresses after external links.
@@ -238,9 +239,15 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'watcher', 'Watcher Documentation',
'Watcher developers', 'watcher', 'One line description of project.',
'Miscellaneous'),
(
'index',
'watcher',
'Watcher Documentation',
'Watcher developers',
'watcher',
'One line description of project.',
'Miscellaneous',
)
]
# Documents to append as an appendix to all manuals.

View File

@@ -16,6 +16,4 @@
import setuptools
setuptools.setup(
setup_requires=['pbr>=2.0.0'],
pbr=True)
setuptools.setup(setup_requires=['pbr>=2.0.0'], pbr=True)

View File

@@ -134,11 +134,12 @@ commands =
[flake8]
filename = *.py,app.wsgi
show-source=True
select = H,N
# W504 line break after binary operator
# H301 one import per line incorrectly triggers when
# imports need to be wrapped in () due to line length
# H306 ruff handles imports
ignore= H105,E123,E226,N320,H202,W504,W503,H306,H301
ignore= H105,H202,H306,H301,N320
builtins= _
enable-extensions = H106,H203,H904
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,*sqlalchemy/alembic/versions/*,demo/,releasenotes

View File

@@ -36,6 +36,8 @@ def install(app, conf, public_routes):
"""
if not CONF.get('enable_authentication'):
return app
return auth_token.AuthTokenMiddleware(app,
conf=dict(conf.keystone_authtoken),
public_api_routes=public_routes)
return auth_token.AuthTokenMiddleware(
app,
conf=dict(conf.keystone_authtoken),
public_api_routes=public_routes,
)

View File

@@ -46,7 +46,7 @@ def setup_app(config=None):
logging=getattr(config, 'logging', {}),
debug=CONF.debug,
wrap_app=_wrap_app,
**app_conf
**app_conf,
)
return acl.install(app, CONF, config.app.acl_public_routes)

View File

@@ -15,7 +15,6 @@
Use this file for deploying the API service under Apache2 mod_wsgi.
"""
# This script is deprecated and it will be removed in U release.
# Please switch to automatically generated watcher-api-wsgi script instead.
from watcher.api import wsgi

View File

@@ -19,10 +19,7 @@ from watcher.api import hooks
# Server Specific Configurations
# See https://pecan.readthedocs.org/en/latest/configuration.html#server-configuration # noqa
server = {
'port': '9322',
'host': '127.0.0.1'
}
server = {'port': '9322', 'host': '127.0.0.1'}
# Pecan Application Configurations
# See https://pecan.readthedocs.org/en/latest/configuration.html#application-configuration # noqa
@@ -33,10 +30,7 @@ if not cfg.CONF.api.get("enable_webhooks_auth"):
app = {
'root': 'watcher.api.controllers.root.RootController',
'modules': ['watcher.api'],
'hooks': [
hooks.ContextHook(),
hooks.NoExceptionTracebackHook(),
],
'hooks': [hooks.ContextHook(), hooks.NoExceptionTracebackHook()],
'static_root': '%(confdir)s/public',
'enable_acl': True,
'acl_public_routes': acl_public_routes,
@@ -44,12 +38,6 @@ app = {
# WSME Configurations
# See https://wsme.readthedocs.org/en/latest/integrate.html#configuration
wsme = {
'debug': cfg.CONF.get("debug") if "debug" in cfg.CONF else False,
}
wsme = {'debug': cfg.CONF.get("debug") if "debug" in cfg.CONF else False}
PECAN_CONFIG = {
"server": server,
"app": app,
"wsme": wsme,
}
PECAN_CONFIG = {"server": server, "app": app, "wsme": wsme}

View File

@@ -23,7 +23,6 @@ from wsme import types as wtypes
class APIBase(wtypes.Base):
created_at = wsme.wsattr(datetime.datetime, readonly=True)
"""The time in UTC at which the object is created"""
@@ -35,10 +34,11 @@ class APIBase(wtypes.Base):
def as_dict(self):
"""Render this object as a dict of its fields."""
return {k: getattr(self, k)
for k in self.fields
if hasattr(self, k) and
getattr(self, k) != wsme.Unset}
return {
k: getattr(self, k)
for k in self.fields
if hasattr(self, k) and getattr(self, k) != wsme.Unset
}
def unset_fields_except(self, except_list=None):
"""Unset fields so they don't appear in the message body.
@@ -77,7 +77,8 @@ class Version:
"""
(self.major, self.minor) = Version.parse_headers(
headers, default_version, latest_version)
headers, default_version, latest_version
)
def __repr__(self):
return f'{self.major}.{self.minor}'
@@ -94,8 +95,8 @@ class Version:
"""
version_str = microversion_parse.get_version(
headers,
service_type='infra-optim')
headers, service_type='infra-optim'
)
minimal_version = (1, 0)
@@ -122,7 +123,8 @@ class Version:
if len(version) != 2:
raise exc.HTTPNotAcceptable(
f"Invalid value for {Version.string} header")
f"Invalid value for {Version.string} header"
)
return version
def __gt__(self, other):

View File

@@ -46,15 +46,24 @@ class Link(base.APIBase):
"""Indicates the type of document/link."""
@staticmethod
def make_link(rel_name, url, resource, resource_args,
bookmark=False, type=wtypes.Unset):
href = build_url(resource, resource_args,
bookmark=bookmark, base_url=url)
def make_link(
rel_name,
url,
resource,
resource_args,
bookmark=False,
type=wtypes.Unset,
):
href = build_url(
resource, resource_args, bookmark=bookmark, base_url=url
)
return Link(href=href, rel=rel_name, type=type)
@classmethod
def sample(cls):
sample = cls(href="http://localhost:6385/chassis/"
"eaaca217-e7d8-47b4-bb41-3f99f20eed89",
rel="bookmark")
sample = cls(
href="http://localhost:6385/chassis/"
"eaaca217-e7d8-47b4-bb41-3f99f20eed89",
rel="bookmark",
)
return sample

View File

@@ -59,14 +59,15 @@ class Version(base.APIBase):
version.status = status
version.max_version = v.max_version_string()
version.min_version = v.min_version_string()
version.links = [link.Link.make_link('self',
pecan.request.application_url,
id, '', bookmark=True)]
version.links = [
link.Link.make_link(
'self', pecan.request.application_url, id, '', bookmark=True
)
]
return version
class Root(base.APIBase):
name = wtypes.text
"""The name of the API"""
@@ -83,16 +84,17 @@ class Root(base.APIBase):
def convert():
root = Root()
root.name = "OpenStack Watcher API"
root.description = ("Watcher is an OpenStack project which aims to "
"improve physical resources usage through "
"better VM placement.")
root.description = (
"Watcher is an OpenStack project which aims to "
"improve physical resources usage through "
"better VM placement."
)
root.versions = [Version.convert('v1')]
root.default_version = Version.convert('v1')
return root
class RootController(rest.RestController):
_versions = ['v1']
"""All supported API versions"""

View File

@@ -47,20 +47,29 @@ from watcher.api.controllers.v1 import webhooks
def min_version():
return base.Version(
{base.Version.string: ' '.join([versions.service_type_string(),
versions.min_version_string()])},
versions.min_version_string(), versions.max_version_string())
{
base.Version.string: ' '.join(
[versions.service_type_string(), versions.min_version_string()]
)
},
versions.min_version_string(),
versions.max_version_string(),
)
def max_version():
return base.Version(
{base.Version.string: ' '.join([versions.service_type_string(),
versions.max_version_string()])},
versions.min_version_string(), versions.max_version_string())
{
base.Version.string: ' '.join(
[versions.service_type_string(), versions.max_version_string()]
)
},
versions.min_version_string(),
versions.max_version_string(),
)
class APIBase(wtypes.Base):
created_at = wsme.wsattr(datetime.datetime, readonly=True)
"""The time in UTC at which the object is created"""
@@ -72,10 +81,11 @@ class APIBase(wtypes.Base):
def as_dict(self):
"""Render this object as a dict of its fields."""
return {k: getattr(self, k)
for k in self.fields
if hasattr(self, k) and
getattr(self, k) != wsme.Unset}
return {
k: getattr(self, k)
for k in self.fields
if hasattr(self, k) and getattr(self, k) != wsme.Unset
}
def unset_fields_except(self, except_list=None):
"""Unset fields so they don't appear in the message body.
@@ -143,77 +153,74 @@ class V1(APIBase):
v1 = V1()
v1.id = "v1"
base_url = pecan.request.application_url
v1.links = [link.Link.make_link('self', base_url,
'v1', '', bookmark=True),
link.Link.make_link('describedby',
'http://docs.openstack.org',
'developer/watcher/dev',
'api-spec-v1.html',
bookmark=True, type='text/html')
]
v1.media_types = [MediaType('application/json',
'application/vnd.openstack.watcher.v1+json')]
v1.audit_templates = [link.Link.make_link('self',
base_url,
'audit_templates', ''),
link.Link.make_link('bookmark',
base_url,
'audit_templates', '',
bookmark=True)
]
v1.audits = [link.Link.make_link('self', base_url,
'audits', ''),
link.Link.make_link('bookmark',
base_url,
'audits', '',
bookmark=True)
]
v1.links = [
link.Link.make_link('self', base_url, 'v1', '', bookmark=True),
link.Link.make_link(
'describedby',
'http://docs.openstack.org',
'developer/watcher/dev',
'api-spec-v1.html',
bookmark=True,
type='text/html',
),
]
v1.media_types = [
MediaType(
'application/json', 'application/vnd.openstack.watcher.v1+json'
)
]
v1.audit_templates = [
link.Link.make_link('self', base_url, 'audit_templates', ''),
link.Link.make_link(
'bookmark', base_url, 'audit_templates', '', bookmark=True
),
]
v1.audits = [
link.Link.make_link('self', base_url, 'audits', ''),
link.Link.make_link(
'bookmark', base_url, 'audits', '', bookmark=True
),
]
if utils.allow_list_datamodel():
v1.data_model = [link.Link.make_link('self', base_url,
'data_model', ''),
link.Link.make_link('bookmark',
base_url,
'data_model', '',
bookmark=True)
]
v1.actions = [link.Link.make_link('self', base_url,
'actions', ''),
link.Link.make_link('bookmark',
base_url,
'actions', '',
bookmark=True)
]
v1.action_plans = [link.Link.make_link(
'self', base_url, 'action_plans', ''),
link.Link.make_link('bookmark',
base_url,
'action_plans', '',
bookmark=True)
v1.data_model = [
link.Link.make_link('self', base_url, 'data_model', ''),
link.Link.make_link(
'bookmark', base_url, 'data_model', '', bookmark=True
),
]
v1.actions = [
link.Link.make_link('self', base_url, 'actions', ''),
link.Link.make_link(
'bookmark', base_url, 'actions', '', bookmark=True
),
]
v1.action_plans = [
link.Link.make_link('self', base_url, 'action_plans', ''),
link.Link.make_link(
'bookmark', base_url, 'action_plans', '', bookmark=True
),
]
v1.scoring_engines = [link.Link.make_link(
'self', base_url, 'scoring_engines', ''),
link.Link.make_link('bookmark',
base_url,
'scoring_engines', '',
bookmark=True)
]
v1.scoring_engines = [
link.Link.make_link('self', base_url, 'scoring_engines', ''),
link.Link.make_link(
'bookmark', base_url, 'scoring_engines', '', bookmark=True
),
]
v1.services = [link.Link.make_link(
'self', base_url, 'services', ''),
link.Link.make_link('bookmark',
base_url,
'services', '',
bookmark=True)
]
v1.services = [
link.Link.make_link('self', base_url, 'services', ''),
link.Link.make_link(
'bookmark', base_url, 'services', '', bookmark=True
),
]
if utils.allow_webhook_api():
v1.webhooks = [link.Link.make_link(
'self', base_url, 'webhooks', ''),
link.Link.make_link('bookmark',
base_url,
'webhooks', '',
bookmark=True)
]
v1.webhooks = [
link.Link.make_link('self', base_url, 'webhooks', ''),
link.Link.make_link(
'bookmark', base_url, 'webhooks', '', bookmark=True
),
]
return v1
@@ -249,7 +256,8 @@ class Controller(rest.RestController):
f"Mutually exclusive versions requested. Version {version} "
"requested but not supported by this service. The supported "
f"version range is: [{min_ver}, {max_ver}].",
headers=headers)
headers=headers,
)
# ensure the minor version is within the supported range
if version < min_version() or version > max_version():
min_ver = versions.min_version_string()
@@ -259,12 +267,16 @@ class Controller(rest.RestController):
"not "
"supported by this service. The supported version range is: "
f"[{min_ver}, {max_ver}].",
headers=headers)
headers=headers,
)
@pecan.expose()
def _route(self, args, request=None):
v = base.Version(pecan.request.headers, versions.min_version_string(),
versions.max_version_string())
v = base.Version(
pecan.request.headers,
versions.min_version_string(),
versions.max_version_string(),
)
# The Vary header is used as a hint to caching proxies and user agents
# that the response is also dependent on the OpenStack-API-Version and
@@ -273,17 +285,20 @@ class Controller(rest.RestController):
# Always set the min and max headers
pecan.response.headers[base.Version.min_string] = (
versions.min_version_string())
versions.min_version_string()
)
pecan.response.headers[base.Version.max_string] = (
versions.max_version_string())
versions.max_version_string()
)
# assert that requested version is supported
self._check_version(v, pecan.response.headers)
pecan.response.headers[base.Version.string] = (
' '.join([versions.service_type_string(), str(v)]))
pecan.response.headers[base.Version.string] = ' '.join(
[versions.service_type_string(), str(v)]
)
pecan.request.version = v
return super()._route(args, request)
__all__ = ("Controller", )
__all__ = ("Controller",)

View File

@@ -89,7 +89,6 @@ def hide_fields_in_newer_versions(obj):
class ActionPatchType(types.JsonPatchType):
@staticmethod
def _validate_state(patch):
serialized_patch = {'path': patch.path, 'op': patch.op}
@@ -100,7 +99,8 @@ class ActionPatchType(types.JsonPatchType):
if state_value and not hasattr(objects.action.State, state_value):
msg = _("Invalid state: %(state)s")
raise exception.PatchError(
patch=serialized_patch, reason=msg % dict(state=state_value))
patch=serialized_patch, reason=msg % dict(state=state_value)
)
@staticmethod
def validate(patch):
@@ -126,6 +126,7 @@ class Action(base.APIBase):
This class enforces type checking and value constraints, and converts
between the internal object model and the API representation of a action.
"""
_action_plan_uuid = None
def _get_action_plan_uuid(self):
@@ -137,7 +138,8 @@ class Action(base.APIBase):
elif value and self._action_plan_uuid != value:
try:
action_plan = objects.ActionPlan.get(
pecan.request.context, value)
pecan.request.context, value
)
self._action_plan_uuid = action_plan.uuid
self.action_plan_id = action_plan.id
except exception.ActionPlanNotFound:
@@ -146,9 +148,12 @@ class Action(base.APIBase):
uuid = wtypes.wsattr(types.uuid, readonly=True)
"""Unique UUID for this action"""
action_plan_uuid = wtypes.wsproperty(types.uuid, _get_action_plan_uuid,
_set_action_plan_uuid,
mandatory=True)
action_plan_uuid = wtypes.wsproperty(
types.uuid,
_get_action_plan_uuid,
_set_action_plan_uuid,
mandatory=True,
)
"""The action plan this action belongs to """
state = wtypes.text
@@ -187,22 +192,32 @@ class Action(base.APIBase):
self.fields.append('action_plan_id')
self.fields.append('description')
setattr(self, 'action_plan_uuid', kwargs.get('action_plan_id',
wtypes.Unset))
setattr(
self,
'action_plan_uuid',
kwargs.get('action_plan_id', wtypes.Unset),
)
@staticmethod
def _convert_with_links(action, url, expand=True):
if not expand:
action.unset_fields_except(['uuid', 'state', 'action_plan_uuid',
'action_plan_id', 'action_type',
'parents'])
action.unset_fields_except(
[
'uuid',
'state',
'action_plan_uuid',
'action_plan_id',
'action_type',
'parents',
]
)
action.links = [link.Link.make_link('self', url,
'actions', action.uuid),
link.Link.make_link('bookmark', url,
'actions', action.uuid,
bookmark=True)
]
action.links = [
link.Link.make_link('self', url, 'actions', action.uuid),
link.Link.make_link(
'bookmark', url, 'actions', action.uuid, bookmark=True
),
]
return action
@classmethod
@@ -210,7 +225,8 @@ class Action(base.APIBase):
action = Action(**action.as_dict())
try:
obj_action_desc = objects.ActionDescription.get_by_type(
pecan.request.context, action.action_type)
pecan.request.context, action.action_type
)
description = obj_action_desc.description
except exception.ActionDescriptionNotFound:
description = ""
@@ -222,13 +238,15 @@ class Action(base.APIBase):
@classmethod
def sample(cls, expand=True):
sample = cls(uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c',
description='action description',
state='PENDING',
created_at=timeutils.utcnow(),
deleted_at=None,
updated_at=timeutils.utcnow(),
parents=[])
sample = cls(
uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c',
description='action description',
state='PENDING',
created_at=timeutils.utcnow(),
deleted_at=None,
updated_at=timeutils.utcnow(),
parents=[],
)
sample._action_plan_uuid = '7ae81bb3-dec3-4289-8d6c-da80bd8001ae'
return cls._convert_with_links(sample, 'http://localhost:9322', expand)
@@ -243,12 +261,11 @@ class ActionCollection(collection.Collection):
self._type = 'actions'
@staticmethod
def convert_with_links(actions, limit, url=None, expand=False,
**kwargs):
def convert_with_links(actions, limit, url=None, expand=False, **kwargs):
collection = ActionCollection()
collection.actions = [Action.convert_with_links(p, expand)
for p in actions]
collection.actions = [
Action.convert_with_links(p, expand) for p in actions
]
collection.next = collection.get_next(limit, url=url, **kwargs)
return collection
@@ -265,25 +282,32 @@ class ActionsController(rest.RestController):
def __init__(self):
super().__init__()
_custom_actions = {
'detail': ['GET'],
}
_custom_actions = {'detail': ['GET']}
def _get_actions_collection(self, marker, limit,
sort_key, sort_dir, expand=False,
resource_url=None,
action_plan_uuid=None, audit_uuid=None):
def _get_actions_collection(
self,
marker,
limit,
sort_key,
sort_dir,
expand=False,
resource_url=None,
action_plan_uuid=None,
audit_uuid=None,
):
additional_fields = ['action_plan_uuid']
api_utils.validate_sort_key(sort_key, list(objects.Action.fields) +
additional_fields)
api_utils.validate_sort_key(
sort_key, list(objects.Action.fields) + additional_fields
)
limit = api_utils.validate_limit(limit)
api_utils.validate_sort_dir(sort_dir)
marker_obj = None
if marker:
marker_obj = objects.Action.get_by_uuid(pecan.request.context,
marker)
marker_obj = objects.Action.get_by_uuid(
pecan.request.context, marker
)
filters = {}
if action_plan_uuid:
@@ -292,33 +316,54 @@ class ActionsController(rest.RestController):
if audit_uuid:
filters['audit_uuid'] = audit_uuid
need_api_sort = api_utils.check_need_api_sort(sort_key,
additional_fields)
sort_db_key = (sort_key if not need_api_sort
else None)
need_api_sort = api_utils.check_need_api_sort(
sort_key, additional_fields
)
sort_db_key = sort_key if not need_api_sort else None
actions = objects.Action.list(pecan.request.context,
limit,
marker_obj, sort_key=sort_db_key,
sort_dir=sort_dir,
filters=filters)
actions = objects.Action.list(
pecan.request.context,
limit,
marker_obj,
sort_key=sort_db_key,
sort_dir=sort_dir,
filters=filters,
)
actions_collection = ActionCollection.convert_with_links(
actions, limit, url=resource_url, expand=expand,
sort_key=sort_key, sort_dir=sort_dir)
actions,
limit,
url=resource_url,
expand=expand,
sort_key=sort_key,
sort_dir=sort_dir,
)
if need_api_sort:
api_utils.make_api_sort(actions_collection.actions,
sort_key, sort_dir)
api_utils.make_api_sort(
actions_collection.actions, sort_key, sort_dir
)
return actions_collection
@wsme_pecan.wsexpose(ActionCollection, types.uuid, int,
wtypes.text, wtypes.text, types.uuid,
types.uuid)
def get_all(self, marker=None, limit=None,
sort_key='id', sort_dir='asc', action_plan_uuid=None,
audit_uuid=None):
@wsme_pecan.wsexpose(
ActionCollection,
types.uuid,
int,
wtypes.text,
wtypes.text,
types.uuid,
types.uuid,
)
def get_all(
self,
marker=None,
limit=None,
sort_key='id',
sort_dir='asc',
action_plan_uuid=None,
audit_uuid=None,
):
"""Retrieve a list of actions.
:param marker: pagination marker for large data sets.
@@ -331,22 +376,38 @@ class ActionsController(rest.RestController):
to get only actions for that audit.
"""
context = pecan.request.context
policy.enforce(context, 'action:get_all',
action='action:get_all')
policy.enforce(context, 'action:get_all', action='action:get_all')
if action_plan_uuid and audit_uuid:
raise exception.ActionFilterCombinationProhibited
return self._get_actions_collection(
marker, limit, sort_key, sort_dir,
action_plan_uuid=action_plan_uuid, audit_uuid=audit_uuid)
marker,
limit,
sort_key,
sort_dir,
action_plan_uuid=action_plan_uuid,
audit_uuid=audit_uuid,
)
@wsme_pecan.wsexpose(ActionCollection, types.uuid, int,
wtypes.text, wtypes.text, types.uuid,
types.uuid)
def detail(self, marker=None, limit=None,
sort_key='id', sort_dir='asc', action_plan_uuid=None,
audit_uuid=None):
@wsme_pecan.wsexpose(
ActionCollection,
types.uuid,
int,
wtypes.text,
wtypes.text,
types.uuid,
types.uuid,
)
def detail(
self,
marker=None,
limit=None,
sort_key='id',
sort_dir='asc',
action_plan_uuid=None,
audit_uuid=None,
):
"""Retrieve a list of actions with detail.
:param marker: pagination marker for large data sets.
@@ -359,8 +420,7 @@ class ActionsController(rest.RestController):
to get only actions for that audit.
"""
context = pecan.request.context
policy.enforce(context, 'action:detail',
action='action:detail')
policy.enforce(context, 'action:detail', action='action:detail')
# NOTE(lucasagomes): /detail should only work against collections
parent = pecan.request.path.split('/')[:-1][-1]
@@ -373,8 +433,15 @@ class ActionsController(rest.RestController):
expand = True
resource_url = '/'.join(['actions', 'detail'])
return self._get_actions_collection(
marker, limit, sort_key, sort_dir, expand, resource_url,
action_plan_uuid=action_plan_uuid, audit_uuid=audit_uuid)
marker,
limit,
sort_key,
sort_dir,
expand,
resource_url,
action_plan_uuid=action_plan_uuid,
audit_uuid=audit_uuid,
)
@wsme_pecan.wsexpose(Action, types.uuid)
def get_one(self, action_uuid):
@@ -398,13 +465,16 @@ class ActionsController(rest.RestController):
"""
if not api_utils.allow_skipped_action():
raise exception.Invalid(
_("API microversion 1.5 or higher is required."))
_("API microversion 1.5 or higher is required.")
)
context = pecan.request.context
action_to_update = api_utils.get_resource(
'Action', action_uuid, eager=True)
policy.enforce(context, 'action:update', action_to_update,
action='action:update')
'Action', action_uuid, eager=True
)
policy.enforce(
context, 'action:update', action_to_update, action='action:update'
)
try:
action_dict = action_to_update.as_dict()
@@ -414,47 +484,59 @@ class ActionsController(rest.RestController):
# Define allowed state transitions for actions
allowed_patch_transitions = [
(objects.action.State.PENDING, objects.action.State.SKIPPED),
(objects.action.State.PENDING, objects.action.State.SKIPPED)
]
# Validate state transitions if state is being modified
if action.state != action_to_update.state:
transition = (action_to_update.state, action.state)
if transition not in allowed_patch_transitions:
error_message = _("State transition not allowed: "
"(%(initial_state)s -> %(new_state)s)")
error_message = _(
"State transition not allowed: "
"(%(initial_state)s -> %(new_state)s)"
)
raise exception.Conflict(
patch=patch,
message=error_message % dict(
message=error_message
% dict(
initial_state=action_to_update.state,
new_state=action.state))
new_state=action.state,
),
)
action_plan = action_to_update.action_plan
if action_plan.state not in [objects.action_plan.State.RECOMMENDED,
objects.action_plan.State.PENDING]:
error_message = _("State update not allowed for actionplan "
"state: %(ap_state)s")
if action_plan.state not in [
objects.action_plan.State.RECOMMENDED,
objects.action_plan.State.PENDING,
]:
error_message = _(
"State update not allowed for actionplan "
"state: %(ap_state)s"
)
raise exception.Conflict(
patch=patch,
message=error_message % dict(
ap_state=action_plan.state))
message=error_message % dict(ap_state=action_plan.state),
)
status_message = _("Action skipped by user.")
# status_message update only allowed with status update or when
# already SKIPPED
# NOTE(dviroel): status_message is an exposed field.
if action.status_message != action_to_update.status_message:
if (action.state == action_to_update.state and
action_to_update.state != objects.action.State.SKIPPED):
if (
action.state == action_to_update.state
and action_to_update.state != objects.action.State.SKIPPED
):
error_message = _(
"status_message update only allowed when action state "
"is SKIPPED")
raise exception.Conflict(
patch=patch,
message=error_message)
"is SKIPPED"
)
raise exception.Conflict(patch=patch, message=error_message)
else:
status_message = (_("%(status_message)s Reason: %(reason)s")
% dict(status_message=status_message,
reason=action.status_message))
status_message = _(
"%(status_message)s Reason: %(reason)s"
) % dict(
status_message=status_message, reason=action.status_message
)
action.status_message = status_message

View File

@@ -94,7 +94,6 @@ def hide_fields_in_newer_versions(obj):
class ActionPlanPatchType(types.JsonPatchType):
@staticmethod
def _validate_state(patch):
serialized_patch = {'path': patch.path, 'op': patch.op}
@@ -105,7 +104,8 @@ class ActionPlanPatchType(types.JsonPatchType):
if state_value and not hasattr(ap_objects.State, state_value):
msg = _("Invalid state: %(state)s")
raise exception.PatchError(
patch=serialized_patch, reason=msg % dict(state=state_value))
patch=serialized_patch, reason=msg % dict(state=state_value)
)
@staticmethod
def validate(patch):
@@ -164,7 +164,8 @@ class ActionPlan(base.APIBase):
try:
_efficacy_indicators = objects.EfficacyIndicator.list(
pecan.request.context,
filters={"action_plan_uuid": self.uuid})
filters={"action_plan_uuid": self.uuid},
)
for indicator in _efficacy_indicators:
efficacy_indicator = efficacyindicator.EfficacyIndicator(
@@ -187,11 +188,11 @@ class ActionPlan(base.APIBase):
strategy = None
try:
if utils.is_uuid_like(value) or utils.is_int_like(value):
strategy = objects.Strategy.get(
pecan.request.context, value)
strategy = objects.Strategy.get(pecan.request.context, value)
else:
strategy = objects.Strategy.get_by_name(
pecan.request.context, value)
pecan.request.context, value
)
except exception.StrategyNotFound:
pass
if strategy:
@@ -221,22 +222,27 @@ class ActionPlan(base.APIBase):
uuid = wtypes.wsattr(types.uuid, readonly=True)
"""Unique UUID for this action plan"""
audit_uuid = wtypes.wsproperty(types.uuid, _get_audit_uuid,
_set_audit_uuid,
mandatory=True)
audit_uuid = wtypes.wsproperty(
types.uuid, _get_audit_uuid, _set_audit_uuid, mandatory=True
)
"""The UUID of the audit this port belongs to"""
strategy_uuid = wtypes.wsproperty(
wtypes.text, _get_strategy_uuid, _set_strategy_uuid, mandatory=False)
wtypes.text, _get_strategy_uuid, _set_strategy_uuid, mandatory=False
)
"""Strategy UUID the action plan refers to"""
strategy_name = wtypes.wsproperty(
wtypes.text, _get_strategy_name, _set_strategy_name, mandatory=False)
wtypes.text, _get_strategy_name, _set_strategy_name, mandatory=False
)
"""The name of the strategy this action plan refers to"""
efficacy_indicators = wtypes.wsproperty(
types.jsontype, _get_efficacy_indicators, _set_efficacy_indicators,
mandatory=True)
types.jsontype,
_get_efficacy_indicators,
_set_efficacy_indicators,
mandatory=True,
)
"""The list of efficacy indicators associated to this action plan"""
global_efficacy = wtypes.wsattr(types.jsontype, readonly=True)
@@ -278,40 +284,60 @@ class ActionPlan(base.APIBase):
def _convert_with_links(action_plan, url, expand=True):
if not expand:
action_plan.unset_fields_except(
['uuid', 'state', 'efficacy_indicators', 'global_efficacy',
'updated_at', 'audit_uuid', 'strategy_uuid', 'strategy_name'])
[
'uuid',
'state',
'efficacy_indicators',
'global_efficacy',
'updated_at',
'audit_uuid',
'strategy_uuid',
'strategy_name',
]
)
action_plan.links = [
link.Link.make_link('self', url, 'action_plans', action_plan.uuid),
link.Link.make_link(
'self', url,
'action_plans', action_plan.uuid),
link.Link.make_link(
'bookmark', url,
'action_plans', action_plan.uuid,
bookmark=True)]
'bookmark',
url,
'action_plans',
action_plan.uuid,
bookmark=True,
),
]
return action_plan
@classmethod
def convert_with_links(cls, rpc_action_plan, expand=True):
action_plan = ActionPlan(**rpc_action_plan.as_dict())
hide_fields_in_newer_versions(action_plan)
return cls._convert_with_links(action_plan, pecan.request.host_url,
expand)
return cls._convert_with_links(
action_plan, pecan.request.host_url, expand
)
@classmethod
def sample(cls, expand=True):
sample = cls(uuid='9ef4d84c-41e8-4418-9220-ce55be0436af',
state='ONGOING',
created_at=timeutils.utcnow(),
deleted_at=None,
updated_at=timeutils.utcnow())
sample = cls(
uuid='9ef4d84c-41e8-4418-9220-ce55be0436af',
state='ONGOING',
created_at=timeutils.utcnow(),
deleted_at=None,
updated_at=timeutils.utcnow(),
)
sample._audit_uuid = 'abcee106-14d3-4515-b744-5a26885cf6f6'
sample._efficacy_indicators = [{'description': 'Test indicator',
'name': 'test_indicator',
'unit': '%'}]
sample._global_efficacy = {'description': 'Global efficacy',
'name': 'test_global_efficacy',
'unit': '%'}
sample._efficacy_indicators = [
{
'description': 'Test indicator',
'name': 'test_indicator',
'unit': '%',
}
]
sample._global_efficacy = {
'description': 'Global efficacy',
'name': 'test_global_efficacy',
'unit': '%',
}
return cls._convert_with_links(sample, 'http://localhost:9322', expand)
@@ -325,11 +351,13 @@ class ActionPlanCollection(collection.Collection):
self._type = 'action_plans'
@staticmethod
def convert_with_links(rpc_action_plans, limit, url=None, expand=False,
**kwargs):
def convert_with_links(
rpc_action_plans, limit, url=None, expand=False, **kwargs
):
ap_collection = ActionPlanCollection()
ap_collection.action_plans = [ActionPlan.convert_with_links(
p, expand) for p in rpc_action_plans]
ap_collection.action_plans = [
ActionPlan.convert_with_links(p, expand) for p in rpc_action_plans
]
ap_collection.next = ap_collection.get_next(limit, url=url, **kwargs)
return ap_collection
@@ -347,26 +375,32 @@ class ActionPlansController(rest.RestController):
super().__init__()
self.applier_client = rpcapi.ApplierAPI()
_custom_actions = {
'start': ['POST'],
'detail': ['GET']
}
_custom_actions = {'start': ['POST'], 'detail': ['GET']}
def _get_action_plans_collection(self, marker, limit,
sort_key, sort_dir, expand=False,
resource_url=None, audit_uuid=None,
strategy=None):
def _get_action_plans_collection(
self,
marker,
limit,
sort_key,
sort_dir,
expand=False,
resource_url=None,
audit_uuid=None,
strategy=None,
):
additional_fields = ['audit_uuid', 'strategy_uuid', 'strategy_name']
api_utils.validate_sort_key(
sort_key, list(objects.ActionPlan.fields) + additional_fields)
sort_key, list(objects.ActionPlan.fields) + additional_fields
)
limit = api_utils.validate_limit(limit)
api_utils.validate_sort_dir(sort_dir)
marker_obj = None
if marker:
marker_obj = objects.ActionPlan.get_by_uuid(
pecan.request.context, marker)
pecan.request.context, marker
)
filters = {}
if audit_uuid:
@@ -378,31 +412,54 @@ class ActionPlansController(rest.RestController):
else:
filters['strategy_name'] = strategy
need_api_sort = api_utils.check_need_api_sort(sort_key,
additional_fields)
sort_db_key = (sort_key if not need_api_sort
else None)
need_api_sort = api_utils.check_need_api_sort(
sort_key, additional_fields
)
sort_db_key = sort_key if not need_api_sort else None
action_plans = objects.ActionPlan.list(
pecan.request.context,
limit,
marker_obj, sort_key=sort_db_key,
sort_dir=sort_dir, filters=filters)
marker_obj,
sort_key=sort_db_key,
sort_dir=sort_dir,
filters=filters,
)
action_plans_collection = ActionPlanCollection.convert_with_links(
action_plans, limit, url=resource_url, expand=expand,
sort_key=sort_key, sort_dir=sort_dir)
action_plans,
limit,
url=resource_url,
expand=expand,
sort_key=sort_key,
sort_dir=sort_dir,
)
if need_api_sort:
api_utils.make_api_sort(action_plans_collection.action_plans,
sort_key, sort_dir)
api_utils.make_api_sort(
action_plans_collection.action_plans, sort_key, sort_dir
)
return action_plans_collection
@wsme_pecan.wsexpose(ActionPlanCollection, types.uuid, int, wtypes.text,
wtypes.text, types.uuid, wtypes.text)
def get_all(self, marker=None, limit=None,
sort_key='id', sort_dir='asc', audit_uuid=None, strategy=None):
@wsme_pecan.wsexpose(
ActionPlanCollection,
types.uuid,
int,
wtypes.text,
wtypes.text,
types.uuid,
wtypes.text,
)
def get_all(
self,
marker=None,
limit=None,
sort_key='id',
sort_dir='asc',
audit_uuid=None,
strategy=None,
):
"""Retrieve a list of action plans.
:param marker: pagination marker for large data sets.
@@ -414,17 +471,37 @@ class ActionPlansController(rest.RestController):
:param strategy: strategy UUID or name to filter by
"""
context = pecan.request.context
policy.enforce(context, 'action_plan:get_all',
action='action_plan:get_all')
policy.enforce(
context, 'action_plan:get_all', action='action_plan:get_all'
)
return self._get_action_plans_collection(
marker, limit, sort_key, sort_dir,
audit_uuid=audit_uuid, strategy=strategy)
marker,
limit,
sort_key,
sort_dir,
audit_uuid=audit_uuid,
strategy=strategy,
)
@wsme_pecan.wsexpose(ActionPlanCollection, types.uuid, int, wtypes.text,
wtypes.text, types.uuid, wtypes.text)
def detail(self, marker=None, limit=None,
sort_key='id', sort_dir='asc', audit_uuid=None, strategy=None):
@wsme_pecan.wsexpose(
ActionPlanCollection,
types.uuid,
int,
wtypes.text,
wtypes.text,
types.uuid,
wtypes.text,
)
def detail(
self,
marker=None,
limit=None,
sort_key='id',
sort_dir='asc',
audit_uuid=None,
strategy=None,
):
"""Retrieve a list of action_plans with detail.
:param marker: pagination marker for large data sets.
@@ -436,8 +513,9 @@ class ActionPlansController(rest.RestController):
:param strategy: strategy UUID or name to filter by
"""
context = pecan.request.context
policy.enforce(context, 'action_plan:detail',
action='action_plan:detail')
policy.enforce(
context, 'action_plan:detail', action='action_plan:detail'
)
# NOTE(lucasagomes): /detail should only work against collections
parent = pecan.request.path.split('/')[:-1][-1]
@@ -447,8 +525,15 @@ class ActionPlansController(rest.RestController):
expand = True
resource_url = '/'.join(['action_plans', 'detail'])
return self._get_action_plans_collection(
marker, limit, sort_key, sort_dir, expand,
resource_url, audit_uuid=audit_uuid, strategy=strategy)
marker,
limit,
sort_key,
sort_dir,
expand,
resource_url,
audit_uuid=audit_uuid,
strategy=strategy,
)
@wsme_pecan.wsexpose(ActionPlan, types.uuid)
def get_one(self, action_plan_uuid):
@@ -459,7 +544,8 @@ class ActionPlansController(rest.RestController):
context = pecan.request.context
action_plan = api_utils.get_resource('ActionPlan', action_plan_uuid)
policy.enforce(
context, 'action_plan:get', action_plan, action='action_plan:get')
context, 'action_plan:get', action_plan, action='action_plan:get'
)
return ActionPlan.convert_with_links(action_plan)
@@ -471,24 +557,29 @@ class ActionPlansController(rest.RestController):
"""
context = pecan.request.context
action_plan = api_utils.get_resource(
'ActionPlan', action_plan_uuid, eager=True)
policy.enforce(context, 'action_plan:delete', action_plan,
action='action_plan:delete')
'ActionPlan', action_plan_uuid, eager=True
)
policy.enforce(
context,
'action_plan:delete',
action_plan,
action='action_plan:delete',
)
allowed_states = (ap_objects.State.SUCCEEDED,
ap_objects.State.RECOMMENDED,
ap_objects.State.FAILED,
ap_objects.State.SUPERSEDED,
ap_objects.State.CANCELLED)
allowed_states = (
ap_objects.State.SUCCEEDED,
ap_objects.State.RECOMMENDED,
ap_objects.State.FAILED,
ap_objects.State.SUPERSEDED,
ap_objects.State.CANCELLED,
)
if action_plan.state not in allowed_states:
raise exception.DeleteError(
state=action_plan.state)
raise exception.DeleteError(state=action_plan.state)
action_plan.soft_delete()
@wsme.validate(types.uuid, [ActionPlanPatchType])
@wsme_pecan.wsexpose(ActionPlan, types.uuid,
body=[ActionPlanPatchType])
@wsme_pecan.wsexpose(ActionPlan, types.uuid, body=[ActionPlanPatchType])
def patch(self, action_plan_uuid, patch):
"""Update an existing action plan.
@@ -497,14 +588,20 @@ class ActionPlansController(rest.RestController):
"""
context = pecan.request.context
action_plan_to_update = api_utils.get_resource(
'ActionPlan', action_plan_uuid, eager=True)
policy.enforce(context, 'action_plan:update', action_plan_to_update,
action='action_plan:update')
'ActionPlan', action_plan_uuid, eager=True
)
policy.enforce(
context,
'action_plan:update',
action_plan_to_update,
action='action_plan:update',
)
try:
action_plan_dict = action_plan_to_update.as_dict()
action_plan = ActionPlan(**api_utils.apply_jsonpatch(
action_plan_dict, patch))
action_plan = ActionPlan(
**api_utils.apply_jsonpatch(action_plan_dict, patch)
)
except api_utils.JSONPATCH_EXCEPTIONS as e:
raise exception.PatchError(patch=patch, reason=e)
@@ -513,27 +610,28 @@ class ActionPlansController(rest.RestController):
# transitions that are allowed via PATCH
allowed_patch_transitions = [
(ap_objects.State.RECOMMENDED,
ap_objects.State.PENDING),
(ap_objects.State.RECOMMENDED,
ap_objects.State.CANCELLED),
(ap_objects.State.ONGOING,
ap_objects.State.CANCELLING),
(ap_objects.State.PENDING,
ap_objects.State.CANCELLED),
(ap_objects.State.RECOMMENDED, ap_objects.State.PENDING),
(ap_objects.State.RECOMMENDED, ap_objects.State.CANCELLED),
(ap_objects.State.ONGOING, ap_objects.State.CANCELLING),
(ap_objects.State.PENDING, ap_objects.State.CANCELLED),
]
# todo: improve this in blueprint watcher-api-validation
if hasattr(action_plan, 'state'):
transition = (action_plan_to_update.state, action_plan.state)
if transition not in allowed_patch_transitions:
error_message = _("State transition not allowed: "
"(%(initial_state)s -> %(new_state)s)")
error_message = _(
"State transition not allowed: "
"(%(initial_state)s -> %(new_state)s)"
)
raise exception.PatchError(
patch=patch,
reason=error_message % dict(
reason=error_message
% dict(
initial_state=action_plan_to_update.state,
new_state=action_plan.state))
new_state=action_plan.state,
),
)
if action_plan.state == ap_objects.State.PENDING:
launch_action_plan = True
@@ -552,8 +650,10 @@ class ActionPlansController(rest.RestController):
if action_plan_to_update[field] != patch_val:
action_plan_to_update[field] = patch_val
if (field == 'state' and
patch_val == objects.action_plan.State.PENDING):
if (
field == 'state'
and patch_val == objects.action_plan.State.PENDING
):
launch_action_plan = True
action_plan_to_update.save()
@@ -562,19 +662,21 @@ class ActionPlansController(rest.RestController):
# state update action state here only
if cancel_action_plan:
filters = {'action_plan_uuid': action_plan.uuid}
actions = objects.Action.list(pecan.request.context,
filters=filters, eager=True)
actions = objects.Action.list(
pecan.request.context, filters=filters, eager=True
)
for a in actions:
a.state = objects.action.State.CANCELLED
a.save()
if launch_action_plan:
self.applier_client.launch_action_plan(pecan.request.context,
action_plan.uuid)
self.applier_client.launch_action_plan(
pecan.request.context, action_plan.uuid
)
action_plan_to_update = objects.ActionPlan.get_by_uuid(
pecan.request.context,
action_plan_uuid)
pecan.request.context, action_plan_uuid
)
return ActionPlan.convert_with_links(action_plan_to_update)
@wsme_pecan.wsexpose(ActionPlan, types.uuid)
@@ -585,23 +687,31 @@ class ActionPlansController(rest.RestController):
"""
action_plan_to_start = api_utils.get_resource(
'ActionPlan', action_plan_uuid, eager=True)
'ActionPlan', action_plan_uuid, eager=True
)
context = pecan.request.context
policy.enforce(context, 'action_plan:start', action_plan_to_start,
action='action_plan:start')
policy.enforce(
context,
'action_plan:start',
action_plan_to_start,
action='action_plan:start',
)
if action_plan_to_start['state'] != \
objects.action_plan.State.RECOMMENDED:
raise exception.StartError(
state=action_plan_to_start.state)
if (
action_plan_to_start['state']
!= objects.action_plan.State.RECOMMENDED
):
raise exception.StartError(state=action_plan_to_start.state)
action_plan_to_start['state'] = objects.action_plan.State.PENDING
action_plan_to_start.save()
self.applier_client.launch_action_plan(pecan.request.context,
action_plan_uuid)
self.applier_client.launch_action_plan(
pecan.request.context, action_plan_uuid
)
action_plan_to_start = objects.ActionPlan.get_by_uuid(
pecan.request.context, action_plan_uuid)
pecan.request.context, action_plan_uuid
)
return ActionPlan.convert_with_links(action_plan_to_start)

View File

@@ -84,7 +84,6 @@ def hide_fields_in_newer_versions(obj):
class AuditPostType(wtypes.Base):
name = wtypes.wsattr(wtypes.text, mandatory=False)
audit_template_uuid = wtypes.wsattr(types.uuid, mandatory=False)
@@ -95,11 +94,13 @@ class AuditPostType(wtypes.Base):
audit_type = wtypes.wsattr(wtypes.text, mandatory=True)
state = wtypes.wsattr(wtypes.text, readonly=True,
default=objects.audit.State.PENDING)
state = wtypes.wsattr(
wtypes.text, readonly=True, default=objects.audit.State.PENDING
)
parameters = wtypes.wsattr({wtypes.text: types.jsontype}, mandatory=False,
default={})
parameters = wtypes.wsattr(
{wtypes.text: types.jsontype}, mandatory=False, default={}
)
interval = wtypes.wsattr(types.interval_or_cron, mandatory=False)
scope = wtypes.wsattr(types.jsontype, readonly=True)
@@ -120,28 +121,35 @@ class AuditPostType(wtypes.Base):
raise exception.AuditTypeNotFound(audit_type=self.audit_type)
if not self.audit_template_uuid and not self.goal:
message = _(
'A valid goal or audit_template_id must be provided')
message = _('A valid goal or audit_template_id must be provided')
raise exception.Invalid(message)
if (self.audit_type == objects.audit.AuditType.ONESHOT.value and
self.interval not in (wtypes.Unset, None)):
if (
self.audit_type == objects.audit.AuditType.ONESHOT.value
and self.interval not in (wtypes.Unset, None)
):
raise exception.AuditIntervalNotAllowed(audit_type=self.audit_type)
if (self.audit_type == objects.audit.AuditType.CONTINUOUS.value and
self.interval in (wtypes.Unset, None)):
if (
self.audit_type == objects.audit.AuditType.CONTINUOUS.value
and self.interval in (wtypes.Unset, None)
):
raise exception.AuditIntervalNotSpecified(
audit_type=self.audit_type)
audit_type=self.audit_type
)
if self.audit_template_uuid and self.goal:
raise exception.Invalid('Either audit_template_uuid '
'or goal should be provided.')
raise exception.Invalid(
'Either audit_template_uuid or goal should be provided.'
)
if (self.audit_type == objects.audit.AuditType.ONESHOT.value and
(self.start_time not in (wtypes.Unset, None) or
self.end_time not in (wtypes.Unset, None))):
if self.audit_type == objects.audit.AuditType.ONESHOT.value and (
self.start_time not in (wtypes.Unset, None)
or self.end_time not in (wtypes.Unset, None)
):
raise exception.AuditStartEndTimeNotAllowed(
audit_type=self.audit_type)
audit_type=self.audit_type
)
if not api_utils.allow_start_end_audit_time():
for field in ('start_time', 'end_time'):
@@ -154,11 +162,14 @@ class AuditPostType(wtypes.Base):
if self.audit_template_uuid:
try:
audit_template = objects.AuditTemplate.get(
context, self.audit_template_uuid)
context, self.audit_template_uuid
)
except exception.AuditTemplateNotFound:
raise exception.Invalid(
message=_('The audit template UUID or name specified is '
'invalid'))
message=_(
'The audit template UUID or name specified is invalid'
)
)
at2a = {
'goal': 'goal_id',
'strategy': 'strategy_id',
@@ -178,12 +189,14 @@ class AuditPostType(wtypes.Base):
# Note: If audit name was not provided, used a default name
if not self.name:
if self.strategy:
strategy = _get_object_by_value(context, objects.Strategy,
self.strategy)
strategy = _get_object_by_value(
context, objects.Strategy, self.strategy
)
self.name = f"{strategy.name}-{timeutils.utcnow().isoformat()}"
elif self.audit_template_uuid:
audit_template = objects.AuditTemplate.get(
context, self.audit_template_uuid)
context, self.audit_template_uuid
)
timestamp = timeutils.utcnow().isoformat()
self.name = f"{audit_template.name}-{timestamp}"
else:
@@ -191,8 +204,7 @@ class AuditPostType(wtypes.Base):
self.name = f"{goal.name}-{timeutils.utcnow().isoformat()}"
# No more than 63 characters
if len(self.name) > 63:
LOG.warning("Audit: %s length exceeds 63 characters",
self.name)
LOG.warning("Audit: %s length exceeds 63 characters", self.name)
self.name = self.name[0:63]
return Audit(
@@ -206,11 +218,11 @@ class AuditPostType(wtypes.Base):
auto_trigger=self.auto_trigger,
start_time=self.start_time,
end_time=self.end_time,
force=self.force)
force=self.force,
)
class AuditPatchType(types.JsonPatchType):
@staticmethod
def mandatory_attrs():
return ['/audit_template_uuid', '/type']
@@ -225,19 +237,21 @@ class AuditPatchType(types.JsonPatchType):
@staticmethod
def validate(patch):
def is_new_state_none(p):
return p.path == '/state' and p.op == 'replace' and p.value is None
serialized_patch = {'path': patch.path,
'op': patch.op,
'value': patch.value}
if (patch.path in AuditPatchType.mandatory_attrs() or
is_new_state_none(patch)):
serialized_patch = {
'path': patch.path,
'op': patch.op,
'value': patch.value,
}
if patch.path in AuditPatchType.mandatory_attrs() or is_new_state_none(
patch
):
msg = _("%(field)s can't be updated.")
raise exception.PatchError(
patch=serialized_patch,
reason=msg % dict(field=patch.path))
patch=serialized_patch, reason=msg % dict(field=patch.path)
)
return types.JsonPatchType.validate(patch)
@@ -247,6 +261,7 @@ class Audit(base.APIBase):
This class enforces type checking and value constraints, and converts
between the internal object model and the API representation of an audit.
"""
_goal_uuid = None
_goal_name = None
_strategy_uuid = None
@@ -258,11 +273,9 @@ class Audit(base.APIBase):
goal = None
try:
if utils.is_uuid_like(value) or utils.is_int_like(value):
goal = objects.Goal.get(
pecan.request.context, value)
goal = objects.Goal.get(pecan.request.context, value)
else:
goal = objects.Goal.get_by_name(
pecan.request.context, value)
goal = objects.Goal.get_by_name(pecan.request.context, value)
except exception.GoalNotFound:
pass
if goal:
@@ -295,11 +308,11 @@ class Audit(base.APIBase):
strategy = None
try:
if utils.is_uuid_like(value) or utils.is_int_like(value):
strategy = objects.Strategy.get(
pecan.request.context, value)
strategy = objects.Strategy.get(pecan.request.context, value)
else:
strategy = objects.Strategy.get_by_name(
pecan.request.context, value)
pecan.request.context, value
)
except exception.StrategyNotFound:
pass
if strategy:
@@ -339,19 +352,23 @@ class Audit(base.APIBase):
"""This audit state"""
goal_uuid = wtypes.wsproperty(
wtypes.text, _get_goal_uuid, _set_goal_uuid, mandatory=True)
wtypes.text, _get_goal_uuid, _set_goal_uuid, mandatory=True
)
"""Goal UUID the audit refers to"""
goal_name = wtypes.wsproperty(
wtypes.text, _get_goal_name, _set_goal_name, mandatory=False)
wtypes.text, _get_goal_name, _set_goal_name, mandatory=False
)
"""The name of the goal this audit refers to"""
strategy_uuid = wtypes.wsproperty(
wtypes.text, _get_strategy_uuid, _set_strategy_uuid, mandatory=False)
wtypes.text, _get_strategy_uuid, _set_strategy_uuid, mandatory=False
)
"""Strategy UUID the audit refers to"""
strategy_name = wtypes.wsproperty(
wtypes.text, _get_strategy_name, _set_strategy_name, mandatory=False)
wtypes.text, _get_strategy_name, _set_strategy_name, mandatory=False
)
"""The name of the strategy this audit refers to"""
parameters = {wtypes.text: types.jsontype}
@@ -401,33 +418,40 @@ class Audit(base.APIBase):
self.fields.append('goal_id')
self.fields.append('strategy_id')
fields.append('goal_uuid')
setattr(self, 'goal_uuid', kwargs.get('goal_id',
wtypes.Unset))
setattr(self, 'goal_uuid', kwargs.get('goal_id', wtypes.Unset))
fields.append('goal_name')
setattr(self, 'goal_name', kwargs.get('goal_id',
wtypes.Unset))
setattr(self, 'goal_name', kwargs.get('goal_id', wtypes.Unset))
fields.append('strategy_uuid')
setattr(self, 'strategy_uuid', kwargs.get('strategy_id',
wtypes.Unset))
setattr(self, 'strategy_uuid', kwargs.get('strategy_id', wtypes.Unset))
fields.append('strategy_name')
setattr(self, 'strategy_name', kwargs.get('strategy_id',
wtypes.Unset))
setattr(self, 'strategy_name', kwargs.get('strategy_id', wtypes.Unset))
@staticmethod
def _convert_with_links(audit, url, expand=True):
if not expand:
audit.unset_fields_except(['uuid', 'name', 'audit_type', 'state',
'goal_uuid', 'interval', 'scope',
'strategy_uuid', 'goal_name',
'strategy_name', 'auto_trigger',
'next_run_time'])
audit.unset_fields_except(
[
'uuid',
'name',
'audit_type',
'state',
'goal_uuid',
'interval',
'scope',
'strategy_uuid',
'goal_name',
'strategy_name',
'auto_trigger',
'next_run_time',
]
)
audit.links = [link.Link.make_link('self', url,
'audits', audit.uuid),
link.Link.make_link('bookmark', url,
'audits', audit.uuid,
bookmark=True)
]
audit.links = [
link.Link.make_link('self', url, 'audits', audit.uuid),
link.Link.make_link(
'bookmark', url, 'audits', audit.uuid, bookmark=True
),
]
return audit
@@ -439,19 +463,21 @@ class Audit(base.APIBase):
@classmethod
def sample(cls, expand=True):
sample = cls(uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c',
name='My Audit',
audit_type='ONESHOT',
state='PENDING',
created_at=timeutils.utcnow(),
deleted_at=None,
updated_at=timeutils.utcnow(),
interval='7200',
scope=[],
auto_trigger=False,
next_run_time=timeutils.utcnow(),
start_time=timeutils.utcnow(),
end_time=timeutils.utcnow())
sample = cls(
uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c',
name='My Audit',
audit_type='ONESHOT',
state='PENDING',
created_at=timeutils.utcnow(),
deleted_at=None,
updated_at=timeutils.utcnow(),
interval='7200',
scope=[],
auto_trigger=False,
next_run_time=timeutils.utcnow(),
start_time=timeutils.utcnow(),
end_time=timeutils.utcnow(),
)
sample.goal_id = '7ae81bb3-dec3-4289-8d6c-da80bd8001ae'
sample.strategy_id = '7ae81bb3-dec3-4289-8d6c-da80bd8001ff'
@@ -470,11 +496,13 @@ class AuditCollection(collection.Collection):
self._type = 'audits'
@staticmethod
def convert_with_links(rpc_audits, limit, url=None, expand=False,
**kwargs):
def convert_with_links(
rpc_audits, limit, url=None, expand=False, **kwargs
):
collection = AuditCollection()
collection.audits = [Audit.convert_with_links(p, expand)
for p in rpc_audits]
collection.audits = [
Audit.convert_with_links(p, expand) for p in rpc_audits
]
collection.next = collection.get_next(limit, url=url, **kwargs)
return collection
@@ -492,26 +520,37 @@ class AuditsController(rest.RestController):
super().__init__()
self.dc_client = rpcapi.DecisionEngineAPI()
_custom_actions = {
'detail': ['GET'],
}
_custom_actions = {'detail': ['GET']}
def _get_audits_collection(self, marker, limit,
sort_key, sort_dir, expand=False,
resource_url=None, goal=None,
strategy=None):
additional_fields = ["goal_uuid", "goal_name", "strategy_uuid",
"strategy_name"]
def _get_audits_collection(
self,
marker,
limit,
sort_key,
sort_dir,
expand=False,
resource_url=None,
goal=None,
strategy=None,
):
additional_fields = [
"goal_uuid",
"goal_name",
"strategy_uuid",
"strategy_name",
]
api_utils.validate_sort_key(
sort_key, list(objects.Audit.fields) + additional_fields)
sort_key, list(objects.Audit.fields) + additional_fields
)
limit = api_utils.validate_limit(limit)
api_utils.validate_sort_dir(sort_dir)
marker_obj = None
if marker:
marker_obj = objects.Audit.get_by_uuid(pecan.request.context,
marker)
marker_obj = objects.Audit.get_by_uuid(
pecan.request.context, marker
)
filters = {}
if goal:
@@ -528,30 +567,54 @@ class AuditsController(rest.RestController):
# TODO(michaelgugino): add method to get goal by name.
filters['strategy_name'] = strategy
need_api_sort = api_utils.check_need_api_sort(sort_key,
additional_fields)
sort_db_key = (sort_key if not need_api_sort
else None)
need_api_sort = api_utils.check_need_api_sort(
sort_key, additional_fields
)
sort_db_key = sort_key if not need_api_sort else None
audits = objects.Audit.list(pecan.request.context,
limit,
marker_obj, sort_key=sort_db_key,
sort_dir=sort_dir, filters=filters)
audits = objects.Audit.list(
pecan.request.context,
limit,
marker_obj,
sort_key=sort_db_key,
sort_dir=sort_dir,
filters=filters,
)
audits_collection = AuditCollection.convert_with_links(
audits, limit, url=resource_url, expand=expand,
sort_key=sort_key, sort_dir=sort_dir)
audits,
limit,
url=resource_url,
expand=expand,
sort_key=sort_key,
sort_dir=sort_dir,
)
if need_api_sort:
api_utils.make_api_sort(audits_collection.audits, sort_key,
sort_dir)
api_utils.make_api_sort(
audits_collection.audits, sort_key, sort_dir
)
return audits_collection
@wsme_pecan.wsexpose(AuditCollection, types.uuid, int, wtypes.text,
wtypes.text, wtypes.text, wtypes.text)
def get_all(self, marker=None, limit=None, sort_key='id', sort_dir='asc',
goal=None, strategy=None):
@wsme_pecan.wsexpose(
AuditCollection,
types.uuid,
int,
wtypes.text,
wtypes.text,
wtypes.text,
wtypes.text,
)
def get_all(
self,
marker=None,
limit=None,
sort_key='id',
sort_dir='asc',
goal=None,
strategy=None,
):
"""Retrieve a list of audits.
:param marker: pagination marker for large data sets.
@@ -563,17 +626,18 @@ class AuditsController(rest.RestController):
"""
context = pecan.request.context
policy.enforce(context, 'audit:get_all',
action='audit:get_all')
policy.enforce(context, 'audit:get_all', action='audit:get_all')
return self._get_audits_collection(marker, limit, sort_key,
sort_dir, goal=goal,
strategy=strategy)
return self._get_audits_collection(
marker, limit, sort_key, sort_dir, goal=goal, strategy=strategy
)
@wsme_pecan.wsexpose(AuditCollection, wtypes.text, types.uuid, int,
wtypes.text, wtypes.text)
def detail(self, goal=None, marker=None, limit=None,
sort_key='id', sort_dir='asc'):
@wsme_pecan.wsexpose(
AuditCollection, wtypes.text, types.uuid, int, wtypes.text, wtypes.text
)
def detail(
self, goal=None, marker=None, limit=None, sort_key='id', sort_dir='asc'
):
"""Retrieve a list of audits with detail.
:param goal: goal UUID or name to filter by
@@ -583,8 +647,7 @@ class AuditsController(rest.RestController):
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
"""
context = pecan.request.context
policy.enforce(context, 'audit:detail',
action='audit:detail')
policy.enforce(context, 'audit:detail', action='audit:detail')
# NOTE(lucasagomes): /detail should only work against collections
parent = pecan.request.path.split('/')[:-1][-1]
if parent != "audits":
@@ -592,10 +655,9 @@ class AuditsController(rest.RestController):
expand = True
resource_url = '/'.join(['audits', 'detail'])
return self._get_audits_collection(marker, limit,
sort_key, sort_dir, expand,
resource_url,
goal=goal)
return self._get_audits_collection(
marker, limit, sort_key, sort_dir, expand, resource_url, goal=goal
)
@wsme_pecan.wsexpose(Audit, wtypes.text)
def get_one(self, audit):
@@ -609,39 +671,46 @@ class AuditsController(rest.RestController):
return Audit.convert_with_links(rpc_audit)
@wsme_pecan.wsexpose(Audit, body=AuditPostType,
status_code=HTTPStatus.CREATED)
@wsme_pecan.wsexpose(
Audit, body=AuditPostType, status_code=HTTPStatus.CREATED
)
def post(self, audit_p):
"""Create a new audit.
:param audit_p: an audit within the request body.
"""
context = pecan.request.context
policy.enforce(context, 'audit:create',
action='audit:create')
policy.enforce(context, 'audit:create', action='audit:create')
audit = audit_p.as_audit(context)
strategy_uuid = audit.strategy_uuid
no_schema = True
if strategy_uuid is not None:
# validate parameter when predefined strategy in audit template
strategy = objects.Strategy.get(pecan.request.context,
strategy_uuid)
strategy = objects.Strategy.get(
pecan.request.context, strategy_uuid
)
schema = strategy.parameters_spec
if schema:
# validate input parameter with default value feedback
no_schema = False
try:
utils.StrictDefaultValidatingDraft4Validator(
schema).validate(audit.parameters)
schema
).validate(audit.parameters)
except jsonschema.exceptions.ValidationError as e:
raise exception.Invalid(
_('Invalid parameters for strategy: %s') % e)
_('Invalid parameters for strategy: %s') % e
)
if no_schema and audit.parameters:
raise exception.Invalid(_('Specify parameters but no predefined '
'strategy for audit, or no '
'parameter spec in predefined strategy'))
raise exception.Invalid(
_(
'Specify parameters but no predefined '
'strategy for audit, or no '
'parameter spec in predefined strategy'
)
)
audit_dict = audit.as_dict()
# convert local time to UTC time
@@ -649,10 +718,12 @@ class AuditsController(rest.RestController):
end_time_value = audit_dict.get('end_time')
if start_time_value:
audit_dict['start_time'] = start_time_value.astimezone(
timezone.utc).replace(tzinfo=None)
timezone.utc
).replace(tzinfo=None)
if end_time_value:
audit_dict['end_time'] = end_time_value.astimezone(
timezone.utc).replace(tzinfo=None)
timezone.utc
).replace(tzinfo=None)
new_audit = objects.Audit(context, **audit_dict)
new_audit.create()
@@ -675,10 +746,10 @@ class AuditsController(rest.RestController):
:param patch: a json PATCH document to apply to this audit.
"""
context = pecan.request.context
audit_to_update = api_utils.get_resource(
'Audit', audit, eager=True)
policy.enforce(context, 'audit:update', audit_to_update,
action='audit:update')
audit_to_update = api_utils.get_resource('Audit', audit, eager=True)
policy.enforce(
context, 'audit:update', audit_to_update, action='audit:update'
)
try:
audit_dict = audit_to_update.as_dict()
@@ -686,21 +757,27 @@ class AuditsController(rest.RestController):
initial_state = audit_dict['state']
new_state = api_utils.get_patch_value(patch, 'state')
if not api_utils.check_audit_state_transition(
patch, initial_state):
error_message = _("State transition not allowed: "
"(%(initial_state)s -> %(new_state)s)")
patch, initial_state
):
error_message = _(
"State transition not allowed: "
"(%(initial_state)s -> %(new_state)s)"
)
raise exception.PatchError(
patch=patch,
reason=error_message % dict(
initial_state=initial_state, new_state=new_state))
reason=error_message
% dict(initial_state=initial_state, new_state=new_state),
)
patch_path = api_utils.get_patch_key(patch, 'path')
if patch_path in ('start_time', 'end_time'):
patch_value = api_utils.get_patch_value(patch, patch_path)
# convert string format to UTC time
new_patch_value = wutils.parse_isodatetime(
patch_value).astimezone(
timezone.utc).replace(tzinfo=None)
new_patch_value = (
wutils.parse_isodatetime(patch_value)
.astimezone(timezone.utc)
.replace(tzinfo=None)
)
api_utils.set_patch_value(patch, patch_path, new_patch_value)
audit = Audit(**api_utils.apply_jsonpatch(audit_dict, patch))
@@ -729,16 +806,16 @@ class AuditsController(rest.RestController):
:param audit: UUID or name of an audit.
"""
context = pecan.request.context
audit_to_delete = api_utils.get_resource(
'Audit', audit, eager=True)
policy.enforce(context, 'audit:delete', audit_to_delete,
action='audit:delete')
audit_to_delete = api_utils.get_resource('Audit', audit, eager=True)
policy.enforce(
context, 'audit:delete', audit_to_delete, action='audit:delete'
)
initial_state = audit_to_delete.state
new_state = objects.audit.State.DELETED
if not objects.audit.AuditStateTransitionManager(
).check_transition(initial_state, new_state):
raise exception.DeleteError(
state=initial_state)
if not objects.audit.AuditStateTransitionManager().check_transition(
initial_state, new_state
):
raise exception.DeleteError(state=initial_state)
audit_to_delete.soft_delete()

View File

@@ -113,17 +113,19 @@ class AuditTemplatePostType(wtypes.Base):
"items": {
"type": "object",
"properties": AuditTemplatePostType._get_schemas(),
"additionalProperties": False
}
"additionalProperties": False,
},
}
return SCHEMA
@staticmethod
def _get_schemas():
collectors = default_loading.ClusterDataModelCollectorLoader(
).list_available()
schemas = {k: c.SCHEMA for k, c
in collectors.items() if hasattr(c, "SCHEMA")}
collectors = (
default_loading.ClusterDataModelCollectorLoader().list_available()
)
schemas = {
k: c.SCHEMA for k, c in collectors.items() if hasattr(c, "SCHEMA")
}
return schemas
@staticmethod
@@ -144,7 +146,7 @@ class AuditTemplatePostType(wtypes.Base):
audit_template.scope = [dict(compute=audit_template.scope)]
common_utils.Draft4Validator(
AuditTemplatePostType._build_schema()
).validate(audit_template.scope)
).validate(audit_template.scope)
include_host_aggregates = False
exclude_host_aggregates = False
@@ -159,34 +161,47 @@ class AuditTemplatePostType(wtypes.Base):
raise exception.Invalid(
message=_(
"host_aggregates can't be "
"included and excluded together"))
"included and excluded together"
)
)
if audit_template.strategy:
try:
if (common_utils.is_uuid_like(audit_template.strategy) or
common_utils.is_int_like(audit_template.strategy)):
if common_utils.is_uuid_like(
audit_template.strategy
) or common_utils.is_int_like(audit_template.strategy):
strategy = objects.Strategy.get(
AuditTemplatePostType._ctx, audit_template.strategy)
AuditTemplatePostType._ctx, audit_template.strategy
)
else:
strategy = objects.Strategy.get_by_name(
AuditTemplatePostType._ctx, audit_template.strategy)
AuditTemplatePostType._ctx, audit_template.strategy
)
except Exception:
raise exception.InvalidStrategy(
strategy=audit_template.strategy)
strategy=audit_template.strategy
)
# Check that the strategy we indicate is actually related to the
# specified goal
if strategy.goal_id != goal.id:
available_strategies = objects.Strategy.list(
AuditTemplatePostType._ctx)
choices = [f"'{s.uuid}' ({s.name})"
for s in available_strategies]
AuditTemplatePostType._ctx
)
choices = [
f"'{s.uuid}' ({s.name})" for s in available_strategies
]
raise exception.InvalidStrategy(
message=_(
"'%(strategy)s' strategy does relate to the "
"'%(goal)s' goal. Possible choices: %(choices)s")
% dict(strategy=strategy.name, goal=goal.name,
choices=", ".join(choices)))
"'%(goal)s' goal. Possible choices: %(choices)s"
)
% dict(
strategy=strategy.name,
goal=goal.name,
choices=", ".join(choices),
)
)
audit_template.strategy = strategy.uuid
# We force the UUID so that we do not need to query the DB with the
@@ -197,7 +212,6 @@ class AuditTemplatePostType(wtypes.Base):
class AuditTemplatePatchType(types.JsonPatchType):
_ctx = context_utils.make_context()
@staticmethod
@@ -210,8 +224,8 @@ class AuditTemplatePatchType(types.JsonPatchType):
AuditTemplatePatchType._validate_goal(patch)
elif patch.path == "/goal" and patch.op == "remove":
raise wsme.exc.ClientSideError(
_("Cannot remove 'goal' attribute "
"from an audit template"))
_("Cannot remove 'goal' attribute from an audit template")
)
if patch.path == "/strategy":
AuditTemplatePatchType._validate_strategy(patch)
return types.JsonPatchType.validate(patch)
@@ -222,8 +236,7 @@ class AuditTemplatePatchType(types.JsonPatchType):
goal = patch.value
if goal:
available_goals = objects.Goal.list(
AuditTemplatePatchType._ctx)
available_goals = objects.Goal.list(AuditTemplatePatchType._ctx)
available_goal_uuids_map = {g.uuid: g for g in available_goals}
available_goal_names_map = {g.name: g for g in available_goals}
if goal in available_goal_uuids_map:
@@ -239,11 +252,14 @@ class AuditTemplatePatchType(types.JsonPatchType):
strategy = patch.value
if strategy:
available_strategies = objects.Strategy.list(
AuditTemplatePatchType._ctx)
AuditTemplatePatchType._ctx
)
available_strategy_uuids_map = {
s.uuid: s for s in available_strategies}
s.uuid: s for s in available_strategies
}
available_strategy_names_map = {
s.name: s for s in available_strategies}
s.name: s for s in available_strategies
}
if strategy in available_strategy_uuids_map:
patch.value = available_strategy_uuids_map[strategy].id
elif strategy in available_strategy_names_map:
@@ -271,13 +287,12 @@ class AuditTemplate(base.APIBase):
return None
goal = None
try:
if (common_utils.is_uuid_like(value) or
common_utils.is_int_like(value)):
goal = objects.Goal.get(
pecan.request.context, value)
if common_utils.is_uuid_like(value) or common_utils.is_int_like(
value
):
goal = objects.Goal.get(pecan.request.context, value)
else:
goal = objects.Goal.get_by_name(
pecan.request.context, value)
goal = objects.Goal.get_by_name(pecan.request.context, value)
except exception.GoalNotFound:
pass
if goal:
@@ -289,13 +304,14 @@ class AuditTemplate(base.APIBase):
return None
strategy = None
try:
if (common_utils.is_uuid_like(value) or
common_utils.is_int_like(value)):
strategy = objects.Strategy.get(
pecan.request.context, value)
if common_utils.is_uuid_like(value) or common_utils.is_int_like(
value
):
strategy = objects.Strategy.get(pecan.request.context, value)
else:
strategy = objects.Strategy.get_by_name(
pecan.request.context, value)
pecan.request.context, value
)
except exception.StrategyNotFound:
pass
if strategy:
@@ -352,19 +368,23 @@ class AuditTemplate(base.APIBase):
"""Short description of this audit template"""
goal_uuid = wtypes.wsproperty(
wtypes.text, _get_goal_uuid, _set_goal_uuid, mandatory=True)
wtypes.text, _get_goal_uuid, _set_goal_uuid, mandatory=True
)
"""Goal UUID the audit template refers to"""
goal_name = wtypes.wsproperty(
wtypes.text, _get_goal_name, _set_goal_name, mandatory=False)
wtypes.text, _get_goal_name, _set_goal_name, mandatory=False
)
"""The name of the goal this audit template refers to"""
strategy_uuid = wtypes.wsproperty(
wtypes.text, _get_strategy_uuid, _set_strategy_uuid, mandatory=False)
wtypes.text, _get_strategy_uuid, _set_strategy_uuid, mandatory=False
)
"""Strategy UUID the audit template refers to"""
strategy_name = wtypes.wsproperty(
wtypes.text, _get_strategy_name, _set_strategy_name, mandatory=False)
wtypes.text, _get_strategy_name, _set_strategy_name, mandatory=False
)
"""The name of the strategy this audit template refers to"""
audits = wtypes.wsattr([link.Link], readonly=True)
@@ -400,50 +420,64 @@ class AuditTemplate(base.APIBase):
self.fields.append('strategy_name')
setattr(self, 'goal_uuid', kwargs.get('goal_id', wtypes.Unset))
setattr(self, 'goal_name', kwargs.get('goal_id', wtypes.Unset))
setattr(self, 'strategy_uuid',
kwargs.get('strategy_id', wtypes.Unset))
setattr(self, 'strategy_name',
kwargs.get('strategy_id', wtypes.Unset))
setattr(self, 'strategy_uuid', kwargs.get('strategy_id', wtypes.Unset))
setattr(self, 'strategy_name', kwargs.get('strategy_id', wtypes.Unset))
@staticmethod
def _convert_with_links(audit_template, url, expand=True):
if not expand:
audit_template.unset_fields_except(
['uuid', 'name', 'goal_uuid', 'goal_name',
'scope', 'strategy_uuid', 'strategy_name'])
[
'uuid',
'name',
'goal_uuid',
'goal_name',
'scope',
'strategy_uuid',
'strategy_name',
]
)
# The numeric ID should not be exposed to
# the user, it's internal only.
audit_template.goal_id = wtypes.Unset
audit_template.strategy_id = wtypes.Unset
audit_template.links = [link.Link.make_link('self', url,
'audit_templates',
audit_template.uuid),
link.Link.make_link('bookmark', url,
'audit_templates',
audit_template.uuid,
bookmark=True)]
audit_template.links = [
link.Link.make_link(
'self', url, 'audit_templates', audit_template.uuid
),
link.Link.make_link(
'bookmark',
url,
'audit_templates',
audit_template.uuid,
bookmark=True,
),
]
return audit_template
@classmethod
def convert_with_links(cls, rpc_audit_template, expand=True):
audit_template = AuditTemplate(**rpc_audit_template.as_dict())
hide_fields_in_newer_versions(audit_template)
return cls._convert_with_links(audit_template, pecan.request.host_url,
expand)
return cls._convert_with_links(
audit_template, pecan.request.host_url, expand
)
@classmethod
def sample(cls, expand=True):
sample = cls(uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c',
name='My Audit Template',
description='Description of my audit template',
goal_uuid='83e44733-b640-40e2-8d8a-7dd3be7134e6',
strategy_uuid='367d826e-b6a4-4b70-bc44-c3f6fe1c9986',
created_at=timeutils.utcnow(),
deleted_at=None,
updated_at=timeutils.utcnow(),
scope=[],)
sample = cls(
uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c',
name='My Audit Template',
description='Description of my audit template',
goal_uuid='83e44733-b640-40e2-8d8a-7dd3be7134e6',
strategy_uuid='367d826e-b6a4-4b70-bc44-c3f6fe1c9986',
created_at=timeutils.utcnow(),
deleted_at=None,
updated_at=timeutils.utcnow(),
scope=[],
)
return cls._convert_with_links(sample, 'http://localhost:9322', expand)
@@ -458,12 +492,14 @@ class AuditTemplateCollection(collection.Collection):
self._type = 'audit_templates'
@staticmethod
def convert_with_links(rpc_audit_templates, limit, url=None, expand=False,
**kwargs):
def convert_with_links(
rpc_audit_templates, limit, url=None, expand=False, **kwargs
):
at_collection = AuditTemplateCollection()
at_collection.audit_templates = [
AuditTemplate.convert_with_links(p, expand)
for p in rpc_audit_templates]
for p in rpc_audit_templates
]
at_collection.next = at_collection.get_next(limit, url=url, **kwargs)
return at_collection
@@ -480,54 +516,90 @@ class AuditTemplatesController(rest.RestController):
def __init__(self):
super().__init__()
_custom_actions = {
'detail': ['GET'],
}
_custom_actions = {'detail': ['GET']}
def _get_audit_templates_collection(self, filters, marker, limit,
sort_key, sort_dir, expand=False,
resource_url=None):
additional_fields = ["goal_uuid", "goal_name", "strategy_uuid",
"strategy_name"]
def _get_audit_templates_collection(
self,
filters,
marker,
limit,
sort_key,
sort_dir,
expand=False,
resource_url=None,
):
additional_fields = [
"goal_uuid",
"goal_name",
"strategy_uuid",
"strategy_name",
]
api_utils.validate_sort_key(
sort_key, list(objects.AuditTemplate.fields) + additional_fields)
sort_key, list(objects.AuditTemplate.fields) + additional_fields
)
api_utils.validate_search_filters(
filters, list(objects.AuditTemplate.fields) + additional_fields)
filters, list(objects.AuditTemplate.fields) + additional_fields
)
limit = api_utils.validate_limit(limit)
api_utils.validate_sort_dir(sort_dir)
marker_obj = None
if marker:
marker_obj = objects.AuditTemplate.get_by_uuid(
pecan.request.context,
marker)
pecan.request.context, marker
)
need_api_sort = api_utils.check_need_api_sort(sort_key,
additional_fields)
sort_db_key = (sort_key if not need_api_sort
else None)
need_api_sort = api_utils.check_need_api_sort(
sort_key, additional_fields
)
sort_db_key = sort_key if not need_api_sort else None
audit_templates = objects.AuditTemplate.list(
pecan.request.context, filters, limit, marker_obj,
sort_key=sort_db_key, sort_dir=sort_dir)
pecan.request.context,
filters,
limit,
marker_obj,
sort_key=sort_db_key,
sort_dir=sort_dir,
)
audit_templates_collection = \
audit_templates_collection = (
AuditTemplateCollection.convert_with_links(
audit_templates, limit, url=resource_url, expand=expand,
sort_key=sort_key, sort_dir=sort_dir)
audit_templates,
limit,
url=resource_url,
expand=expand,
sort_key=sort_key,
sort_dir=sort_dir,
)
)
if need_api_sort:
api_utils.make_api_sort(
audit_templates_collection.audit_templates, sort_key,
sort_dir)
audit_templates_collection.audit_templates, sort_key, sort_dir
)
return audit_templates_collection
@wsme_pecan.wsexpose(AuditTemplateCollection, wtypes.text, wtypes.text,
types.uuid, int, wtypes.text, wtypes.text)
def get_all(self, goal=None, strategy=None, marker=None,
limit=None, sort_key='id', sort_dir='asc'):
@wsme_pecan.wsexpose(
AuditTemplateCollection,
wtypes.text,
wtypes.text,
types.uuid,
int,
wtypes.text,
wtypes.text,
)
def get_all(
self,
goal=None,
strategy=None,
marker=None,
limit=None,
sort_key='id',
sort_dir='asc',
):
"""Retrieve a list of audit templates.
:param goal: goal UUID or name to filter by
@@ -538,8 +610,9 @@ class AuditTemplatesController(rest.RestController):
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
"""
context = pecan.request.context
policy.enforce(context, 'audit_template:get_all',
action='audit_template:get_all')
policy.enforce(
context, 'audit_template:get_all', action='audit_template:get_all'
)
filters = {}
if goal:
if common_utils.is_uuid_like(goal):
@@ -554,12 +627,27 @@ class AuditTemplatesController(rest.RestController):
filters['strategy_name'] = strategy
return self._get_audit_templates_collection(
filters, marker, limit, sort_key, sort_dir)
filters, marker, limit, sort_key, sort_dir
)
@wsme_pecan.wsexpose(AuditTemplateCollection, wtypes.text, wtypes.text,
types.uuid, int, wtypes.text, wtypes.text)
def detail(self, goal=None, strategy=None, marker=None,
limit=None, sort_key='id', sort_dir='asc'):
@wsme_pecan.wsexpose(
AuditTemplateCollection,
wtypes.text,
wtypes.text,
types.uuid,
int,
wtypes.text,
wtypes.text,
)
def detail(
self,
goal=None,
strategy=None,
marker=None,
limit=None,
sort_key='id',
sort_dir='asc',
):
"""Retrieve a list of audit templates with detail.
:param goal: goal UUID or name to filter by
@@ -570,8 +658,9 @@ class AuditTemplatesController(rest.RestController):
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
"""
context = pecan.request.context
policy.enforce(context, 'audit_template:detail',
action='audit_template:detail')
policy.enforce(
context, 'audit_template:detail', action='audit_template:detail'
)
# NOTE(lucasagomes): /detail should only work against collections
parent = pecan.request.path.split('/')[:-1][-1]
@@ -593,9 +682,9 @@ class AuditTemplatesController(rest.RestController):
expand = True
resource_url = '/'.join(['audit_templates', 'detail'])
return self._get_audit_templates_collection(filters, marker, limit,
sort_key, sort_dir, expand,
resource_url)
return self._get_audit_templates_collection(
filters, marker, limit, sort_key, sort_dir, expand, resource_url
)
@wsme_pecan.wsexpose(AuditTemplate, wtypes.text)
def get_one(self, audit_template):
@@ -604,16 +693,24 @@ class AuditTemplatesController(rest.RestController):
:param audit_template: UUID or name of an audit template.
"""
context = pecan.request.context
rpc_audit_template = api_utils.get_resource('AuditTemplate',
audit_template)
policy.enforce(context, 'audit_template:get', rpc_audit_template,
action='audit_template:get')
rpc_audit_template = api_utils.get_resource(
'AuditTemplate', audit_template
)
policy.enforce(
context,
'audit_template:get',
rpc_audit_template,
action='audit_template:get',
)
return AuditTemplate.convert_with_links(rpc_audit_template)
@wsme.validate(types.uuid, AuditTemplatePostType)
@wsme_pecan.wsexpose(AuditTemplate, body=AuditTemplatePostType,
status_code=HTTPStatus.CREATED)
@wsme_pecan.wsexpose(
AuditTemplate,
body=AuditTemplatePostType,
status_code=HTTPStatus.CREATED,
)
def post(self, audit_template_postdata):
"""Create a new audit template.
@@ -621,24 +718,28 @@ class AuditTemplatesController(rest.RestController):
from the request body.
"""
context = pecan.request.context
policy.enforce(context, 'audit_template:create',
action='audit_template:create')
policy.enforce(
context, 'audit_template:create', action='audit_template:create'
)
context = pecan.request.context
audit_template = audit_template_postdata.as_audit_template()
audit_template_dict = audit_template.as_dict()
new_audit_template = objects.AuditTemplate(context,
**audit_template_dict)
new_audit_template = objects.AuditTemplate(
context, **audit_template_dict
)
new_audit_template.create()
# Set the HTTP Location Header
pecan.response.location = link.build_url(
'audit_templates', new_audit_template.uuid)
'audit_templates', new_audit_template.uuid
)
return AuditTemplate.convert_with_links(new_audit_template)
@wsme.validate(types.uuid, [AuditTemplatePatchType])
@wsme_pecan.wsexpose(AuditTemplate, wtypes.text,
body=[AuditTemplatePatchType])
@wsme_pecan.wsexpose(
AuditTemplate, wtypes.text, body=[AuditTemplatePatchType]
)
def patch(self, audit_template, patch):
"""Update an existing audit template.
@@ -646,25 +747,30 @@ class AuditTemplatesController(rest.RestController):
:param patch: a json PATCH document to apply to this audit template.
"""
context = pecan.request.context
audit_template_to_update = api_utils.get_resource('AuditTemplate',
audit_template)
policy.enforce(context, 'audit_template:update',
audit_template_to_update,
action='audit_template:update')
audit_template_to_update = api_utils.get_resource(
'AuditTemplate', audit_template
)
policy.enforce(
context,
'audit_template:update',
audit_template_to_update,
action='audit_template:update',
)
if common_utils.is_uuid_like(audit_template):
audit_template_to_update = objects.AuditTemplate.get_by_uuid(
pecan.request.context,
audit_template)
pecan.request.context, audit_template
)
else:
audit_template_to_update = objects.AuditTemplate.get_by_name(
pecan.request.context,
audit_template)
pecan.request.context, audit_template
)
try:
audit_template_dict = audit_template_to_update.as_dict()
audit_template = AuditTemplate(**api_utils.apply_jsonpatch(
audit_template_dict, patch))
audit_template = AuditTemplate(
**api_utils.apply_jsonpatch(audit_template_dict, patch)
)
except api_utils.JSONPATCH_EXCEPTIONS as e:
raise exception.PatchError(patch=patch, reason=e)
@@ -690,10 +796,14 @@ class AuditTemplatesController(rest.RestController):
:param template_uuid: UUID or name of an audit template.
"""
context = pecan.request.context
audit_template_to_delete = api_utils.get_resource('AuditTemplate',
audit_template)
policy.enforce(context, 'audit_template:delete',
audit_template_to_delete,
action='audit_template:delete')
audit_template_to_delete = api_utils.get_resource(
'AuditTemplate', audit_template
)
policy.enforce(
context,
'audit_template:delete',
audit_template_to_delete,
action='audit_template:delete',
)
audit_template_to_delete.soft_delete()

View File

@@ -23,7 +23,6 @@ from watcher.api.controllers import link
class Collection(base.APIBase):
next = wtypes.text
"""A link to retrieve the next subset of the collection"""
@@ -45,5 +44,6 @@ class Collection(base.APIBase):
marker = getattr(self.collection[-1], marker_field)
next_args = f'?{q_args}limit={limit:d}&marker={marker}'
return link.Link.make_link('next', pecan.request.host_url,
resource_url, next_args).href
return link.Link.make_link(
'next', pecan.request.host_url, resource_url, next_args
).href

View File

@@ -65,19 +65,18 @@ class DataModelController(rest.RestController):
"""
if not utils.allow_list_datamodel():
raise exception.NotAcceptable
allowed_data_model_type = [
'compute',
]
allowed_data_model_type = ['compute']
if data_model_type not in allowed_data_model_type:
raise exception.DataModelTypeNotFound(
data_model_type=data_model_type)
data_model_type=data_model_type
)
context = pecan.request.context
de_client = rpcapi.DecisionEngineAPI()
policy.enforce(context, 'data_model:get_all',
action='data_model:get_all')
policy.enforce(
context, 'data_model:get_all', action='data_model:get_all'
)
rpc_all_data_model = de_client.get_data_model_info(
context,
data_model_type,
audit_uuid)
context, data_model_type, audit_uuid
)
hide_fields_in_newer_versions(rpc_all_data_model)
return rpc_all_data_model

View File

@@ -93,14 +93,16 @@ class Goal(base.APIBase):
@staticmethod
def _convert_with_links(goal, url, expand=True):
if not expand:
goal.unset_fields_except(['uuid', 'name', 'display_name',
'efficacy_specification'])
goal.unset_fields_except(
['uuid', 'name', 'display_name', 'efficacy_specification']
)
goal.links = [link.Link.make_link('self', url,
'goals', goal.uuid),
link.Link.make_link('bookmark', url,
'goals', goal.uuid,
bookmark=True)]
goal.links = [
link.Link.make_link('self', url, 'goals', goal.uuid),
link.Link.make_link(
'bookmark', url, 'goals', goal.uuid, bookmark=True
),
]
return goal
@classmethod
@@ -116,11 +118,15 @@ class Goal(base.APIBase):
name='DUMMY',
display_name='Dummy strategy',
efficacy_specification=[
{'description': 'Dummy indicator', 'name': 'dummy',
'schema': 'Range(min=0, max=100, min_included=True, '
'max_included=True, msg=None)',
'unit': '%'}
])
{
'description': 'Dummy indicator',
'name': 'dummy',
'schema': 'Range(min=0, max=100, min_included=True, '
'max_included=True, msg=None)',
'unit': '%',
}
],
)
return cls._convert_with_links(sample, 'http://localhost:9322', expand)
@@ -135,13 +141,14 @@ class GoalCollection(collection.Collection):
self._type = 'goals'
@staticmethod
def convert_with_links(goals, limit, url=None, expand=False,
**kwargs):
def convert_with_links(goals, limit, url=None, expand=False, **kwargs):
goal_collection = GoalCollection()
goal_collection.goals = [
Goal.convert_with_links(g, expand) for g in goals]
Goal.convert_with_links(g, expand) for g in goals
]
goal_collection.next = goal_collection.get_next(
limit, url=url, **kwargs)
limit, url=url, **kwargs
)
return goal_collection
@classmethod
@@ -157,36 +164,49 @@ class GoalsController(rest.RestController):
def __init__(self):
super().__init__()
_custom_actions = {
'detail': ['GET'],
}
_custom_actions = {'detail': ['GET']}
def _get_goals_collection(self, marker, limit, sort_key, sort_dir,
expand=False, resource_url=None):
api_utils.validate_sort_key(
sort_key, list(objects.Goal.fields))
def _get_goals_collection(
self,
marker,
limit,
sort_key,
sort_dir,
expand=False,
resource_url=None,
):
api_utils.validate_sort_key(sort_key, list(objects.Goal.fields))
limit = api_utils.validate_limit(limit)
api_utils.validate_sort_dir(sort_dir)
marker_obj = None
if marker:
marker_obj = objects.Goal.get_by_uuid(
pecan.request.context, marker)
pecan.request.context, marker
)
sort_db_key = (sort_key if sort_key in objects.Goal.fields
else None)
sort_db_key = sort_key if sort_key in objects.Goal.fields else None
goals = objects.Goal.list(pecan.request.context, limit, marker_obj,
sort_key=sort_db_key, sort_dir=sort_dir)
goals = objects.Goal.list(
pecan.request.context,
limit,
marker_obj,
sort_key=sort_db_key,
sort_dir=sort_dir,
)
return GoalCollection.convert_with_links(goals, limit,
url=resource_url,
expand=expand,
sort_key=sort_key,
sort_dir=sort_dir)
return GoalCollection.convert_with_links(
goals,
limit,
url=resource_url,
expand=expand,
sort_key=sort_key,
sort_dir=sort_dir,
)
@wsme_pecan.wsexpose(GoalCollection, wtypes.text,
int, wtypes.text, wtypes.text)
@wsme_pecan.wsexpose(
GoalCollection, wtypes.text, int, wtypes.text, wtypes.text
)
def get_all(self, marker=None, limit=None, sort_key='id', sort_dir='asc'):
"""Retrieve a list of goals.
@@ -196,12 +216,12 @@ class GoalsController(rest.RestController):
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
"""
context = pecan.request.context
policy.enforce(context, 'goal:get_all',
action='goal:get_all')
policy.enforce(context, 'goal:get_all', action='goal:get_all')
return self._get_goals_collection(marker, limit, sort_key, sort_dir)
@wsme_pecan.wsexpose(GoalCollection, wtypes.text, int,
wtypes.text, wtypes.text)
@wsme_pecan.wsexpose(
GoalCollection, wtypes.text, int, wtypes.text, wtypes.text
)
def detail(self, marker=None, limit=None, sort_key='id', sort_dir='asc'):
"""Retrieve a list of goals with detail.
@@ -211,16 +231,16 @@ class GoalsController(rest.RestController):
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
"""
context = pecan.request.context
policy.enforce(context, 'goal:detail',
action='goal:detail')
policy.enforce(context, 'goal:detail', action='goal:detail')
# NOTE(lucasagomes): /detail should only work against collections
parent = pecan.request.path.split('/')[:-1][-1]
if parent != "goals":
raise exception.HTTPNotFound
expand = True
resource_url = '/'.join(['goals', 'detail'])
return self._get_goals_collection(marker, limit, sort_key, sort_dir,
expand, resource_url)
return self._get_goals_collection(
marker, limit, sort_key, sort_dir, expand, resource_url
)
@wsme_pecan.wsexpose(Goal, wtypes.text)
def get_one(self, goal):

View File

@@ -91,14 +91,14 @@ class ScoringEngine(base.APIBase):
@staticmethod
def _convert_with_links(se, url, expand=True):
if not expand:
se.unset_fields_except(
['uuid', 'name', 'description'])
se.unset_fields_except(['uuid', 'name', 'description'])
se.links = [link.Link.make_link('self', url,
'scoring_engines', se.uuid),
link.Link.make_link('bookmark', url,
'scoring_engines', se.uuid,
bookmark=True)]
se.links = [
link.Link.make_link('self', url, 'scoring_engines', se.uuid),
link.Link.make_link(
'bookmark', url, 'scoring_engines', se.uuid, bookmark=True
),
]
return se
@classmethod
@@ -106,13 +106,16 @@ class ScoringEngine(base.APIBase):
scoring_engine = ScoringEngine(**scoring_engine.as_dict())
hide_fields_in_newer_versions(scoring_engine)
return cls._convert_with_links(
scoring_engine, pecan.request.host_url, expand)
scoring_engine, pecan.request.host_url, expand
)
@classmethod
def sample(cls, expand=True):
sample = cls(uuid='81bbd3c7-3b08-4d12-a268-99354dbf7b71',
name='sample-se-123',
description='Sample Scoring Engine 123 just for testing')
sample = cls(
uuid='81bbd3c7-3b08-4d12-a268-99354dbf7b71',
name='sample-se-123',
description='Sample Scoring Engine 123 just for testing',
)
return cls._convert_with_links(sample, 'http://localhost:9322', expand)
@@ -127,12 +130,14 @@ class ScoringEngineCollection(collection.Collection):
self._type = 'scoring_engines'
@staticmethod
def convert_with_links(scoring_engines, limit, url=None, expand=False,
**kwargs):
def convert_with_links(
scoring_engines, limit, url=None, expand=False, **kwargs
):
collection = ScoringEngineCollection()
collection.scoring_engines = [ScoringEngine.convert_with_links(
se, expand) for se in scoring_engines]
collection.scoring_engines = [
ScoringEngine.convert_with_links(se, expand)
for se in scoring_engines
]
collection.next = collection.get_next(limit, url=url, **kwargs)
return collection
@@ -149,27 +154,34 @@ class ScoringEngineController(rest.RestController):
def __init__(self):
super().__init__()
_custom_actions = {
'detail': ['GET'],
}
_custom_actions = {'detail': ['GET']}
def _get_scoring_engines_collection(self, marker, limit,
sort_key, sort_dir, expand=False,
resource_url=None):
def _get_scoring_engines_collection(
self,
marker,
limit,
sort_key,
sort_dir,
expand=False,
resource_url=None,
):
api_utils.validate_sort_key(
sort_key, list(objects.ScoringEngine.fields))
sort_key, list(objects.ScoringEngine.fields)
)
limit = api_utils.validate_limit(limit)
api_utils.validate_sort_dir(sort_dir)
marker_obj = None
if marker:
marker_obj = objects.ScoringEngine.get_by_uuid(
pecan.request.context, marker)
pecan.request.context, marker
)
filters = {}
sort_db_key = (sort_key if sort_key in objects.ScoringEngine.fields
else None)
sort_db_key = (
sort_key if sort_key in objects.ScoringEngine.fields else None
)
scoring_engines = objects.ScoringEngine.list(
context=pecan.request.context,
@@ -177,7 +189,8 @@ class ScoringEngineController(rest.RestController):
marker=marker_obj,
sort_key=sort_db_key,
sort_dir=sort_dir,
filters=filters)
filters=filters,
)
return ScoringEngineCollection.convert_with_links(
scoring_engines,
@@ -185,12 +198,13 @@ class ScoringEngineController(rest.RestController):
url=resource_url,
expand=expand,
sort_key=sort_key,
sort_dir=sort_dir)
sort_dir=sort_dir,
)
@wsme_pecan.wsexpose(ScoringEngineCollection, wtypes.text,
int, wtypes.text, wtypes.text)
def get_all(self, marker=None, limit=None, sort_key='id',
sort_dir='asc'):
@wsme_pecan.wsexpose(
ScoringEngineCollection, wtypes.text, int, wtypes.text, wtypes.text
)
def get_all(self, marker=None, limit=None, sort_key='id', sort_dir='asc'):
"""Retrieve a list of Scoring Engines.
:param marker: pagination marker for large data sets.
@@ -199,14 +213,17 @@ class ScoringEngineController(rest.RestController):
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
"""
context = pecan.request.context
policy.enforce(context, 'scoring_engine:get_all',
action='scoring_engine:get_all')
policy.enforce(
context, 'scoring_engine:get_all', action='scoring_engine:get_all'
)
return self._get_scoring_engines_collection(
marker, limit, sort_key, sort_dir)
marker, limit, sort_key, sort_dir
)
@wsme_pecan.wsexpose(ScoringEngineCollection, wtypes.text,
int, wtypes.text, wtypes.text)
@wsme_pecan.wsexpose(
ScoringEngineCollection, wtypes.text, int, wtypes.text, wtypes.text
)
def detail(self, marker=None, limit=None, sort_key='id', sort_dir='asc'):
"""Retrieve a list of Scoring Engines with detail.
@@ -216,8 +233,9 @@ class ScoringEngineController(rest.RestController):
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
"""
context = pecan.request.context
policy.enforce(context, 'scoring_engine:detail',
action='scoring_engine:detail')
policy.enforce(
context, 'scoring_engine:detail', action='scoring_engine:detail'
)
parent = pecan.request.path.split('/')[:-1][-1]
if parent != "scoring_engines":
@@ -225,7 +243,8 @@ class ScoringEngineController(rest.RestController):
expand = True
resource_url = '/'.join(['scoring_engines', 'detail'])
return self._get_scoring_engines_collection(
marker, limit, sort_key, sort_dir, expand, resource_url)
marker, limit, sort_key, sort_dir, expand, resource_url
)
@wsme_pecan.wsexpose(ScoringEngine, wtypes.text)
def get_one(self, scoring_engine):
@@ -234,9 +253,11 @@ class ScoringEngineController(rest.RestController):
:param scoring_engine_name: The name of the Scoring Engine.
"""
context = pecan.request.context
policy.enforce(context, 'scoring_engine:get',
action='scoring_engine:get')
policy.enforce(
context, 'scoring_engine:get', action='scoring_engine:get'
)
rpc_scoring_engine = api_utils.get_resource(
'ScoringEngine', scoring_engine)
'ScoringEngine', scoring_engine
)
return ScoringEngine.convert_with_links(rpc_scoring_engine)

View File

@@ -67,8 +67,9 @@ class Service(base.APIBase):
def _set_status(self, id):
service = objects.Service.get(pecan.request.context, id)
last_heartbeat = (service.last_seen_up or service.updated_at or
service.created_at)
last_heartbeat = (
service.last_seen_up or service.updated_at or service.created_at
)
if isinstance(last_heartbeat, str):
# NOTE(russellb) If this service came in over rpc via
# conductor, then the timestamp will be a string and needs to be
@@ -81,12 +82,17 @@ class Service(base.APIBase):
elapsed = timeutils.delta_seconds(last_heartbeat, timeutils.utcnow())
is_up = abs(elapsed) <= CONF.service_down_time
if not is_up:
LOG.warning('Seems service %(name)s on host %(host)s is down. '
'Last heartbeat was %(lhb)s.'
'Elapsed time is %(el)s',
{'name': service.name,
'host': service.host,
'lhb': str(last_heartbeat), 'el': str(elapsed)})
LOG.warning(
'Seems service %(name)s on host %(host)s is down. '
'Last heartbeat was %(lhb)s.'
'Elapsed time is %(el)s',
{
'name': service.name,
'host': service.host,
'lhb': str(last_heartbeat),
'el': str(elapsed),
},
)
self._status = objects.service.ServiceStatus.FAILED
else:
self._status = objects.service.ServiceStatus.ACTIVE
@@ -103,8 +109,9 @@ class Service(base.APIBase):
last_seen_up = wtypes.wsattr(datetime.datetime, readonly=True)
"""Time when Watcher service sent latest heartbeat."""
status = wtypes.wsproperty(wtypes.text, _get_status, _set_status,
mandatory=True)
status = wtypes.wsproperty(
wtypes.text, _get_status, _set_status, mandatory=True
)
links = wtypes.wsattr([link.Link], readonly=True)
"""A list containing a self link."""
@@ -116,34 +123,39 @@ class Service(base.APIBase):
self.fields = []
for field in fields:
self.fields.append(field)
setattr(self, field, kwargs.get(
field if field != 'status' else 'id', wtypes.Unset))
setattr(
self,
field,
kwargs.get(field if field != 'status' else 'id', wtypes.Unset),
)
@staticmethod
def _convert_with_links(service, url, expand=True):
if not expand:
service.unset_fields_except(
['id', 'name', 'host', 'status'])
service.unset_fields_except(['id', 'name', 'host', 'status'])
service.links = [
link.Link.make_link('self', url, 'services', str(service.id)),
link.Link.make_link('bookmark', url, 'services', str(service.id),
bookmark=True)]
link.Link.make_link(
'bookmark', url, 'services', str(service.id), bookmark=True
),
]
return service
@classmethod
def convert_with_links(cls, service, expand=True):
service = Service(**service.as_dict())
hide_fields_in_newer_versions(service)
return cls._convert_with_links(
service, pecan.request.host_url, expand)
return cls._convert_with_links(service, pecan.request.host_url, expand)
@classmethod
def sample(cls, expand=True):
sample = cls(id=1,
name='watcher-applier',
host='Controller',
last_seen_up=datetime.datetime(2016, 1, 1))
sample = cls(
id=1,
name='watcher-applier',
host='Controller',
last_seen_up=datetime.datetime(2016, 1, 1),
)
return cls._convert_with_links(sample, 'http://localhost:9322', expand)
@@ -158,13 +170,14 @@ class ServiceCollection(collection.Collection):
self._type = 'services'
@staticmethod
def convert_with_links(services, limit, url=None, expand=False,
**kwargs):
def convert_with_links(services, limit, url=None, expand=False, **kwargs):
service_collection = ServiceCollection()
service_collection.services = [
Service.convert_with_links(g, expand) for g in services]
Service.convert_with_links(g, expand) for g in services
]
service_collection.next = service_collection.get_next(
limit, url=url, marker_field='id', **kwargs)
limit, url=url, marker_field='id', **kwargs
)
return service_collection
@classmethod
@@ -180,32 +193,43 @@ class ServicesController(rest.RestController):
def __init__(self):
super().__init__()
_custom_actions = {
'detail': ['GET'],
}
_custom_actions = {'detail': ['GET']}
def _get_services_collection(self, marker, limit, sort_key, sort_dir,
expand=False, resource_url=None):
api_utils.validate_sort_key(
sort_key, list(objects.Service.fields))
def _get_services_collection(
self,
marker,
limit,
sort_key,
sort_dir,
expand=False,
resource_url=None,
):
api_utils.validate_sort_key(sort_key, list(objects.Service.fields))
limit = api_utils.validate_limit(limit)
api_utils.validate_sort_dir(sort_dir)
marker_obj = None
if marker:
marker_obj = objects.Service.get(
pecan.request.context, marker)
marker_obj = objects.Service.get(pecan.request.context, marker)
sort_db_key = (sort_key if sort_key in objects.Service.fields
else None)
sort_db_key = sort_key if sort_key in objects.Service.fields else None
services = objects.Service.list(
pecan.request.context, limit, marker_obj,
sort_key=sort_db_key, sort_dir=sort_dir)
pecan.request.context,
limit,
marker_obj,
sort_key=sort_db_key,
sort_dir=sort_dir,
)
return ServiceCollection.convert_with_links(
services, limit, url=resource_url, expand=expand,
sort_key=sort_key, sort_dir=sort_dir)
services,
limit,
url=resource_url,
expand=expand,
sort_key=sort_key,
sort_dir=sort_dir,
)
@wsme_pecan.wsexpose(ServiceCollection, int, int, wtypes.text, wtypes.text)
def get_all(self, marker=None, limit=None, sort_key='id', sort_dir='asc'):
@@ -217,8 +241,7 @@ class ServicesController(rest.RestController):
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
"""
context = pecan.request.context
policy.enforce(context, 'service:get_all',
action='service:get_all')
policy.enforce(context, 'service:get_all', action='service:get_all')
return self._get_services_collection(marker, limit, sort_key, sort_dir)
@@ -232,8 +255,7 @@ class ServicesController(rest.RestController):
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
"""
context = pecan.request.context
policy.enforce(context, 'service:detail',
action='service:detail')
policy.enforce(context, 'service:detail', action='service:detail')
# NOTE(lucasagomes): /detail should only work against collections
parent = pecan.request.path.split('/')[:-1][-1]
if parent != "services":
@@ -242,7 +264,8 @@ class ServicesController(rest.RestController):
resource_url = '/'.join(['services', 'detail'])
return self._get_services_collection(
marker, limit, sort_key, sort_dir, expand, resource_url)
marker, limit, sort_key, sort_dir, expand, resource_url
)
@wsme_pecan.wsexpose(Service, wtypes.text)
def get_one(self, service):
@@ -252,7 +275,8 @@ class ServicesController(rest.RestController):
"""
context = pecan.request.context
rpc_service = api_utils.get_resource('Service', service)
policy.enforce(context, 'service:get', rpc_service,
action='service:get')
policy.enforce(
context, 'service:get', rpc_service, action='service:get'
)
return Service.convert_with_links(rpc_service)

View File

@@ -60,6 +60,7 @@ class Strategy(base.APIBase):
This class enforces type checking and value constraints, and converts
between the internal object model and the API representation of a strategy.
"""
_goal_uuid = None
_goal_name = None
@@ -68,8 +69,9 @@ class Strategy(base.APIBase):
return None
goal = None
try:
if (common_utils.is_uuid_like(value) or
common_utils.is_int_like(value)):
if common_utils.is_uuid_like(value) or common_utils.is_int_like(
value
):
goal = objects.Goal.get(pecan.request.context, value)
else:
goal = objects.Goal.get_by_name(pecan.request.context, value)
@@ -111,12 +113,14 @@ class Strategy(base.APIBase):
links = wtypes.wsattr([link.Link], readonly=True)
"""A list containing a self link and associated goal links"""
goal_uuid = wtypes.wsproperty(wtypes.text, _get_goal_uuid, _set_goal_uuid,
mandatory=True)
goal_uuid = wtypes.wsproperty(
wtypes.text, _get_goal_uuid, _set_goal_uuid, mandatory=True
)
"""The UUID of the goal this audit refers to"""
goal_name = wtypes.wsproperty(wtypes.text, _get_goal_name, _set_goal_name,
mandatory=False)
goal_name = wtypes.wsproperty(
wtypes.text, _get_goal_name, _set_goal_name, mandatory=False
)
"""The name of the goal this audit refers to"""
parameters_spec = {wtypes.text: types.jsontype}
@@ -137,19 +141,25 @@ class Strategy(base.APIBase):
setattr(self, 'display_name', kwargs.get('display_name', wtypes.Unset))
setattr(self, 'goal_uuid', kwargs.get('goal_id', wtypes.Unset))
setattr(self, 'goal_name', kwargs.get('goal_id', wtypes.Unset))
setattr(self, 'parameters_spec', kwargs.get('parameters_spec',
wtypes.Unset))
setattr(
self,
'parameters_spec',
kwargs.get('parameters_spec', wtypes.Unset),
)
@staticmethod
def _convert_with_links(strategy, url, expand=True):
if not expand:
strategy.unset_fields_except(
['uuid', 'name', 'display_name', 'goal_uuid', 'goal_name'])
['uuid', 'name', 'display_name', 'goal_uuid', 'goal_name']
)
strategy.links = [
link.Link.make_link('self', url, 'strategies', strategy.uuid),
link.Link.make_link('bookmark', url, 'strategies', strategy.uuid,
bookmark=True)]
link.Link.make_link(
'bookmark', url, 'strategies', strategy.uuid, bookmark=True
),
]
return strategy
@classmethod
@@ -157,13 +167,16 @@ class Strategy(base.APIBase):
strategy = Strategy(**strategy.as_dict())
hide_fields_in_newer_versions(strategy)
return cls._convert_with_links(
strategy, pecan.request.host_url, expand)
strategy, pecan.request.host_url, expand
)
@classmethod
def sample(cls, expand=True):
sample = cls(uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c',
name='DUMMY',
display_name='Dummy strategy')
sample = cls(
uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c',
name='DUMMY',
display_name='Dummy strategy',
)
return cls._convert_with_links(sample, 'http://localhost:9322', expand)
@@ -178,13 +191,16 @@ class StrategyCollection(collection.Collection):
self._type = 'strategies'
@staticmethod
def convert_with_links(strategies, limit, url=None, expand=False,
**kwargs):
def convert_with_links(
strategies, limit, url=None, expand=False, **kwargs
):
strategy_collection = StrategyCollection()
strategy_collection.strategies = [
Strategy.convert_with_links(g, expand) for g in strategies]
Strategy.convert_with_links(g, expand) for g in strategies
]
strategy_collection.next = strategy_collection.get_next(
limit, url=url, **kwargs)
limit, url=url, **kwargs
)
return strategy_collection
@classmethod
@@ -200,50 +216,76 @@ class StrategiesController(rest.RestController):
def __init__(self):
super().__init__()
_custom_actions = {
'detail': ['GET'],
'state': ['GET'],
}
_custom_actions = {'detail': ['GET'], 'state': ['GET']}
def _get_strategies_collection(self, filters, marker, limit, sort_key,
sort_dir, expand=False, resource_url=None):
def _get_strategies_collection(
self,
filters,
marker,
limit,
sort_key,
sort_dir,
expand=False,
resource_url=None,
):
additional_fields = ["goal_uuid", "goal_name"]
api_utils.validate_sort_key(
sort_key, list(objects.Strategy.fields) + additional_fields)
sort_key, list(objects.Strategy.fields) + additional_fields
)
api_utils.validate_search_filters(
filters, list(objects.Strategy.fields) + additional_fields)
filters, list(objects.Strategy.fields) + additional_fields
)
limit = api_utils.validate_limit(limit)
api_utils.validate_sort_dir(sort_dir)
marker_obj = None
if marker:
marker_obj = objects.Strategy.get_by_uuid(
pecan.request.context, marker)
pecan.request.context, marker
)
need_api_sort = api_utils.check_need_api_sort(sort_key,
additional_fields)
sort_db_key = (sort_key if not need_api_sort
else None)
need_api_sort = api_utils.check_need_api_sort(
sort_key, additional_fields
)
sort_db_key = sort_key if not need_api_sort else None
strategies = objects.Strategy.list(
pecan.request.context, limit, marker_obj, filters=filters,
sort_key=sort_db_key, sort_dir=sort_dir)
pecan.request.context,
limit,
marker_obj,
filters=filters,
sort_key=sort_db_key,
sort_dir=sort_dir,
)
strategies_collection = StrategyCollection.convert_with_links(
strategies, limit, url=resource_url, expand=expand,
sort_key=sort_key, sort_dir=sort_dir)
strategies,
limit,
url=resource_url,
expand=expand,
sort_key=sort_key,
sort_dir=sort_dir,
)
if need_api_sort:
api_utils.make_api_sort(strategies_collection.strategies,
sort_key, sort_dir)
api_utils.make_api_sort(
strategies_collection.strategies, sort_key, sort_dir
)
return strategies_collection
@wsme_pecan.wsexpose(StrategyCollection, wtypes.text, wtypes.text,
int, wtypes.text, wtypes.text)
def get_all(self, goal=None, marker=None, limit=None,
sort_key='id', sort_dir='asc'):
@wsme_pecan.wsexpose(
StrategyCollection,
wtypes.text,
wtypes.text,
int,
wtypes.text,
wtypes.text,
)
def get_all(
self, goal=None, marker=None, limit=None, sort_key='id', sort_dir='asc'
):
"""Retrieve a list of strategies.
:param goal: goal UUID or name to filter by.
@@ -253,8 +295,7 @@ class StrategiesController(rest.RestController):
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
"""
context = pecan.request.context
policy.enforce(context, 'strategy:get_all',
action='strategy:get_all')
policy.enforce(context, 'strategy:get_all', action='strategy:get_all')
filters = {}
if goal:
if common_utils.is_uuid_like(goal):
@@ -263,12 +304,20 @@ class StrategiesController(rest.RestController):
filters['goal_name'] = goal
return self._get_strategies_collection(
filters, marker, limit, sort_key, sort_dir)
filters, marker, limit, sort_key, sort_dir
)
@wsme_pecan.wsexpose(StrategyCollection, wtypes.text, wtypes.text, int,
wtypes.text, wtypes.text)
def detail(self, goal=None, marker=None, limit=None,
sort_key='id', sort_dir='asc'):
@wsme_pecan.wsexpose(
StrategyCollection,
wtypes.text,
wtypes.text,
int,
wtypes.text,
wtypes.text,
)
def detail(
self, goal=None, marker=None, limit=None, sort_key='id', sort_dir='asc'
):
"""Retrieve a list of strategies with detail.
:param goal: goal UUID or name to filter by.
@@ -278,8 +327,7 @@ class StrategiesController(rest.RestController):
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
"""
context = pecan.request.context
policy.enforce(context, 'strategy:detail',
action='strategy:detail')
policy.enforce(context, 'strategy:detail', action='strategy:detail')
# NOTE(lucasagomes): /detail should only work against collections
parent = pecan.request.path.split('/')[:-1][-1]
if parent != "strategies":
@@ -295,7 +343,8 @@ class StrategiesController(rest.RestController):
filters['goal_name'] = goal
return self._get_strategies_collection(
filters, marker, limit, sort_key, sort_dir, expand, resource_url)
filters, marker, limit, sort_key, sort_dir, expand, resource_url
)
@wsme_pecan.wsexpose(wtypes.text, wtypes.text)
def state(self, strategy):
@@ -310,11 +359,19 @@ class StrategiesController(rest.RestController):
raise exception.HTTPNotFound
rpc_strategy = api_utils.get_resource('Strategy', strategy)
de_client = rpcapi.DecisionEngineAPI()
strategy_state = de_client.get_strategy_info(context,
rpc_strategy.name)
strategy_state.extend([{
'type': 'Name', 'state': rpc_strategy.name,
'mandatory': '', 'comment': ''}])
strategy_state = de_client.get_strategy_info(
context, rpc_strategy.name
)
strategy_state.extend(
[
{
'type': 'Name',
'state': rpc_strategy.name,
'mandatory': '',
'comment': '',
}
]
)
return strategy_state
@wsme_pecan.wsexpose(Strategy, wtypes.text)
@@ -325,7 +382,8 @@ class StrategiesController(rest.RestController):
"""
context = pecan.request.context
rpc_strategy = api_utils.get_resource('Strategy', strategy)
policy.enforce(context, 'strategy:get', rpc_strategy,
action='strategy:get')
policy.enforce(
context, 'strategy:get', rpc_strategy, action='strategy:get'
)
return Strategy.convert_with_links(rpc_strategy)

View File

@@ -132,8 +132,9 @@ class JsonType(wtypes.UserType):
def __str__(self):
# These are the json serializable native types
return ' | '.join(map(str, (wtypes.text, int, float,
BooleanType, list, dict, None)))
return ' | '.join(
map(str, (wtypes.text, int, float, BooleanType, list, dict, None))
)
@staticmethod
def validate(value):
@@ -178,17 +179,20 @@ class MultiType(wtypes.UserType):
else:
raise ValueError(
_("Wrong type. Expected '%(type)s', got '%(value)s'"),
type=self.types, value=type(value)
type=self.types,
value=type(value),
)
class JsonPatchType(wtypes.Base):
"""A complex type that represents a single json-patch operation."""
path = wtypes.wsattr(wtypes.StringType(pattern=r'^(/[\w-]+)+$'),
mandatory=True)
op = wtypes.wsattr(wtypes.Enum(str, 'add', 'replace', 'remove'),
mandatory=True)
path = wtypes.wsattr(
wtypes.StringType(pattern=r'^(/[\w-]+)+$'), mandatory=True
)
op = wtypes.wsattr(
wtypes.Enum(str, 'add', 'replace', 'remove'), mandatory=True
)
value = wsme.wsattr(jsontype, default=wtypes.Unset)
@staticmethod
@@ -199,8 +203,14 @@ class JsonPatchType(wtypes.Base):
method may be overwritten by derived class.
"""
return ['/created_at', '/id', '/links', '/updated_at',
'/deleted_at', '/uuid']
return [
'/created_at',
'/id',
'/links',
'/updated_at',
'/deleted_at',
'/uuid',
]
@staticmethod
def mandatory_attrs():
@@ -222,8 +232,9 @@ class JsonPatchType(wtypes.Base):
_path = '/{}'.format(patch.path.split('/')[1])
if len(patch.allowed_attrs()) > 0:
if _path not in patch.allowed_attrs():
msg = _("'%s' is not an allowed attribute and can not be "
"updated")
msg = _(
"'%s' is not an allowed attribute and can not be updated"
)
raise wsme.exc.ClientSideError(msg % patch.path)
if _path in patch.internal_attrs():

View File

@@ -32,9 +32,11 @@ from watcher.common import utils
CONF = cfg.CONF
JSONPATCH_EXCEPTIONS = (jsonpatch.JsonPatchException,
jsonpatch.JsonPointerException,
KeyError)
JSONPATCH_EXCEPTIONS = (
jsonpatch.JsonPatchException,
jsonpatch.JsonPointerException,
KeyError,
)
def validate_limit(limit):
@@ -54,16 +56,20 @@ def validate_limit(limit):
def validate_sort_dir(sort_dir):
if sort_dir not in ['asc', 'desc']:
raise wsme.exc.ClientSideError(_("Invalid sort direction: %s. "
"Acceptable values are "
"'asc' or 'desc'") % sort_dir)
raise wsme.exc.ClientSideError(
_(
"Invalid sort direction: %s. "
"Acceptable values are "
"'asc' or 'desc'"
)
% sort_dir
)
def validate_sort_key(sort_key, allowed_fields):
# Very lightweight validation for now
if sort_key not in allowed_fields:
raise wsme.exc.ClientSideError(
_("Invalid sort key: %s") % sort_key)
raise wsme.exc.ClientSideError(_("Invalid sort key: %s") % sort_key)
def validate_search_filters(filters, allowed_fields):
@@ -72,7 +78,8 @@ def validate_search_filters(filters, allowed_fields):
for filter_name in filters:
if filter_name not in allowed_fields:
raise wsme.exc.ClientSideError(
_("Invalid filter: %s") % filter_name)
_("Invalid filter: %s") % filter_name
)
def check_need_api_sort(sort_key, additional_fields):
@@ -83,7 +90,7 @@ def make_api_sort(sorting_list, sort_key, sort_dir):
# First sort by uuid field, than sort by sort_key
# sort() ensures stable sorting, so we could
# make lexicographical sort
reverse_direction = (sort_dir == 'desc')
reverse_direction = sort_dir == 'desc'
sorting_list.sort(key=attrgetter('uuid'), reverse=reverse_direction)
sorting_list.sort(key=attrgetter(sort_key), reverse=reverse_direction)
@@ -92,8 +99,10 @@ def apply_jsonpatch(doc, patch):
for p in patch:
if p['op'] == 'add' and p['path'].count('/') == 1:
if p['path'].lstrip('/') not in doc:
msg = _('Adding a new attribute (%s) to the root of '
' the resource is not allowed')
msg = _(
'Adding a new attribute (%s) to the root of '
' the resource is not allowed'
)
raise wsme.exc.ClientSideError(msg % p['path'])
return jsonpatch.apply_patch(doc, jsonpatch.JsonPatch(patch))
@@ -120,8 +129,11 @@ def check_audit_state_transition(patch, initial):
is_transition_valid = True
state_value = get_patch_value(patch, "state")
if state_value is not None:
is_transition_valid = objects.audit.AuditStateTransitionManager(
).check_transition(initial, state_value)
is_transition_valid = (
objects.audit.AuditStateTransitionManager().check_transition(
initial, state_value
)
)
return is_transition_valid
@@ -167,7 +179,8 @@ def allow_start_end_audit_time():
audits.
"""
return pecan.request.version.minor >= (
versions.VERSIONS.MINOR_1_START_END_TIMING.value)
versions.VERSIONS.MINOR_1_START_END_TIMING.value
)
def allow_force():
@@ -177,7 +190,8 @@ def allow_force():
launch audit when other action plan is ongoing.
"""
return pecan.request.version.minor >= (
versions.VERSIONS.MINOR_2_FORCE.value)
versions.VERSIONS.MINOR_2_FORCE.value
)
def allow_list_datamodel():
@@ -186,7 +200,8 @@ def allow_list_datamodel():
Version 1.3 of the API added support to list data model.
"""
return pecan.request.version.minor >= (
versions.VERSIONS.MINOR_3_DATAMODEL.value)
versions.VERSIONS.MINOR_3_DATAMODEL.value
)
def allow_webhook_api():
@@ -195,7 +210,8 @@ def allow_webhook_api():
Version 1.4 of the API added support to trigger webhook.
"""
return pecan.request.version.minor >= (
versions.VERSIONS.MINOR_4_WEBHOOK_API.value)
versions.VERSIONS.MINOR_4_WEBHOOK_API.value
)
def allow_skipped_action():
@@ -204,7 +220,8 @@ def allow_skipped_action():
Version 1.5 of the API added support to skipped actions.
"""
return pecan.request.version.minor >= (
versions.VERSIONS.MINOR_5_SKIPPED_ACTION.value)
versions.VERSIONS.MINOR_5_SKIPPED_ACTION.value
)
def allow_list_extend_compute_model():
@@ -214,4 +231,5 @@ def allow_list_extend_compute_model():
to the compute data model.
"""
return pecan.request.version.minor >= (
versions.VERSIONS.MINOR_6_EXT_COMPUTE_MODEL.value)
versions.VERSIONS.MINOR_6_EXT_COMPUTE_MODEL.value
)

View File

@@ -40,8 +40,9 @@ class WebhookController(rest.RestController):
super().__init__()
self.dc_client = rpcapi.DecisionEngineAPI()
@wsme_pecan.wsexpose(None, wtypes.text, body=types.jsontype,
status_code=HTTPStatus.ACCEPTED)
@wsme_pecan.wsexpose(
None, wtypes.text, body=types.jsontype, status_code=HTTPStatus.ACCEPTED
)
def post(self, audit_ident, body):
"""Trigger the given audit.
@@ -59,7 +60,7 @@ class WebhookController(rest.RestController):
allowed_state = (
objects.audit.State.PENDING,
objects.audit.State.SUCCEEDED,
)
)
if audit.state not in allowed_state:
raise exception.AuditStateNotAllowed(state=audit.state)

View File

@@ -56,8 +56,9 @@ class ContextHook(hooks.PecanHook):
auth_token = headers.get('X-Auth-Token', auth_token)
show_deleted = headers.get('X-Show-Deleted')
auth_token_info = state.request.environ.get('keystone.token_info')
roles = (headers.get('X-Roles', None) and
headers.get('X-Roles').split(','))
roles = headers.get('X-Roles', None) and headers.get('X-Roles').split(
','
)
state.request.context = context.make_context(
auth_token=auth_token,
@@ -69,7 +70,8 @@ class ContextHook(hooks.PecanHook):
domain_id=domain_id,
domain_name=domain_name,
show_deleted=show_deleted,
roles=roles)
roles=roles,
)
class NoExceptionTracebackHook(hooks.PecanHook):
@@ -79,6 +81,7 @@ class NoExceptionTracebackHook(hooks.PecanHook):
message which is then sent to the client. Such behavior is a security
concern so this hook is aimed to cut-off traceback from the error message.
"""
# NOTE(max_lobur): 'after' hook used instead of 'on_error' because
# 'on_error' never fired for wsme+pecan pair. wsme @wsexpose decorator
# catches and handles all the errors, so 'on_error' dedicated for unhandled
@@ -92,8 +95,7 @@ class NoExceptionTracebackHook(hooks.PecanHook):
# Do nothing if there is no error.
# Status codes in the range 200 (OK) to 399 (400 = BAD_REQUEST) are not
# an error.
if (HTTPStatus.OK <= state.response.status_int <
HTTPStatus.BAD_REQUEST):
if HTTPStatus.OK <= state.response.status_int < HTTPStatus.BAD_REQUEST:
return
json_body = state.response.json

View File

@@ -37,12 +37,15 @@ class AuthTokenMiddleware(auth_token.AuthProtocol):
route_pattern_tpl = r'%s(\.json|\.xml)?$'
try:
self.public_api_routes = [re.compile(route_pattern_tpl % route_tpl)
for route_tpl in public_api_routes]
self.public_api_routes = [
re.compile(route_pattern_tpl % route_tpl)
for route_tpl in public_api_routes
]
except re.error as e:
LOG.exception(e)
raise exception.ConfigInvalid(
error_msg=_('Cannot compile public API routes'))
error_msg=_('Cannot compile public API routes')
)
super().__init__(app, conf)
@@ -52,8 +55,9 @@ class AuthTokenMiddleware(auth_token.AuthProtocol):
# The information whether the API call is being performed against the
# public API is required for some other components. Saving it to the
# WSGI environment is reasonable thereby.
env['is_public_api'] = any(re.match(pattern, path)
for pattern in self.public_api_routes)
env['is_public_api'] = any(
re.match(pattern, path) for pattern in self.public_api_routes
)
if env['is_public_api']:
return self._app(env, start_response)

View File

@@ -49,17 +49,20 @@ class ParsableErrorMiddleware:
status_code = int(status.split(' ')[0])
state['status_code'] = status_code
except (ValueError, TypeError): # pragma: nocover
raise Exception(_(
'ErrorDocumentMiddleware received an invalid '
'status %s') % status)
raise Exception(
_('ErrorDocumentMiddleware received an invalid status %s')
% status
)
else:
if (state['status_code'] // 100) not in (2, 3):
# Remove some headers so we can replace them later
# when we have the full error message and can
# compute the length.
headers = [(h, v)
for (h, v) in headers
if h not in ('Content-Length', 'Content-Type')]
headers = [
(h, v)
for (h, v) in headers
if h not in ('Content-Length', 'Content-Type')
]
# Save the headers in case we need to modify them.
state['headers'] = headers
return start_response(status, headers, exc_info)
@@ -68,25 +71,31 @@ class ParsableErrorMiddleware:
if (state['status_code'] // 100) not in (2, 3):
req = webob.Request(environ)
if (
req.accept.best_match(
['application/json',
'application/xml']) == 'application/xml'
req.accept.best_match(['application/json', 'application/xml'])
== 'application/xml'
):
try:
# simple check xml is valid
body = [
et.ElementTree.tostring(
et.ElementTree.Element(
'error_message', text='\n'.join(app_iter)))]
'error_message', text='\n'.join(app_iter)
)
)
]
except et.ElementTree.ParseError as err:
LOG.error('Error parsing HTTP response: %s', err)
body = ['<error_message>{}'
'</error_message>'.format(state['status_code'])]
body = [
'<error_message>{}</error_message>'.format(
state['status_code']
)
]
state['headers'].append(('Content-Type', 'application/xml'))
else:
app_iter = [i.decode('utf-8') for i in app_iter]
body = [jsonutils.dumps(
{'error_message': '\n'.join(app_iter)})]
body = [
jsonutils.dumps({'error_message': '\n'.join(app_iter)})
]
body = [item.encode('utf-8') for item in body]
state['headers'].append(('Content-Type', 'application/json'))
state['headers'].append(('Content-Length', str(len(body[0]))))

View File

@@ -35,8 +35,10 @@ def initialize_wsgi_app(show_deprecated=False):
CONF.log_opt_values(LOG, log.DEBUG)
if show_deprecated:
LOG.warning("Using watcher/api/app.wsgi is deprecated and it will "
"be removed in U release. Please use automatically "
"generated watcher-api-wsgi instead.")
LOG.warning(
"Using watcher/api/app.wsgi is deprecated and it will "
"be removed in U release. Please use automatically "
"generated watcher-api-wsgi instead."
)
return app.VersionSelectorApplication()

View File

@@ -32,7 +32,6 @@ LOG = log.getLogger(__name__)
class DefaultActionPlanHandler(base.BaseActionPlanHandler):
def __init__(self, context, service, action_plan_uuid):
super().__init__()
self.ctx = context
@@ -42,16 +41,19 @@ class DefaultActionPlanHandler(base.BaseActionPlanHandler):
def execute(self):
try:
action_plan = objects.ActionPlan.get_by_uuid(
self.ctx, self.action_plan_uuid, eager=True)
self.ctx, self.action_plan_uuid, eager=True
)
if action_plan.state == objects.action_plan.State.CANCELLED:
self._update_action_from_pending_to_cancelled()
return
action_plan.state = objects.action_plan.State.ONGOING
action_plan.save()
notifications.action_plan.send_action_notification(
self.ctx, action_plan,
self.ctx,
action_plan,
action=fields.NotificationAction.EXECUTION,
phase=fields.NotificationPhase.START)
phase=fields.NotificationPhase.START,
)
applier = default.DefaultApplier(self.ctx, self.service)
applier.execute(self.action_plan_uuid)
@@ -59,25 +61,29 @@ class DefaultActionPlanHandler(base.BaseActionPlanHandler):
# If any action has failed the action plan should be FAILED
# Define default values for successful execution
ap_state = objects.action_plan.State.SUCCEEDED
notification_kwargs = {
'phase': fields.NotificationPhase.END
}
notification_kwargs = {'phase': fields.NotificationPhase.END}
failed_filter = {'action_plan_uuid': self.action_plan_uuid,
'state': objects.action.State.FAILED}
failed_filter = {
'action_plan_uuid': self.action_plan_uuid,
'state': objects.action.State.FAILED,
}
failed_actions = objects.Action.list(
self.ctx, filters=failed_filter, eager=True)
self.ctx, filters=failed_filter, eager=True
)
if failed_actions:
ap_state = objects.action_plan.State.FAILED
notification_kwargs = {
'phase': fields.NotificationPhase.ERROR,
'priority': fields.NotificationPriority.ERROR
'priority': fields.NotificationPriority.ERROR,
}
skipped_filter = {'action_plan_uuid': self.action_plan_uuid,
'state': objects.action.State.SKIPPED}
skipped_filter = {
'action_plan_uuid': self.action_plan_uuid,
'state': objects.action.State.SKIPPED,
}
skipped_actions = objects.Action.list(
self.ctx, filters=skipped_filter, eager=True)
self.ctx, filters=skipped_filter, eager=True
)
if skipped_actions:
status_message = _("One or more actions were skipped.")
action_plan.status_message = status_message
@@ -85,9 +91,11 @@ class DefaultActionPlanHandler(base.BaseActionPlanHandler):
action_plan.state = ap_state
action_plan.save()
notifications.action_plan.send_action_notification(
self.ctx, action_plan,
self.ctx,
action_plan,
action=fields.NotificationAction.EXECUTION,
**notification_kwargs)
**notification_kwargs,
)
except exception.ActionPlanCancelled as e:
LOG.exception(e)
@@ -95,34 +103,43 @@ class DefaultActionPlanHandler(base.BaseActionPlanHandler):
self._update_action_from_pending_to_cancelled()
action_plan.save()
notifications.action_plan.send_cancel_notification(
self.ctx, action_plan,
self.ctx,
action_plan,
action=fields.NotificationAction.CANCEL,
phase=fields.NotificationPhase.END)
phase=fields.NotificationPhase.END,
)
except Exception as e:
LOG.exception(e)
action_plan = objects.ActionPlan.get_by_uuid(
self.ctx, self.action_plan_uuid, eager=True)
self.ctx, self.action_plan_uuid, eager=True
)
if action_plan.state == objects.action_plan.State.CANCELLING:
action_plan.state = objects.action_plan.State.FAILED
action_plan.save()
notifications.action_plan.send_cancel_notification(
self.ctx, action_plan,
self.ctx,
action_plan,
action=fields.NotificationAction.CANCEL,
priority=fields.NotificationPriority.ERROR,
phase=fields.NotificationPhase.ERROR)
phase=fields.NotificationPhase.ERROR,
)
else:
action_plan.state = objects.action_plan.State.FAILED
action_plan.save()
notifications.action_plan.send_action_notification(
self.ctx, action_plan,
self.ctx,
action_plan,
action=fields.NotificationAction.EXECUTION,
priority=fields.NotificationPriority.ERROR,
phase=fields.NotificationPhase.ERROR)
phase=fields.NotificationPhase.ERROR,
)
def _update_action_from_pending_to_cancelled(self):
filters = {'action_plan_uuid': self.action_plan_uuid,
'state': objects.action.State.PENDING}
filters = {
'action_plan_uuid': self.action_plan_uuid,
'state': objects.action.State.PENDING,
}
actions = objects.Action.list(self.ctx, filters=filters, eager=True)
if actions:
for a in actions:

View File

@@ -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."

View File

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

View File

@@ -28,8 +28,9 @@ class ActionFactory:
def make_action(self, object_action, osc=None):
LOG.debug("Creating instance of %s", object_action.action_type)
loaded_action = self.action_loader.load(name=object_action.action_type,
osc=osc)
loaded_action = self.action_loader.load(
name=object_action.action_type, osc=osc
)
loaded_action.input_parameters = object_action.input_parameters
LOG.debug("Checking the input parameters")
# NOTE(jed) if we change the schema of an action and we try to reload

View File

@@ -39,12 +39,14 @@ class Migrate(base.BaseAction):
The action schema is::
schema = Schema({
'resource_id': str, # should be a UUID
'migration_type': str, # choices -> "live", "cold"
'destination_node': str,
'source_node': str,
})
schema = Schema(
{
'resource_id': str, # should be a UUID
'migration_type': str, # choices -> "live", "cold"
'destination_node': str,
'source_node': str,
}
)
The `resource_id` is the UUID of the server to migrate.
The `source_node` and `destination_node` parameters are respectively the
@@ -72,28 +74,21 @@ class Migrate(base.BaseAction):
'destination_node': {
"anyof": [
{'type': 'string', "minLength": 1},
{'type': 'None'}
]
},
'migration_type': {
'type': 'string',
"enum": ["live", "cold"]
{'type': 'None'},
]
},
'migration_type': {'type': 'string', "enum": ["live", "cold"]},
'resource_id': {
'type': 'string',
"minlength": 1,
"pattern": ("^([a-fA-F0-9]){8}-([a-fA-F0-9]){4}-"
"([a-fA-F0-9]){4}-([a-fA-F0-9]){4}-"
"([a-fA-F0-9]){12}$")
"pattern": (
"^([a-fA-F0-9]){8}-([a-fA-F0-9]){4}-"
"([a-fA-F0-9]){4}-([a-fA-F0-9]){4}-"
"([a-fA-F0-9]){12}$"
),
},
'resource_name': {
'type': 'string',
"minlength": 1
},
'source_node': {
'type': 'string',
"minLength": 1
}
'resource_name': {'type': 'string', "minlength": 1},
'source_node': {'type': 'string', "minLength": 1},
},
'required': ['migration_type', 'resource_id', 'source_node'],
'additionalProperties': False,
@@ -118,19 +113,25 @@ class Migrate(base.BaseAction):
def _live_migrate_instance(self, nova, destination):
result = None
try:
result = nova.live_migrate_instance(instance_id=self.instance_uuid,
dest_hostname=destination)
result = nova.live_migrate_instance(
instance_id=self.instance_uuid, dest_hostname=destination
)
except exception.NovaClientError as e:
LOG.debug("Nova client exception occurred while live "
"migrating instance "
"%(instance)s.Exception: %(exception)s",
{'instance': self.instance_uuid, 'exception': e})
LOG.debug(
"Nova client exception occurred while live "
"migrating instance "
"%(instance)s.Exception: %(exception)s",
{'instance': self.instance_uuid, 'exception': e},
)
except Exception as e:
LOG.exception(e)
LOG.critical("Unexpected error occurred. Migration failed for "
"instance %s. Leaving instance on previous "
"host.", self.instance_uuid)
LOG.critical(
"Unexpected error occurred. Migration failed for "
"instance %s. Leaving instance on previous "
"host.",
self.instance_uuid,
)
return result
@@ -138,13 +139,16 @@ class Migrate(base.BaseAction):
result = None
try:
result = nova.watcher_non_live_migrate_instance(
instance_id=self.instance_uuid,
dest_hostname=destination)
instance_id=self.instance_uuid, dest_hostname=destination
)
except Exception as exc:
LOG.exception(exc)
LOG.critical("Unexpected error occurred. Migration failed for "
"instance %s. Leaving instance on previous "
"host.", self.instance_uuid)
LOG.critical(
"Unexpected error occurred. Migration failed for "
"instance %s. Leaving instance on previous "
"host.",
self.instance_uuid,
)
return result
def _abort_cold_migrate(self, nova):
@@ -156,17 +160,24 @@ class Migrate(base.BaseAction):
LOG.warning("Abort operation for cold migration is not implemented")
def _abort_live_migrate(self, nova, source, destination):
return nova.abort_live_migrate(instance_id=self.instance_uuid,
source=source, destination=destination)
return nova.abort_live_migrate(
instance_id=self.instance_uuid,
source=source,
destination=destination,
)
def migrate(self, destination=None):
nova = nova_helper.NovaHelper(osc=self.osc)
if destination is None:
LOG.debug("Migrating instance %s, destination node will be "
"determined by nova-scheduler", self.instance_uuid)
LOG.debug(
"Migrating instance %s, destination node will be "
"determined by nova-scheduler",
self.instance_uuid,
)
else:
LOG.debug("Migrate instance %s to %s", self.instance_uuid,
destination)
LOG.debug(
"Migrate instance %s to %s", self.instance_uuid, destination
)
try:
nova.find_instance(self.instance_uuid)
except exception.ComputeResourceNotFound:
@@ -178,9 +189,14 @@ class Migrate(base.BaseAction):
return self._cold_migrate_instance(nova, destination)
else:
raise exception.Invalid(
message=(_("Migration of type '%(migration_type)s' is not "
"supported.") %
{'migration_type': self.migration_type}))
message=(
_(
"Migration of type '%(migration_type)s' is not "
"supported."
)
% {'migration_type': self.migration_type}
)
)
def execute(self):
return self.migrate(destination=self.destination_node)
@@ -199,8 +215,10 @@ class Migrate(base.BaseAction):
return self._abort_cold_migrate(nova)
elif self.migration_type == self.LIVE_MIGRATION:
return self._abort_live_migrate(
nova, source=self.source_node,
destination=self.destination_node)
nova,
source=self.source_node,
destination=self.destination_node,
)
def pre_condition(self):
"""Check migration preconditions
@@ -219,44 +237,57 @@ class Migrate(base.BaseAction):
instance = nova.find_instance(self.instance_uuid)
except exception.ComputeResourceNotFound:
raise exception.ActionSkipped(
_("Instance %s not found") % self.instance_uuid)
_("Instance %s not found") % self.instance_uuid
)
# Check that the instance is running on source_node
instance_host = instance.host
if instance_host != self.source_node:
raise exception.ActionSkipped(
_("Instance %(instance)s is not running on source node "
"%(source)s (currently on %(current)s)") %
{'instance': self.instance_uuid,
'source': self.source_node,
'current': instance_host})
_(
"Instance %(instance)s is not running on source node "
"%(source)s (currently on %(current)s)"
)
% {
'instance': self.instance_uuid,
'source': self.source_node,
'current': instance_host,
}
)
# Check destination node if specified
if self.destination_node:
try:
# Find the compute node and check if service is enabled
dest_node = nova.get_compute_node_by_hostname(
self.destination_node)
self.destination_node
)
# Check if compute service is enabled
if dest_node.status != 'enabled':
raise exception.ActionExecutionFailure(
_("Destination node %s is not in enabled state") %
self.destination_node)
_("Destination node %s is not in enabled state")
% self.destination_node
)
except exception.ComputeNodeNotFound:
raise exception.ActionExecutionFailure(
_("Destination node %s not found") %
self.destination_node)
_("Destination node %s not found") % self.destination_node
)
# Check instance status based on migration type
instance_status = instance.status
if self.migration_type == self.LIVE_MIGRATION:
if instance_status != 'ACTIVE':
raise exception.ActionExecutionFailure(
_("Live migration requires instance %(instance)s to be "
"in ACTIVE status (current status: %(status)s)") %
{'instance': self.instance_uuid,
'status': instance_status})
_(
"Live migration requires instance %(instance)s to be "
"in ACTIVE status (current status: %(status)s)"
)
% {
'instance': self.instance_uuid,
'status': instance_status,
}
)
def post_condition(self):
# TODO(jed): check extra parameters (network response, etc.)

View File

@@ -30,9 +30,7 @@ class Nop(base.BaseAction):
The action schema is::
schema = Schema({
'message': str,
})
schema = Schema({'message': str})
The `message` is the actual message that will be logged.
"""
@@ -44,25 +42,11 @@ class Nop(base.BaseAction):
return {
'type': 'object',
'properties': {
'message': {
'type': ['string', 'null']
},
'skip_pre_condition': {
'type': 'boolean',
'default': False
},
'fail_pre_condition': {
'type': 'boolean',
'default': False
},
'fail_execute': {
'type': 'boolean',
'default': False
},
'fail_post_condition': {
'type': 'boolean',
'default': False
}
'message': {'type': ['string', 'null']},
'skip_pre_condition': {'type': 'boolean', 'default': False},
'fail_pre_condition': {'type': 'boolean', 'default': False},
'fail_execute': {'type': 'boolean', 'default': False},
'fail_post_condition': {'type': 'boolean', 'default': False},
},
'required': ['message'],
'additionalProperties': False,

View File

@@ -34,10 +34,12 @@ class Resize(base.BaseAction):
The action schema is::
schema = Schema({
'resource_id': str, # should be a UUID
'flavor': str, # should be either ID or Name of Flavor
})
schema = Schema(
{
'resource_id': str, # should be a UUID
'flavor': str, # should be either ID or Name of Flavor
}
)
The `resource_id` is the UUID of the server to resize.
The `flavor` is the ID or Name of Flavor (Nova accepts either ID or Name
@@ -55,14 +57,13 @@ class Resize(base.BaseAction):
'resource_id': {
'type': 'string',
'minlength': 1,
'pattern': ('^([a-fA-F0-9]){8}-([a-fA-F0-9]){4}-'
'([a-fA-F0-9]){4}-([a-fA-F0-9]){4}-'
'([a-fA-F0-9]){12}$')
},
'flavor': {
'type': 'string',
'minlength': 1,
'pattern': (
'^([a-fA-F0-9]){8}-([a-fA-F0-9]){4}-'
'([a-fA-F0-9]){4}-([a-fA-F0-9]){4}-'
'([a-fA-F0-9]){12}$'
),
},
'flavor': {'type': 'string', 'minlength': 1},
},
'required': ['resource_id', 'flavor'],
'additionalProperties': False,
@@ -78,24 +79,28 @@ class Resize(base.BaseAction):
def resize(self):
nova = nova_helper.NovaHelper(osc=self.osc)
LOG.debug("Resize instance %s to %s flavor", self.instance_uuid,
self.flavor)
LOG.debug(
"Resize instance %s to %s flavor", self.instance_uuid, self.flavor
)
try:
nova.find_instance(self.instance_uuid)
except exception.ComputeResourceNotFound:
LOG.warning("Instance %s not found, skipping resize",
self.instance_uuid)
LOG.warning(
"Instance %s not found, skipping resize", self.instance_uuid
)
raise exception.InstanceNotFound(name=self.instance_uuid)
result = None
try:
result = nova.resize_instance(
instance_id=self.instance_uuid, flavor=self.flavor)
instance_id=self.instance_uuid, flavor=self.flavor
)
except Exception as exc:
LOG.exception(exc)
LOG.critical(
"Unexpected error occurred. Resizing failed for "
"instance %s.", self.instance_uuid)
"Unexpected error occurred. Resizing failed for instance %s.",
self.instance_uuid,
)
return False
return result
@@ -120,13 +125,15 @@ class Resize(base.BaseAction):
nova.find_instance(self.instance_uuid)
except exception.ComputeResourceNotFound:
raise exception.ActionSkipped(
_("Instance %s not found") % self.instance_uuid)
_("Instance %s not found") % self.instance_uuid
)
try:
nova.get_flavor_id(self.flavor)
except exception.ComputeResourceNotFound:
raise exception.ActionExecutionFailure(
_("Flavor %s not found") % self.flavor)
_("Flavor %s not found") % self.flavor
)
def post_condition(self):
# TODO(jed): check extra parameters (network response, etc.)

View File

@@ -31,9 +31,7 @@ class Sleep(base.BaseAction):
The action schema is::
schema = Schema({
'duration': float,
})
schema = Schema({'duration': float})
The `duration` is expressed in seconds.
"""
@@ -44,12 +42,7 @@ class Sleep(base.BaseAction):
def schema(self):
return {
'type': 'object',
'properties': {
'duration': {
'type': 'number',
'minimum': 0
},
},
'properties': {'duration': {'type': 'number', 'minimum': 0}},
'required': ['duration'],
'additionalProperties': False,
}

View File

@@ -31,9 +31,11 @@ class Stop(base.BaseAction):
The action schema is::
schema = Schema({
'resource_id': str, # should be a UUID
})
schema = Schema(
{
'resource_id': str # should be a UUID
}
)
The `resource_id` is the UUID of the server instance to stop.
The action will check if the instance exists, verify its current state,
@@ -48,10 +50,12 @@ class Stop(base.BaseAction):
'resource_id': {
'type': 'string',
"minlength": 1,
"pattern": ("^([a-fA-F0-9]){8}-([a-fA-F0-9]){4}-"
"([a-fA-F0-9]){4}-([a-fA-F0-9]){4}-"
"([a-fA-F0-9]){12}$")
},
"pattern": (
"^([a-fA-F0-9]){8}-([a-fA-F0-9]){4}-"
"([a-fA-F0-9]){4}-([a-fA-F0-9]){4}-"
"([a-fA-F0-9]){12}$"
),
}
},
'required': ['resource_id'],
'additionalProperties': False,
@@ -68,19 +72,24 @@ class Stop(base.BaseAction):
try:
result = nova.stop_instance(instance_id=self.instance_uuid)
except exception.NovaClientError as e:
LOG.debug("Nova client exception occurred while stopping "
"instance %(instance)s. Exception: %(exception)s",
{'instance': self.instance_uuid, 'exception': e})
LOG.debug(
"Nova client exception occurred while stopping "
"instance %(instance)s. Exception: %(exception)s",
{'instance': self.instance_uuid, 'exception': e},
)
return False
except Exception as e:
LOG.debug("An unexpected error occurred while stopping "
"instance %s: %s", self.instance_uuid, str(e))
LOG.debug(
"An unexpected error occurred while stopping instance %s: %s",
self.instance_uuid,
str(e),
)
return False
if result:
LOG.debug(
"Successfully stopped instance %(uuid)s",
{'uuid': self.instance_uuid}
{'uuid': self.instance_uuid},
)
return True
else:
@@ -91,13 +100,13 @@ class Stop(base.BaseAction):
LOG.info(
"Instance %(uuid)s not found, "
"considering stop operation successful",
{'uuid': self.instance_uuid}
{'uuid': self.instance_uuid},
)
return True
LOG.error(
"Failed to stop instance %(uuid)s",
{'uuid': self.instance_uuid}
{'uuid': self.instance_uuid},
)
return False
@@ -115,20 +124,20 @@ class Stop(base.BaseAction):
LOG.debug(
"Successfully reverted stop action and started instance "
"%(uuid)s",
{'uuid': self.instance_uuid}
{'uuid': self.instance_uuid},
)
return result
else:
LOG.info(
"Failed to start instance %(uuid)s during revert. "
"This may be normal for instances with special configs.",
{'uuid': self.instance_uuid}
{'uuid': self.instance_uuid},
)
except Exception as exc:
LOG.info(
"Could not start instance %(uuid)s during revert: %(error)s. "
"This may be normal for instances with special configs.",
{'uuid': self.instance_uuid, 'error': str(exc)}
{'uuid': self.instance_uuid, 'error': str(exc)},
)
return False
@@ -138,8 +147,11 @@ class Stop(base.BaseAction):
def abort(self):
"""Abort the stop action - not applicable for stop operations"""
LOG.info("Abort operation is not applicable for stop action on "
" instance %s", self.instance_uuid)
LOG.info(
"Abort operation is not applicable for stop action on "
" instance %s",
self.instance_uuid,
)
return False
def pre_condition(self):
@@ -156,14 +168,19 @@ class Stop(base.BaseAction):
instance = nova.find_instance(self.instance_uuid)
except exception.ComputeResourceNotFound:
raise exception.ActionSkipped(
_("Instance %s not found") % self.instance_uuid)
_("Instance %s not found") % self.instance_uuid
)
current_state = instance.status
LOG.debug("Instance %s pre-condition check: state=%s",
self.instance_uuid, current_state)
LOG.debug(
"Instance %s pre-condition check: state=%s",
self.instance_uuid,
current_state,
)
if current_state == 'SHUTOFF':
raise exception.ActionSkipped(
_("Instance %s is already stopped") % self.instance_uuid)
_("Instance %s is already stopped") % self.instance_uuid
)
def post_condition(self):
pass

View File

@@ -42,12 +42,14 @@ class VolumeMigrate(base.BaseAction):
The action schema is::
schema = Schema({
'resource_id': str, # should be a UUID
'migration_type': str, # choices -> "swap", "migrate","retype"
'destination_node': str,
'destination_type': str,
})
schema = Schema(
{
'resource_id': str, # should be a UUID
'migration_type': str, # choices -> "swap", "migrate","retype"
'destination_node': str,
'destination_type': str,
}
)
The `resource_id` is the UUID of cinder volume to migrate.
The `destination_node` is the destination block storage pool name.
@@ -80,30 +82,29 @@ class VolumeMigrate(base.BaseAction):
'resource_id': {
'type': 'string',
"minlength": 1,
"pattern": ("^([a-fA-F0-9]){8}-([a-fA-F0-9]){4}-"
"([a-fA-F0-9]){4}-([a-fA-F0-9]){4}-"
"([a-fA-F0-9]){12}$")
},
'resource_name': {
'type': 'string',
"minlength": 1
"pattern": (
"^([a-fA-F0-9]){8}-([a-fA-F0-9]){4}-"
"([a-fA-F0-9]){4}-([a-fA-F0-9]){4}-"
"([a-fA-F0-9]){12}$"
),
},
'resource_name': {'type': 'string', "minlength": 1},
'migration_type': {
'type': 'string',
"enum": ["swap", "retype", "migrate"]
"enum": ["swap", "retype", "migrate"],
},
'destination_node': {
"anyof": [
{'type': 'string', "minLength": 1},
{'type': 'None'}
]
{'type': 'None'},
]
},
'destination_type': {
"anyof": [
{'type': 'string', "minLength": 1},
{'type': 'None'}
]
}
{'type': 'None'},
]
},
},
'required': ['resource_id', 'migration_type'],
'additionalProperties': False,
@@ -149,21 +150,26 @@ class VolumeMigrate(base.BaseAction):
except exception.ComputeResourceNotFound:
LOG.debug(
"Could not find instance %s, could not determine whether"
"it's safe to migrate", instance_id
"it's safe to migrate",
instance_id,
)
return False
instance_status = instance.status
LOG.debug(
"volume: %s is attached to instance: %s "
"in instance status: %s",
volume.id, instance_id, instance_status)
"volume: %s is attached to instance: %s in instance status: %s",
volume.id,
instance_id,
instance_status,
)
# NOTE(sean-k-mooney): This used to allow RESIZED which
# is the resize_verify task state, that is not an acceptable time
# to migrate volumes, if nova does not block this in the API
# today that is probably a bug. PAUSED is also questionable but
# it should generally be safe.
return (volume.status == 'in-use' and
instance_status in ('ACTIVE', 'PAUSED'))
return volume.status == 'in-use' and instance_status in (
'ACTIVE',
'PAUSED',
)
def _migrate(self, volume_id, dest_node, dest_type):
try:
@@ -172,15 +178,21 @@ class VolumeMigrate(base.BaseAction):
if self.migration_type in (self.SWAP, self.MIGRATE):
if not self._can_swap(volume):
raise exception.Invalid(
message=(_("Invalid state for swapping volume")))
message=(_("Invalid state for swapping volume"))
)
return self.cinder_util.migrate(volume, dest_node)
elif self.migration_type == self.RETYPE:
return self.cinder_util.retype(volume, dest_type)
else:
raise exception.Invalid(
message=(_("Migration of type '%(migration_type)s' is not "
"supported.") %
{'migration_type': self.migration_type}))
message=(
_(
"Migration of type '%(migration_type)s' is not "
"supported."
)
% {'migration_type': self.migration_type}
)
)
except exception.Invalid as ei:
LOG.exception(ei)
return False
@@ -190,9 +202,9 @@ class VolumeMigrate(base.BaseAction):
return False
def execute(self):
return self._migrate(self.volume_id,
self.destination_node,
self.destination_type)
return self._migrate(
self.volume_id, self.destination_node, self.destination_type
)
def revert(self):
LOG.warning("revert not supported")
@@ -215,7 +227,8 @@ class VolumeMigrate(base.BaseAction):
volume = self.cinder_util.get_volume(self.volume_id)
except cinder_exception.NotFound:
raise exception.ActionSkipped(
_("Volume %s not found") % self.volume_id)
_("Volume %s not found") % self.volume_id
)
# Check if destination_type exists (if specified)
if self.destination_type:
@@ -223,31 +236,40 @@ class VolumeMigrate(base.BaseAction):
type_names = [vt.name for vt in volume_types]
if self.destination_type not in type_names:
raise exception.ActionExecutionFailure(
_("Volume type %s not found") % self.destination_type)
_("Volume type %s not found") % self.destination_type
)
# Check if destination_node (pool) exists (if specified)
if self.destination_node:
try:
self.cinder_util.get_storage_pool_by_name(
self.destination_node)
self.destination_node
)
except exception.PoolNotFound:
raise exception.ActionExecutionFailure(
_("Pool %s not found") % self.destination_node)
_("Pool %s not found") % self.destination_node
)
# Check if retype to same type
if (self.migration_type == self.RETYPE and
self.destination_type and
volume.volume_type == self.destination_type):
if (
self.migration_type == self.RETYPE
and self.destination_type
and volume.volume_type == self.destination_type
):
raise exception.ActionSkipped(
_("Volume type is already %s") % self.destination_type)
_("Volume type is already %s") % self.destination_type
)
# Check if migrate to same node
if (self.migration_type in (self.SWAP, self.MIGRATE) and
self.destination_node):
if (
self.migration_type in (self.SWAP, self.MIGRATE)
and self.destination_node
):
current_host = getattr(volume, 'os-vol-host-attr:host')
if current_host == self.destination_node:
raise exception.ActionSkipped(
_("Volume is already on node %s") % self.destination_node)
_("Volume is already on node %s") % self.destination_node
)
def post_condition(self):
pass

View File

@@ -51,13 +51,15 @@ class DefaultApplier(base.BaseApplier):
self._engine = self._loader.load(
name=selected_workflow_engine,
context=self.context,
applier_manager=self.applier_manager)
applier_manager=self.applier_manager,
)
return self._engine
def execute(self, action_plan_uuid):
LOG.debug("Executing action plan %s ", action_plan_uuid)
filters = {'action_plan_uuid': action_plan_uuid}
actions = objects.Action.list(self.context, filters=filters,
eager=True)
actions = objects.Action.list(
self.context, filters=filters, eager=True
)
return self.engine.execute(actions)

View File

@@ -16,11 +16,9 @@ from watcher.common.loader import default
class DefaultWorkFlowEngineLoader(default.DefaultLoader):
def __init__(self):
super().__init__(
namespace='watcher_workflow_engines')
super().__init__(namespace='watcher_workflow_engines')
class DefaultActionLoader(default.DefaultLoader):
def __init__(self):
super().__init__(
namespace='watcher_actions')
super().__init__(namespace='watcher_actions')

View File

@@ -26,7 +26,6 @@ CONF = conf.CONF
class ApplierManager(service_manager.ServiceManager):
@property
def service_name(self):
return 'watcher-applier'

View File

@@ -32,13 +32,14 @@ class TriggerActionPlan:
self.applier_manager = applier_manager
workers = CONF.watcher_applier.workers
self.executor = executor.get_futurist_pool_executor(
max_workers=workers)
max_workers=workers
)
def do_launch_action_plan(self, context, action_plan_uuid):
try:
cmd = default.DefaultActionPlanHandler(context,
self.applier_manager,
action_plan_uuid)
cmd = default.DefaultActionPlanHandler(
context, self.applier_manager, action_plan_uuid
)
cmd.execute()
except Exception as e:
LOG.exception(e)
@@ -46,12 +47,14 @@ class TriggerActionPlan:
def launch_action_plan(self, context, action_plan_uuid):
LOG.debug("Trigger ActionPlan %s", action_plan_uuid)
action_plan = objects.ActionPlan.get_by_uuid(
context, action_plan_uuid, eager=True)
context, action_plan_uuid, eager=True
)
action_plan.hostname = CONF.host
action_plan.save()
# submit
executor.log_executor_stats(self.executor, name="action-plan-pool")
self.executor.submit(self.do_launch_action_plan, context,
action_plan_uuid)
self.executor.submit(
self.do_launch_action_plan, context, action_plan_uuid
)
return action_plan_uuid

View File

@@ -27,7 +27,6 @@ CONF = conf.CONF
class ApplierAPI(service.Service):
def __init__(self):
super().__init__(ApplierAPIManager)
@@ -36,11 +35,11 @@ class ApplierAPI(service.Service):
raise exception.InvalidUuidOrName(name=action_plan_uuid)
self.conductor_client.cast(
context, 'launch_action_plan', action_plan_uuid=action_plan_uuid)
context, 'launch_action_plan', action_plan_uuid=action_plan_uuid
)
class ApplierAPIManager(service_manager.ServiceManager):
@property
def service_name(self):
return None

View File

@@ -44,12 +44,19 @@ class ApplierMonitor(service.ServiceMonitoringBase):
pending_actionplans = objects.ActionPlan.list(
context,
filters={'state': objects.action_plan.State.PENDING,
'hostname': host},
eager=True)
filters={
'state': objects.action_plan.State.PENDING,
'hostname': host,
},
eager=True,
)
for actionplan in pending_actionplans:
LOG.warning("Retriggering action plan %s in Pending state on "
"failed host %s", actionplan.uuid, host)
LOG.warning(
"Retriggering action plan %s in Pending state on "
"failed host %s",
actionplan.uuid,
host,
)
actionplan.hostname = None
actionplan.save()
self.applier_client.launch_action_plan(context, actionplan.uuid)
@@ -75,16 +82,18 @@ class ApplierMonitor(service.ServiceMonitoringBase):
# on services status changes
continue
if changed:
notifications.service.send_service_update(context,
watcher_service,
state=result)
notifications.service.send_service_update(
context, watcher_service, state=result
)
if result == failed_s:
# Cancel ongoing action plans on the failed service using
# the existing startup sync method
syncer = sync.Syncer()
syncer._cancel_ongoing_actionplans(context,
watcher_service.host)
syncer._cancel_ongoing_actionplans(
context, watcher_service.host
)
# Pending action plans should be unassigned and
# re-triggered
self._retrigger_pending_actionplans(context,
watcher_service.host)
self._retrigger_pending_actionplans(
context, watcher_service.host
)

View File

@@ -39,7 +39,8 @@ class Syncer:
load_description = load_action.get_description()
try:
action_desc = objects.ActionDescription.get_by_type(
ctx, action_type)
ctx, action_type
)
if action_desc.description != load_description:
action_desc.description = load_description
action_desc.save()
@@ -54,28 +55,43 @@ class Syncer:
hostname = host or CONF.host
actions_plans = objects.ActionPlan.list(
context,
filters={'state': objects.action_plan.State.ONGOING,
'hostname': hostname},
eager=True)
filters={
'state': objects.action_plan.State.ONGOING,
'hostname': hostname,
},
eager=True,
)
for ap in actions_plans:
ap.state = objects.action_plan.State.CANCELLED
ap.status_message = ("Action plan was cancelled because Applier "
f"{hostname} was stopped while the action "
"plan was ongoing.")
ap.status_message = (
"Action plan was cancelled because Applier "
f"{hostname} was stopped while the action "
"plan was ongoing."
)
ap.save()
filters = {'action_plan_uuid': ap.uuid,
'state__in': (objects.action.State.PENDING,
objects.action.State.ONGOING)}
filters = {
'action_plan_uuid': ap.uuid,
'state__in': (
objects.action.State.PENDING,
objects.action.State.ONGOING,
),
}
actions = objects.Action.list(context, filters=filters, eager=True)
for a in actions:
a.state = objects.action.State.CANCELLED
a.status_message = ("Action was cancelled because Applier "
f"{hostname} was stopped while the "
"action plan was ongoing.")
a.status_message = (
"Action was cancelled because Applier "
f"{hostname} was stopped while the "
"action plan was ongoing."
)
a.save()
LOG.info("Action Plan %(uuid)s along with appropriate Actions "
"has been cancelled because it was in %(state)s state "
"when Applier had been stopped on %(hostname)s host.",
{'uuid': ap.uuid,
'state': objects.action_plan.State.ONGOING,
'hostname': ap.hostname})
LOG.info(
"Action Plan %(uuid)s along with appropriate Actions "
"has been cancelled because it was in %(state)s state "
"when Applier had been stopped on %(hostname)s host.",
{
'uuid': ap.uuid,
'state': objects.action_plan.State.ONGOING,
'hostname': ap.hostname,
},
)

View File

@@ -34,7 +34,6 @@ LOG = log.getLogger(__name__)
class BaseWorkFlowEngine(loadable.Loadable, metaclass=abc.ABCMeta):
def __init__(self, config, context=None, applier_manager=None):
"""Constructor
@@ -80,8 +79,9 @@ class BaseWorkFlowEngine(loadable.Loadable, metaclass=abc.ABCMeta):
return self._action_factory
def notify(self, action, state, status_message=None):
db_action = objects.Action.get_by_uuid(self.context, action.uuid,
eager=True)
db_action = objects.Action.get_by_uuid(
self.context, action.uuid, eager=True
)
db_action.state = state
if status_message:
db_action.status_message = status_message
@@ -89,15 +89,17 @@ class BaseWorkFlowEngine(loadable.Loadable, metaclass=abc.ABCMeta):
return db_action
def notify_cancel_start(self, action_plan_uuid):
action_plan = objects.ActionPlan.get_by_uuid(self.context,
action_plan_uuid,
eager=True)
action_plan = objects.ActionPlan.get_by_uuid(
self.context, action_plan_uuid, eager=True
)
if not self._is_notified:
self._is_notified = True
notifications.action_plan.send_cancel_notification(
self._context, action_plan,
self._context,
action_plan,
action=fields.NotificationAction.CANCEL,
phase=fields.NotificationPhase.START)
phase=fields.NotificationPhase.START,
)
@abc.abstractmethod
def execute(self, actions):
@@ -105,7 +107,6 @@ class BaseWorkFlowEngine(loadable.Loadable, metaclass=abc.ABCMeta):
class BaseTaskFlowActionContainer(flow_task.Task):
def __init__(self, name, db_action, engine, **kwargs):
super().__init__(name=name)
self._db_action = db_action
@@ -120,8 +121,8 @@ class BaseTaskFlowActionContainer(flow_task.Task):
def action(self):
if self.loaded_action is None:
action = self.engine.action_factory.make_action(
self._db_action,
osc=self._engine.osc)
self._db_action, osc=self._engine.osc
)
self.loaded_action = action
return self.loaded_action
@@ -148,20 +149,25 @@ class BaseTaskFlowActionContainer(flow_task.Task):
def _fail_action(self, phase, reason=None):
# Fail action and send notification to the user.
# If a reason is given, it will be used to set the status_message.
LOG.error('The workflow engine has failed '
'to execute the action: %s', self._db_action.uuid)
LOG.error(
'The workflow engine has failed to execute the action: %s',
self._db_action.uuid,
)
kwargs = {}
if reason:
kwargs["status_message"] = (_(
"Action failed in %s: %s") % (phase, reason))
db_action = self.engine.notify(self._db_action,
objects.action.State.FAILED,
**kwargs)
kwargs["status_message"] = _(
"Action failed in %(phase)s: %(reason)s"
) % {'phase': phase, 'reason': reason}
db_action = self.engine.notify(
self._db_action, objects.action.State.FAILED, **kwargs
)
notifications.action.send_execution_notification(
self.engine.context, db_action,
self.engine.context,
db_action,
fields.NotificationAction.EXECUTION,
fields.NotificationPhase.ERROR,
priority=fields.NotificationPriority.ERROR)
priority=fields.NotificationPriority.ERROR,
)
# NOTE(alexchadin): taskflow does 3 method calls (pre_execute, execute,
# post_execute) independently. We want to support notifications in base
@@ -172,33 +178,43 @@ class BaseTaskFlowActionContainer(flow_task.Task):
# next action, if action plan is cancelled raise the exceptions
# so that taskflow does not schedule further actions.
action_plan = objects.ActionPlan.get_by_id(
self.engine.context, self._db_action.action_plan_id)
self.engine.context, self._db_action.action_plan_id
)
if action_plan.state in objects.action_plan.State.CANCEL_STATES:
raise exception.ActionPlanCancelled(uuid=action_plan.uuid)
if self._db_action.state == objects.action.State.SKIPPED:
LOG.debug("Action %s is skipped manually",
self._db_action.uuid)
LOG.debug(
"Action %s is skipped manually", self._db_action.uuid
)
return
db_action = self.do_pre_execute()
notifications.action.send_execution_notification(
self.engine.context, db_action,
self.engine.context,
db_action,
fields.NotificationAction.EXECUTION,
fields.NotificationPhase.START)
fields.NotificationPhase.START,
)
except exception.ActionPlanCancelled as e:
LOG.exception(e)
self.engine.notify_cancel_start(action_plan.uuid)
raise
except exception.ActionSkipped as e:
LOG.info("Action %s was skipped automatically: %s",
self._db_action.uuid, str(e))
status_message = (_(
"Action was skipped automatically: %s") % str(e))
db_action = self.engine.notify(self._db_action,
objects.action.State.SKIPPED,
status_message=status_message)
LOG.info(
"Action %s was skipped automatically: %s",
self._db_action.uuid,
str(e),
)
status_message = _("Action was skipped automatically: %s") % str(e)
db_action = self.engine.notify(
self._db_action,
objects.action.State.SKIPPED,
status_message=status_message,
)
notifications.action.send_update(
self.engine.context, db_action,
old_state=objects.action.State.PENDING)
self.engine.context,
db_action,
old_state=objects.action.State.PENDING,
)
except exception.WatcherException as e:
LOG.exception(e)
self._fail_action("pre_condition", reason=str(e))
@@ -208,19 +224,25 @@ class BaseTaskFlowActionContainer(flow_task.Task):
def execute(self, *args, **kwargs):
action_object = objects.Action.get_by_uuid(
self.engine.context, self._db_action.uuid, eager=True)
if action_object.state in [objects.action.State.SKIPPED,
objects.action.State.FAILED]:
self.engine.context, self._db_action.uuid, eager=True
)
if action_object.state in [
objects.action.State.SKIPPED,
objects.action.State.FAILED,
]:
return True
try:
db_action = self.do_execute(*args, **kwargs)
notifications.action.send_execution_notification(
self.engine.context, db_action,
self.engine.context,
db_action,
fields.NotificationAction.EXECUTION,
fields.NotificationPhase.END)
fields.NotificationPhase.END,
)
action_object = objects.Action.get_by_uuid(
self.engine.context, self._db_action.uuid, eager=True)
self.engine.context, self._db_action.uuid, eager=True
)
if action_object.state == objects.action.State.SUCCEEDED:
return True
else:
@@ -236,7 +258,8 @@ class BaseTaskFlowActionContainer(flow_task.Task):
def post_execute(self):
action_object = objects.Action.get_by_uuid(
self.engine.context, self._db_action.uuid, eager=True)
self.engine.context, self._db_action.uuid, eager=True
)
if action_object.state == objects.action.State.SKIPPED:
return
try:
@@ -264,18 +287,21 @@ class BaseTaskFlowActionContainer(flow_task.Task):
def revert(self, *args, **kwargs):
action_plan = objects.ActionPlan.get_by_id(
self.engine.context, self._db_action.action_plan_id, eager=True)
self.engine.context, self._db_action.action_plan_id, eager=True
)
action_object = objects.Action.get_by_uuid(
self.engine.context, self._db_action.uuid, eager=True)
self.engine.context, self._db_action.uuid, eager=True
)
# NOTE: check if revert cause by cancel action plan or
# some other exception occurred during action plan execution
# if due to some other exception keep the flow intact.
# NOTE(dviroel): If the action was skipped, we should not
# revert it.
if (action_plan.state not in
objects.action_plan.State.CANCEL_STATES and
action_object.state != objects.action.State.SKIPPED):
if (
action_plan.state not in objects.action_plan.State.CANCEL_STATES
and action_object.state != objects.action.State.SKIPPED
):
self.do_revert()
return
@@ -284,37 +310,47 @@ class BaseTaskFlowActionContainer(flow_task.Task):
action_object.state = objects.action.State.CANCELLING
action_object.save()
notifications.action.send_cancel_notification(
self.engine.context, action_object,
self.engine.context,
action_object,
fields.NotificationAction.CANCEL,
fields.NotificationPhase.START)
fields.NotificationPhase.START,
)
action_object = self.abort()
notifications.action.send_cancel_notification(
self.engine.context, action_object,
self.engine.context,
action_object,
fields.NotificationAction.CANCEL,
fields.NotificationPhase.END)
fields.NotificationPhase.END,
)
if action_object.state == objects.action.State.PENDING:
notifications.action.send_cancel_notification(
self.engine.context, action_object,
self.engine.context,
action_object,
fields.NotificationAction.CANCEL,
fields.NotificationPhase.START)
fields.NotificationPhase.START,
)
action_object.state = objects.action.State.CANCELLED
action_object.save()
notifications.action.send_cancel_notification(
self.engine.context, action_object,
self.engine.context,
action_object,
fields.NotificationAction.CANCEL,
fields.NotificationPhase.END)
fields.NotificationPhase.END,
)
except Exception as e:
LOG.exception(e)
action_object.state = objects.action.State.FAILED
action_object.save()
notifications.action.send_cancel_notification(
self.engine.context, action_object,
self.engine.context,
action_object,
fields.NotificationAction.CANCEL,
fields.NotificationPhase.ERROR,
priority=fields.NotificationPriority.ERROR)
priority=fields.NotificationPriority.ERROR,
)
def abort(self, *args, **kwargs):
# NOTE(dviroel): only ONGOING actions are called

View File

@@ -65,26 +65,29 @@ class DefaultWorkFlowEngine(base.BaseWorkFlowEngine):
min=1,
required=True,
help='Number of workers for taskflow engine '
'to execute actions.'),
'to execute actions.',
),
cfg.DictOpt(
'action_execution_rule',
default={},
help='The execution rule for linked actions,'
'the key is strategy name and '
'value ALWAYS means all actions will be executed,'
'value ANY means if previous action executes '
'success, the next action will be ignored.'
'None means ALWAYS.')
]
'the key is strategy name and '
'value ALWAYS means all actions will be executed,'
'value ANY means if previous action executes '
'success, the next action will be ignored.'
'None means ALWAYS.',
),
]
def get_execution_rule(self, actions):
if actions:
actionplan_object = objects.ActionPlan.get_by_id(
self.context, actions[0].action_plan_id)
self.context, actions[0].action_plan_id
)
strategy_object = objects.Strategy.get_by_id(
self.context, actionplan_object.strategy_id)
return self.config.action_execution_rule.get(
strategy_object.name)
self.context, actionplan_object.strategy_id
)
return self.config.action_execution_rule.get(strategy_object.name)
def execute(self, actions):
try:
@@ -106,20 +109,28 @@ class DefaultWorkFlowEngine(base.BaseWorkFlowEngine):
for a in actions:
for parent_id in a.parents:
flow.link(actions_uuid[parent_id], actions_uuid[a.uuid],
decider=self.decider)
flow.link(
actions_uuid[parent_id],
actions_uuid[a.uuid],
decider=self.decider,
)
e = None
engine_type = "parallel"
if eventlet_helper.is_patched():
executor_type = "greenthreaded"
else:
LOG.info("Using Taskflow parallel engine when running "
"in native threading mode.")
LOG.info(
"Using Taskflow parallel engine when running "
"in native threading mode."
)
executor_type = "threaded"
e = engines.load(
flow, executor=executor_type, engine=engine_type,
max_workers=self.config.max_workers)
flow,
executor=executor_type,
engine=engine_type,
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):

View File

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

View File

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

View File

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

View File

@@ -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__':

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,7 +17,6 @@ import abc
class BaseLoader(metaclass=abc.ABCMeta):
@abc.abstractmethod
def list_available(self):
raise NotImplementedError()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,7 +17,6 @@ import abc
class ServiceManager(metaclass=abc.ABCMeta):
@property
@abc.abstractmethod
def service_name(self):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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(),
)
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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