Compare commits

..

4 Commits

Author SHA1 Message Date
Thierry Carrez
ab64069e23 Move queue declaration to project level
This moves the watcher queue declaration from the pipeline level
(where it is no longer valid) to the project level.

https: //lists.openstack.org/pipermail/openstack-discuss/2022-May/028603.html
Change-Id: I06923abb00f7eecd59587f44cd1f6a069e88a9fc
(cherry picked from commit 6003322711)
2023-08-19 07:30:34 +00:00
OpenStack Release Bot
d8c7ad662f Update TOX_CONSTRAINTS_FILE for stable/victoria
Update the URL to the upper-constraints file to point to the redirect
rule on releases.openstack.org so that anyone working on this branch
will switch to the correct upper-constraints list automatically when
the requirements repository branches.

Until the requirements repository has as stable/victoria branch, tests will
continue to use the upper-constraints list on master.

Change-Id: Ieb0a95af10dd8e054254618a9021e69aca68191a
2020-09-30 01:54:48 +00:00
OpenStack Release Bot
9e3ce13f7b Update .gitreview for stable/victoria
Change-Id: I3314bd862a24fb5b114a562068168ef673b49397
2020-09-30 01:40:38 +00:00
Ghanshyam Mann
507765dc3e [goal] Migrate testing to ubuntu focal
As per victoria cycle testing runtime and community goal[1]
we need to migrate upstream CI/CD to Ubuntu Focal(20.04).

Fixing:
- bug#1886298
Bump the lower constraints for required deps which added python3.8 support
in their later version.

- Move multinode jobs to focal nodeset

Story: #2007865
Task: #40227

Closes-Bug: #1886298

[1] https://governance.openstack.org/tc/goals/selected/victoria/migrate-ci-cd-jobs-to-ubuntu-focal>

Depends-On: https://review.opendev.org/#/c/752294/

Change-Id: Iec953f3294087cd0b628b701ad3d684cea61c057
(cherry picked from commit e21e5f609e)
2020-09-29 09:02:47 +00:00
127 changed files with 1498 additions and 2981 deletions

View File

@@ -2,4 +2,4 @@
host=review.opendev.org
port=29418
project=openstack/watcher.git
defaultbranch=stable/2024.2
defaultbranch=stable/victoria

View File

@@ -3,7 +3,8 @@
templates:
- check-requirements
- openstack-cover-jobs
- openstack-python3-jobs
- openstack-lower-constraints-jobs
- openstack-python3-victoria-jobs
- publish-openstack-docs-pti
- release-notes-jobs-python3
check:
@@ -13,6 +14,7 @@
- watcher-tempest-strategies
- watcher-tempest-actuator
- watcherclient-tempest-functional
- watcher-tls-test
- watcher-tempest-functional-ipv6-only
gate:
jobs:
@@ -85,12 +87,22 @@
vars:
tempest_concurrency: 1
tempest_test_regex: watcher_tempest_plugin.tests.scenario.test_execute_strategies
tempest_exclude_regex: .*\[.*\breal_load\b.*\].*
- job:
name: watcher-tls-test
parent: watcher-tempest-multinode
group-vars:
subnode:
devstack_services:
tls-proxy: true
vars:
devstack_services:
tls-proxy: true
- job:
name: watcher-tempest-multinode
parent: watcher-tempest-functional
nodeset: openstack-two-node-jammy
nodeset: openstack-two-node-focal
roles:
- zuul: openstack/tempest
group-vars:
@@ -108,7 +120,8 @@
watcher-api: false
watcher-decision-engine: true
watcher-applier: false
c-bak: false
# We need to add TLS support for watcher plugin
tls-proxy: false
ceilometer: false
ceilometer-acompute: false
ceilometer-acentral: false
@@ -156,6 +169,7 @@
devstack_plugins:
watcher: https://opendev.org/openstack/watcher
devstack_services:
tls-proxy: false
watcher-api: true
watcher-decision-engine: true
watcher-applier: true

View File

@@ -275,9 +275,6 @@ function install_watcherclient {
git_clone_by_name "python-watcherclient"
setup_dev_lib "python-watcherclient"
fi
if [[ "$GLOBAL_VENV" == "True" ]]; then
sudo ln -sf /opt/stack/data/venv/bin/watcher /usr/local/bin
fi
}
# install_watcher() - Collect source and prepare
@@ -341,19 +338,6 @@ function stop_watcher {
done
}
# configure_tempest_for_watcher() - Configure Tempest for watcher
function configure_tempest_for_watcher {
# Set default microversion for watcher-tempest-plugin
# Please make sure to update this when the microversion is updated, otherwise
# new tests may be skipped.
TEMPEST_WATCHER_MIN_MICROVERSION=${TEMPEST_WATCHER_MIN_MICROVERSION:-"1.0"}
TEMPEST_WATCHER_MAX_MICROVERSION=${TEMPEST_WATCHER_MAX_MICROVERSION:-"1.4"}
# Set microversion options in tempest.conf
iniset $TEMPEST_CONFIG optimize min_microversion $TEMPEST_WATCHER_MIN_MICROVERSION
iniset $TEMPEST_CONFIG optimize max_microversion $TEMPEST_WATCHER_MAX_MICROVERSION
}
# Restore xtrace
$_XTRACE_WATCHER

View File

@@ -38,9 +38,6 @@ if is_service_enabled watcher-api watcher-decision-engine watcher-applier; then
# Start the watcher components
echo_summary "Starting watcher"
start_watcher
elif [[ "$1" == "stack" && "$2" == "test-config" ]]; then
echo_summary "Configuring tempest for watcher"
configure_tempest_for_watcher
fi
if [[ "$1" == "unstack" ]]; then

View File

@@ -70,7 +70,7 @@ then write_uwsgi_config "$WATCHER_UWSGI_CONF" "$WATCHER_UWSGI" "/infra-optim"
fi
# Migrate the database
$WATCHER_BIN_DIR/watcher-db-manage upgrade || die $LINO "DB migration error"
watcher-db-manage upgrade || die $LINO "DB migration error"
start_watcher

View File

@@ -17,14 +17,6 @@
Policies
========
.. warning::
JSON formatted policy file is deprecated since Watcher 6.0.0 (Wallaby).
This `oslopolicy-convert-json-to-yaml`__ tool will migrate your existing
JSON-formatted policy file to YAML in a backward-compatible way.
.. __: https://docs.openstack.org/oslo.policy/latest/cli/oslopolicy-convert-json-to-yaml.html
Watcher's public API calls may be restricted to certain sets of users using a
policy configuration file. This document explains exactly how policies are
configured and what they apply to.

View File

@@ -56,8 +56,8 @@ source_suffix = '.rst'
master_doc = 'index'
# General information about the project.
project = 'Watcher'
copyright = 'OpenStack Foundation'
project = u'Watcher'
copyright = u'OpenStack Foundation'
# A list of ignored prefixes for module index sorting.
modindex_common_prefix = ['watcher.']
@@ -91,14 +91,14 @@ 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-api', 'watcher-api', u'Watcher API Server',
[u'OpenStack'], 1),
('man/watcher-applier', 'watcher-applier', u'Watcher Applier',
[u'OpenStack'], 1),
('man/watcher-db-manage', 'watcher-db-manage',
'Watcher Db Management Utility', ['OpenStack'], 1),
u'Watcher Db Management Utility', [u'OpenStack'], 1),
('man/watcher-decision-engine', 'watcher-decision-engine',
'Watcher Decision Engine', ['OpenStack'], 1),
u'Watcher Decision Engine', [u'OpenStack'], 1),
]
# -- Options for HTML output --------------------------------------------------
@@ -128,8 +128,8 @@ openstackdocs_bug_tag = ''
latex_documents = [
('index',
'doc-watcher.tex',
'Watcher Documentation',
'OpenStack Foundation', 'manual'),
u'Watcher Documentation',
u'OpenStack Foundation', 'manual'),
]
# If false, no module index is generated.

View File

@@ -372,7 +372,7 @@ You can configure and install Ceilometer by following the documentation below :
#. https://docs.openstack.org/ceilometer/latest
The built-in strategy 'basic_consolidation' provided by watcher requires
"**compute.node.cpu.percent**" and "**cpu**" measurements to be collected
"**compute.node.cpu.percent**" and "**cpu_util**" measurements to be collected
by Ceilometer.
The measurements available depend on the hypervisors that OpenStack manages on
the specific implementation.

View File

@@ -47,8 +47,6 @@ unavailable as well as `instance_l3_cpu_cache`::
[[local|localrc]]
enable_plugin watcher https://opendev.org/openstack/watcher
enable_plugin watcher-dashboard https://opendev.org/openstack/watcher-dashboard
enable_plugin ceilometer https://opendev.org/openstack/ceilometer.git
CEILOMETER_BACKEND=gnocchi

View File

@@ -56,6 +56,9 @@ Here is an example showing how you can write a plugin called ``NewStrategy``:
# filepath: thirdparty/new.py
# import path: thirdparty.new
import abc
import six
from watcher._i18n import _
from watcher.decision_engine.strategy.strategies import base
@@ -300,6 +303,6 @@ Using that you can now query the values for that specific metric:
.. code-block:: py
avg_meter = self.datasource_backend.statistic_aggregation(
instance.uuid, 'instance_cpu_usage', self.periods['instance'],
instance.uuid, 'cpu_util', self.periods['instance'],
self.granularity,
aggregation=self.aggregation_method['instance'])

View File

@@ -26,7 +26,8 @@ metric service name plugins comment
``compute_monitors`` option
to ``cpu.virt_driver`` in
the nova.conf.
``cpu`` ceilometer_ none
``cpu_util`` ceilometer_ none cpu_util has been removed
since Stein.
============================ ============ ======= ===========================
.. _ceilometer: https://docs.openstack.org/ceilometer/latest/admin/telemetry-measurements.html#openstack-compute

View File

@@ -89,9 +89,9 @@ step 2: Create audit to do optimization
.. code-block:: shell
$ openstack optimize audittemplate create \
saving_energy_template1 saving_energy --strategy saving_energy
at1 saving_energy --strategy saving_energy
$ openstack optimize audit create -a saving_energy_audit1 \
$ openstack optimize audit create -a at1 \
-p free_used_percent=20.0
External Links

View File

@@ -22,19 +22,14 @@ The *vm_workload_consolidation* strategy requires the following metrics:
============================ ============ ======= =========================
metric service name plugins comment
============================ ============ ======= =========================
``cpu`` ceilometer_ none
``cpu_util`` ceilometer_ none cpu_util has been removed
since Stein.
``memory.resident`` ceilometer_ none
``memory`` ceilometer_ none
``disk.root.size`` ceilometer_ none
``compute.node.cpu.percent`` ceilometer_ none (optional) need to set the
``compute_monitors`` option
to ``cpu.virt_driver`` in the
nova.conf.
``hardware.memory.used`` ceilometer_ SNMP_ (optional)
============================ ============ ======= =========================
.. _ceilometer: https://docs.openstack.org/ceilometer/latest/admin/telemetry-measurements.html#openstack-compute
.. _SNMP: https://docs.openstack.org/ceilometer/latest/admin/telemetry-measurements.html#snmp-based-meters
Cluster data model
******************

View File

@@ -27,8 +27,9 @@ metric service name plugins comment
to ``cpu.virt_driver`` in the
nova.conf.
``hardware.memory.used`` ceilometer_ SNMP_
``cpu`` ceilometer_ none
``instance_ram_usage`` ceilometer_ none
``cpu_util`` ceilometer_ none cpu_util has been removed
since Stein.
``memory.resident`` ceilometer_ none
============================ ============ ======= =============================
.. _ceilometer: https://docs.openstack.org/ceilometer/latest/admin/telemetry-measurements.html#openstack-compute
@@ -106,10 +107,10 @@ parameter type default Value description
period of all received ones.
==================== ====== ===================== =============================
.. |metrics| replace:: ["instance_cpu_usage", "instance_ram_usage"]
.. |thresholds| replace:: {"instance_cpu_usage": 0.2, "instance_ram_usage": 0.2}
.. |weights| replace:: {"instance_cpu_usage_weight": 1.0, "instance_ram_usage_weight": 1.0}
.. |instance_metrics| replace:: {"instance_cpu_usage": "compute.node.cpu.percent", "instance_ram_usage": "hardware.memory.used"}
.. |metrics| replace:: ["cpu_util", "memory.resident"]
.. |thresholds| replace:: {"cpu_util": 0.2, "memory.resident": 0.2}
.. |weights| replace:: {"cpu_util_weight": 1.0, "memory.resident_weight": 1.0}
.. |instance_metrics| replace:: {"cpu_util": "compute.node.cpu.percent", "memory.resident": "hardware.memory.used"}
.. |periods| replace:: {"instance": 720, "node": 600}
Efficacy Indicator
@@ -135,8 +136,8 @@ How to use it ?
at1 workload_balancing --strategy workload_stabilization
$ openstack optimize audit create -a at1 \
-p thresholds='{"instance_ram_usage": 0.05}' \
-p metrics='["instance_ram_usage"]'
-p thresholds='{"memory.resident": 0.05}' \
-p metrics='["memory.resident"]'
External Links
--------------

View File

@@ -24,7 +24,8 @@ The *workload_balance* strategy requires the following metrics:
======================= ============ ======= =========================
metric service name plugins comment
======================= ============ ======= =========================
``cpu`` ceilometer_ none
``cpu_util`` ceilometer_ none cpu_util has been removed
since Stein.
``memory.resident`` ceilometer_ none
======================= ============ ======= =========================
@@ -64,16 +65,15 @@ Configuration
Strategy parameters are:
============== ====== ==================== ====================================
parameter type default Value description
============== ====== ==================== ====================================
``metrics`` String 'instance_cpu_usage' Workload balance base on cpu or ram
utilization. Choices:
['instance_cpu_usage',
'instance_ram_usage']
``threshold`` Number 25.0 Workload threshold for migration
``period`` Number 300 Aggregate time period of ceilometer
============== ====== ==================== ====================================
============== ====== ============= ====================================
parameter type default Value description
============== ====== ============= ====================================
``metrics`` String 'cpu_util' Workload balance base on cpu or ram
utilization. choice: ['cpu_util',
'memory.resident']
``threshold`` Number 25.0 Workload threshold for migration
``period`` Number 300 Aggregate time period of ceilometer
============== ====== ============= ====================================
Efficacy Indicator
------------------
@@ -95,7 +95,7 @@ How to use it ?
at1 workload_balancing --strategy workload_balance
$ openstack optimize audit create -a at1 -p threshold=26.0 \
-p period=310 -p metrics=instance_cpu_usage
-p period=310 -p metrics=cpu_util
External Links
--------------

150
lower-constraints.txt Normal file
View File

@@ -0,0 +1,150 @@
alabaster==0.7.10
alembic==0.9.8
amqp==2.2.2
appdirs==1.4.3
APScheduler==3.5.1
asn1crypto==0.24.0
automaton==1.14.0
beautifulsoup4==4.6.0
cachetools==2.0.1
certifi==2018.1.18
cffi==1.14.0
chardet==3.0.4
cliff==2.11.0
cmd2==0.8.1
contextlib2==0.5.5
coverage==4.5.1
croniter==0.3.20
cryptography==2.1.4
debtcollector==1.19.0
decorator==4.2.1
deprecation==2.0
doc8==0.8.0
docutils==0.14
dogpile.cache==0.6.5
dulwich==0.19.0
enum34==1.1.6
enum-compat==0.0.2
eventlet==0.20.0
extras==1.0.0
fasteners==0.14.1
fixtures==3.0.0
freezegun==0.3.10
futurist==1.8.0
gitdb2==2.0.3
GitPython==2.1.8
gnocchiclient==7.0.1
greenlet==0.4.15
idna==2.6
imagesize==1.0.0
iso8601==0.1.12
Jinja2==2.10
jmespath==0.9.3
jsonpatch==1.21
jsonpointer==2.0
jsonschema==3.2.0
keystoneauth1==3.4.0
keystonemiddleware==4.21.0
kombu==5.0.0
linecache2==1.0.0
logutils==0.3.5
lxml==4.5.1
Mako==1.0.7
MarkupSafe==1.1.1
mccabe==0.2.1
microversion_parse==0.2.1
monotonic==1.4
msgpack==0.5.6
munch==2.2.0
netaddr==0.7.19
netifaces==0.10.6
networkx==2.4
openstacksdk==0.12.0
os-api-ref===1.4.0
os-client-config==1.29.0
os-service-types==1.2.0
os-testr==1.0.0
osc-lib==1.10.0
os-resource-classes==0.4.0
oslo.cache==1.29.0
oslo.concurrency==3.26.0
oslo.config==5.2.0
oslo.context==2.21.0
oslo.db==4.44.0
oslo.i18n==3.20.0
oslo.log==3.37.0
oslo.messaging==8.1.2
oslo.middleware==3.35.0
oslo.policy==1.34.0
oslo.reports==1.27.0
oslo.serialization==2.25.0
oslo.service==1.30.0
oslo.upgradecheck==0.1.0
oslo.utils==3.36.0
oslo.versionedobjects==1.32.0
oslotest==3.3.0
packaging==17.1
Paste==2.0.3
PasteDeploy==1.5.2
pbr==3.1.1
pecan==1.3.2
pika==0.10.0
pika-pool==0.1.3
prettytable==0.7.2
psutil==5.4.3
pycadf==2.7.0
pycparser==2.18
Pygments==2.2.0
pyinotify==0.9.6
pyOpenSSL==17.5.0
pyparsing==2.2.0
pyperclip==1.6.0
python-ceilometerclient==2.9.0
python-cinderclient==3.5.0
python-dateutil==2.7.0
python-editor==1.0.3
python-glanceclient==2.9.1
python-ironicclient==2.5.0
python-keystoneclient==3.15.0
python-mimeparse==1.6.0
python-monascaclient==1.12.0
python-neutronclient==6.7.0
python-novaclient==14.1.0
python-openstackclient==3.14.0
python-subunit==1.2.0
pytz==2018.3
PyYAML==3.13
repoze.lru==0.7
requests==2.18.4
requestsexceptions==1.4.0
restructuredtext-lint==1.1.3
rfc3986==1.1.0
Routes==2.4.1
simplegeneric==0.8.1
simplejson==3.13.2
smmap2==2.0.3
snowballstemmer==1.2.1
SQLAlchemy==1.2.5
sqlalchemy-migrate==0.11.0
sqlparse==0.2.4
statsd==3.2.2
stestr==2.0.0
stevedore==1.28.0
taskflow==3.8.0
Tempita==0.5.2
tenacity==4.12.0
testresources==2.0.1
testscenarios==0.5.0
testtools==2.3.0
traceback2==1.4.0
tzlocal==1.5.1
ujson==1.35
unittest2==1.1.0
urllib3==1.22
vine==1.1.4
waitress==1.1.0
warlock==1.3.0
WebOb==1.8.5
WebTest==2.0.29
wrapt==1.10.11
WSME==0.9.2

View File

@@ -1,47 +0,0 @@
---
security:
- |
Watchers no longer forges requests on behalf of a tenant when
swapping volumes. Prior to this release watcher had 2 implementations
of moving a volume, it could use cinders volume migrate api or its own
internal implementation that directly calls nova volume attachment update
api. The former is safe and the recommend way to move volumes between
cinder storage backend the internal implementation was insecure, fragile
due to a lack of error handling and capable of deleting user data.
Insecure: the internal volume migration operation created a new keystone
user with a weak name and password and added it to the tenants project
with the admin role. It then used that user to forge request on behalf
of the tenant with admin right to swap the volume. if the applier was
restarted during the execution of this operation it would never be cleaned
up.
Fragile: the error handling was minimal, the swap volume api is async
so watcher has to poll for completion, there was no support to resume
that if interrupted of the time out was exceeded.
Data-loss: while the internal polling logic returned success or failure
watcher did not check the result, once the function returned it
unconditionally deleted the source volume. For larger volumes this
could result in irretrievable data loss.
Finally if a volume was swapped using the internal workflow it put
the nova instance in an out of sync state. If the VM was live migrated
after the swap volume completed successfully prior to a hard reboot
then the migration would fail or succeed and break tenant isolation.
see: https://bugs.launchpad.net/nova/+bug/2112187 for details.
fixes:
- |
All code related to creating keystone user and granting roles has been
removed. The internal swap volume implementation has been removed and
replaced by cinders volume migrate api. Note as part of this change
Watcher will no longer attempt volume migrations or retypes if the
instance is in the `Verify Resize` task state. This resolves several
issues related to volume migration in the zone migration and
Storage capacity balance strategies. While efforts have been made
to maintain backward compatibility these changes are required to
address a security weakness in watcher's prior approach.
see: https://bugs.launchpad.net/nova/+bug/2112187 for more context.

View File

@@ -1,20 +0,0 @@
---
upgrade:
- |
The default value of ``[oslo_policy] policy_file`` config option has
been changed from ``policy.json`` to ``policy.yaml``.
Operators who are utilizing customized or previously generated
static policy JSON files (which are not needed by default), should
generate new policy files or convert them in YAML format. Use the
`oslopolicy-convert-json-to-yaml
<https://docs.openstack.org/oslo.policy/latest/cli/oslopolicy-convert-json-to-yaml.html>`_
tool to convert a JSON to YAML formatted policy file in
backward compatible way.
deprecations:
- |
Use of JSON policy files was deprecated by the ``oslo.policy`` library
during the Victoria development cycle. As a result, this deprecation is
being noted in the Wallaby cycle with an anticipated future removal of support
by ``oslo.policy``. As such operators will need to convert to YAML policy
files. Please see the upgrade notes for details on migration of any
custom policy files.

View File

@@ -1,6 +0,0 @@
===========================
2023.1 Series Release Notes
===========================
.. release-notes::
:branch: stable/2023.1

View File

@@ -1,6 +0,0 @@
===========================
2023.2 Series Release Notes
===========================
.. release-notes::
:branch: stable/2023.2

View File

@@ -53,7 +53,7 @@ source_suffix = '.rst'
master_doc = 'index'
# General information about the project.
copyright = '2016, Watcher developers'
copyright = u'2016, Watcher developers'
# Release notes are version independent
# The short X.Y version.
@@ -196,8 +196,8 @@ 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', u'Watcher Documentation',
u'Watcher developers', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
@@ -226,8 +226,8 @@ 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', u'Watcher Documentation',
[u'Watcher developers'], 1)
]
# If true, show URL addresses after external links.
@@ -240,8 +240,8 @@ 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.',
('index', 'watcher', u'Watcher Documentation',
u'Watcher developers', 'watcher', 'One line description of project.',
'Miscellaneous'),
]

View File

@@ -21,13 +21,6 @@ Contents:
:maxdepth: 1
unreleased
2023.2
2023.1
zed
yoga
xena
wallaby
victoria
ussuri
train
stein

View File

@@ -1,18 +1,14 @@
# Andi Chandler <andi@gowling.com>, 2017. #zanata
# Andi Chandler <andi@gowling.com>, 2018. #zanata
# Andi Chandler <andi@gowling.com>, 2020. #zanata
# Andi Chandler <andi@gowling.com>, 2022. #zanata
# Andi Chandler <andi@gowling.com>, 2023. #zanata
# Andi Chandler <andi@gowling.com>, 2024. #zanata
msgid ""
msgstr ""
"Project-Id-Version: python-watcher\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-05-31 14:40+0000\n"
"POT-Creation-Date: 2018-11-08 01:22+0000\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"PO-Revision-Date: 2024-04-18 12:21+0000\n"
"PO-Revision-Date: 2018-11-07 06:15+0000\n"
"Last-Translator: Andi Chandler <andi@gowling.com>\n"
"Language-Team: English (United Kingdom)\n"
"Language: en_GB\n"
@@ -58,70 +54,6 @@ msgstr "1.7.0"
msgid "1.9.0"
msgstr "1.9.0"
msgid "2.0.0"
msgstr "2.0.0"
msgid "2023.1 Series Release Notes"
msgstr "2023.1 Series Release Notes"
msgid "2023.2 Series Release Notes"
msgstr "2023.2 Series Release Notes"
msgid "3.0.0"
msgstr "3.0.0"
msgid "4.0.0"
msgstr "4.0.0"
msgid "6.0.0"
msgstr "6.0.0"
msgid "A ``watcher-status upgrade check`` has been added for this."
msgstr "A ``watcher-status upgrade check`` has been added for this."
msgid ""
"A new threadpool for the decision engine that contributors can use to "
"improve the performance of many operations, primarily I/O bound onces. The "
"amount of workers used by the decision engine threadpool can be configured "
"to scale according to the available infrastructure using the "
"`watcher_decision_engine.max_general_workers` config option. Documentation "
"for contributors to effectively use this threadpool is available online: "
"https://docs.openstack.org/watcher/latest/contributor/concurrency.html"
msgstr ""
"A new threadpool for the decision engine that contributors can use to "
"improve the performance of many operations, primarily I/O bound onces. The "
"amount of workers used by the decision engine threadpool can be configured "
"to scale according to the available infrastructure using the "
"`watcher_decision_engine.max_general_workers` config option. Documentation "
"for contributors to effectively use this threadpool is available online: "
"https://docs.openstack.org/watcher/latest/contributor/concurrency.html"
msgid ""
"API calls while building the Compute data model will be retried upon "
"failure. The amount of failures allowed before giving up and the time before "
"reattempting are configurable. The `api_call_retries` and "
"`api_query_timeout` parameters in the `[collector]` group can be used to "
"adjust these paremeters. 10 retries with a 1 second time in between "
"reattempts is the default."
msgstr ""
"API calls while building the Compute data model will be retried upon "
"failure. The amount of failures allowed before giving up and the time before "
"reattempting are configurable. The `api_call_retries` and "
"`api_query_timeout` parameters in the `[collector]` group can be used to "
"adjust these parameters. 10 retries with a 1 second time in between "
"reattempts is the default."
msgid ""
"Add a new webhook API and a new audit type EVENT, the microversion is 1.4. "
"Now Watcher user can create audit with EVENT type and the audit will be "
"triggered by webhook API. The user guide is available online: https://docs."
"openstack.org/watcher/latest/user/event_type_audit.html"
msgstr ""
"Add a new webhook API and a new audit type EVENT, the microversion is 1.4. "
"Now Watcher user can create audit with EVENT type and the audit will be "
"triggered by webhook API. The user guide is available online: https://docs."
"openstack.org/watcher/latest/user/event_type_audit.html"
msgid "Add a service supervisor to watch Watcher deamons."
msgstr "Add a service supervisor to watch Watcher daemons."
@@ -135,24 +67,6 @@ msgstr ""
"Add description property for dynamic action. Admin can see detail "
"information of any specify action."
msgid ""
"Add force field to Audit. User can set --force to enable the new option when "
"launching audit. If force is True, audit will be executed despite of ongoing "
"actionplan. The new audit may create a wrong actionplan if they use the same "
"data model."
msgstr ""
"Add force field to Audit. User can set --force to enable the new option when "
"launching audit. If force is True, audit will be executed despite of ongoing "
"actionplan. The new audit may create a wrong actionplan if they use the same "
"data model."
msgid ""
"Add keystone_client Group for user to configure 'interface' and "
"'region_name' by watcher.conf. The default value of 'interface' is 'admin'."
msgstr ""
"Add keystone_client Group for user to configure 'interface' and "
"'region_name' by watcher.conf. The default value of 'interface' is 'admin'."
msgid "Add notifications related to Action object."
msgstr "Add notifications related to Action object."
@@ -165,25 +79,6 @@ msgstr "Add notifications related to Audit object."
msgid "Add notifications related to Service object."
msgstr "Add notifications related to Service object."
msgid ""
"Add show data model api for Watcher. New in version 1.3. User can use "
"'openstack optimize datamodel list' command to view the current data model "
"information in memory. User can also add '--audit <Audit_UUID>' to view "
"specific data model in memory filted by the scope in audit. User can also "
"add '--detail' to view detailed information about current data model. User "
"can also add '--type <type>' to specify the type of data model. Default type "
"is 'compute'. In the future, type 'storage' and 'baremetal' will be "
"supported."
msgstr ""
"Add show data model API for Watcher. New in version 1.3. User can use "
"'openstack optimize datamodel list' command to view the current data model "
"information in memory. User can also add '--audit <Audit_UUID>' to view "
"specific data model in memory filtered by the scope in audit. User can also "
"add '--detail' to view detailed information about current data model. User "
"can also add '--type <type>' to specify the type of data model. Default type "
"is 'compute'. In the future, type 'storage' and 'baremetal' will be "
"supported."
msgid ""
"Add start_time and end_time fields in audits table. User can set the start "
"time and/or end time when creating CONTINUOUS audit."
@@ -198,19 +93,6 @@ msgstr ""
"Add superseded state for an action plan if the cluster data model has "
"changed after it has been created."
msgid ""
"Added Placement API helper to Watcher. Now Watcher can get information about "
"resource providers, it can be used for the data model and strategies. Config "
"group placement_client with options 'api_version', 'interface' and "
"'region_name' is also added. The default values for 'api_version' and "
"'interface' are 1.29 and 'public', respectively."
msgstr ""
"Added Placement API helper to Watcher. Now Watcher can get information about "
"resource providers, it can be used for the data model and strategies. Config "
"group placement_client with options 'api_version', 'interface' and "
"'region_name' is also added. The default values for 'api_version' and "
"'interface' are 1.29 and 'public', respectively."
msgid "Added SUSPENDED audit state"
msgstr "Added SUSPENDED audit state"
@@ -225,31 +107,6 @@ msgstr ""
"scoring engine by different Strategies, which improve the code and data "
"model re-use."
msgid ""
"Added a new config option 'action_execution_rule' which is a dict type. Its "
"key field is strategy name and the value is 'ALWAYS' or 'ANY'. 'ALWAYS' "
"means the callback function returns True as usual. 'ANY' means the return "
"depends on the result of previous action execution. The callback returns "
"True if previous action gets failed, and the engine continues to run the "
"next action. If previous action executes success, the callback returns False "
"then the next action will be ignored. For strategies that aren't in "
"'action_execution_rule', the callback always returns True. Please add the "
"next section in the watcher.conf file if your strategy needs this feature. "
"[watcher_workflow_engines.taskflow] action_execution_rule = {'your strategy "
"name': 'ANY'}"
msgstr ""
"Added a new config option 'action_execution_rule' which is a dict type. Its "
"key field is strategy name and the value is 'ALWAYS' or 'ANY'. 'ALWAYS' "
"means the callback function returns True as usual. 'ANY' means the return "
"depends on the result of previous action execution. The callback returns "
"True if previous action gets failed, and the engine continues to run the "
"next action. If previous action executes success, the callback returns False "
"then the next action will be ignored. For strategies that aren't in "
"'action_execution_rule', the callback always returns True. Please add the "
"next section in the watcher.conf file if your strategy needs this feature. "
"[watcher_workflow_engines.taskflow] action_execution_rule = {'your strategy "
"name': 'ANY'}"
msgid ""
"Added a new strategy based on the airflow of servers. This strategy makes "
"decisions to migrate VMs to make the airflow uniform."
@@ -391,15 +248,6 @@ msgstr ""
"The strategy migrates many instances and volumes efficiently with minimum "
"downtime automatically."
msgid ""
"Added strategy \"node resource consolidation\". This strategy is used to "
"centralize VMs to as few nodes as possible by VM migration. User can set an "
"input parameter to decide how to select the destination node."
msgstr ""
"Added strategy \"node resource consolidation\". This strategy is used to "
"centralize VMs to as few nodes as possible by VM migration. User can set an "
"input parameter to decide how to select the destination node."
msgid ""
"Added strategy to identify and migrate a Noisy Neighbor - a low priority VM "
"that negatively affects peformance of a high priority VM by over utilizing "
@@ -436,19 +284,6 @@ msgstr ""
msgid "Adds baremetal data model in Watcher"
msgstr "Adds baremetal data model in Watcher"
msgid ""
"All datasources can now be configured to retry retrieving a metric upon "
"encountering an error. Between each attempt will be a set amount of time "
"which can be adjusted from the configuration. These configuration options "
"can be found in the `[watcher_datasources]` group and are named "
"`query_max_retries` and `query_timeout`."
msgstr ""
"All datasources can now be configured to retry retrieving a metric upon "
"encountering an error. Between each attempt will be a set amount of time "
"which can be adjusted from the configuration. These configuration options "
"can be found in the `[watcher_datasources]` group and are named "
"`query_max_retries` and `query_timeout`."
msgid ""
"Allow decision engine to pass strategy parameters, like optimization "
"threshold, to selected strategy, also strategy to provide parameters info to "
@@ -458,34 +293,6 @@ msgstr ""
"threshold, to selected strategy, also strategy to provide parameters info to "
"end user."
msgid ""
"Allow using file to override metric map. Override the metric map of each "
"datasource as soon as it is created by the manager. This override comes from "
"a file whose path is provided by a setting in config file. The setting is "
"`watcher_decision_engine/metric_map_path`. 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. This setting "
"defaults to `/etc/watcher/metric_map.yaml`, and presence of this file is "
"optional."
msgstr ""
"Allow using file to override metric map. Override the metric map of each "
"datasource as soon as it is created by the manager. This override comes from "
"a file whose path is provided by a setting in config file. The setting is "
"`watcher_decision_engine/metric_map_path`. 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. This setting "
"defaults to `/etc/watcher/metric_map.yaml`, and presence of this file is "
"optional."
msgid ""
"An Watcher API WSGI application script ``watcher-api-wsgi`` is now "
"available. It is auto-generated by ``pbr`` and allows to run the API service "
"using WSGI server (for example Nginx and uWSGI)."
msgstr ""
"An Watcher API WSGI application script ``watcher-api-wsgi`` is now "
"available. It is auto-generated by ``pbr`` and allows to run the API service "
"using WSGI server (for example Nginx and uWSGI)."
msgid ""
"Audits have 'name' field now, that is more friendly to end users. Audit's "
"name can't exceed 63 characters."
@@ -493,25 +300,9 @@ msgstr ""
"Audits have 'name' field now, that is more friendly to end users. Audit's "
"name can't exceed 63 characters."
msgid ""
"Baremetal Model gets Audit scoper with an ability to exclude Ironic nodes."
msgstr ""
"Baremetal Model gets Audit scope with an ability to exclude Ironic nodes."
msgid "Bug Fixes"
msgstr "Bug Fixes"
msgid ""
"Ceilometer Datasource has been deprecated since its API has been deprecated "
"in Ocata cycle. Watcher has supported Ceilometer for some releases after "
"Ocata to let users migrate to Gnocchi/Monasca datasources. Since Train "
"release, Ceilometer support will be removed."
msgstr ""
"Ceilometer Datasource has been deprecated since its API has been deprecated "
"in Ocata cycle. Watcher has supported Ceilometer for some releases after "
"Ocata to let users migrate to Gnocchi/Monasca datasources. Since Train "
"release, Ceilometer support will be removed."
msgid "Centralize all configuration options for Watcher."
msgstr "Centralise all configuration options for Watcher."
@@ -569,52 +360,6 @@ msgstr ""
"Now instances from particular project in OpenStack can be excluded from "
"audit defining scope in audit templates."
msgid ""
"For a large cloud infrastructure, retrieving data from Nova may take a long "
"time. To avoid getting too much data from Nova, building the compute data "
"model according to the scope of audit."
msgstr ""
"For a large cloud infrastructure, retrieving data from Nova may take a long "
"time. To avoid getting too much data from Nova, building the compute data "
"model according to the scope of audit."
msgid ""
"Grafana has been added as datasource that can be used for collecting "
"metrics. The configuration options allow to specify what metrics and how "
"they are stored in grafana so that no matter how Grafana is configured it "
"can still be used. The configuration can be done via the typical "
"configuration file but it is recommended to configure most options in the "
"yaml file for metrics. For a complete walkthrough on configuring Grafana "
"see: https://docs.openstack.org/watcher/latest/datasources/grafana.html"
msgstr ""
"Grafana has been added as datasource that can be used for collecting "
"metrics. The configuration options allow to specify what metrics and how "
"they are stored in Grafana so that no matter how Grafana is configured it "
"can still be used. The configuration can be done via the typical "
"configuration file but it is recommended to configure most options in the "
"yaml file for metrics. For a complete walkthrough on configuring Grafana "
"see: https://docs.openstack.org/watcher/latest/datasources/grafana.html"
msgid ""
"If Gnocchi was configured to have a custom amount of retries and or a custom "
"timeout then the configuration needs to moved into the "
"`[watcher_datasources]` group instead of the `[gnocchi_client]` group."
msgstr ""
"If Gnocchi was configured to have a custom amount of retries and or a custom "
"timeout then the configuration needs to moved into the "
"`[watcher_datasources]` group instead of the `[gnocchi_client]` group."
msgid ""
"Improved interface for datasource baseclass that better defines expected "
"values and types for parameters and return types of all abstract methods. "
"This allows all strategies to work with every datasource provided the "
"metrics are configured for that given datasource."
msgstr ""
"Improved interface for datasource baseclass that better defines expected "
"values and types for parameters and return types of all abstract methods. "
"This allows all strategies to work with every datasource provided the "
"metrics are configured for that given datasource."
msgid ""
"Instance cold migration logic is now replaced with using Nova migrate "
"Server(migrate Action) API which has host option since v2.56."
@@ -622,17 +367,6 @@ msgstr ""
"Instance cold migration logic is now replaced with using Nova migrate "
"Server(migrate Action) API which has host option since v2.56."
msgid ""
"Many operations in the decision engine will block on I/O. Such I/O "
"operations can stall the execution of a sequential application "
"significantly. To reduce the potential bottleneck of many operations the "
"general purpose decision engine threadpool is introduced."
msgstr ""
"Many operations in the decision engine will block on I/O. Such I/O "
"operations can stall the execution of a sequential application "
"significantly. To reduce the potential bottleneck of many operations the "
"general purpose decision engine threadpool is introduced."
msgid "New Features"
msgstr "New Features"
@@ -655,13 +389,6 @@ msgstr ""
"Nova API version is now set to 2.56 by default. This needs the migrate "
"action of migration type cold with destination_node parameter to work."
msgid ""
"Now Watcher strategy can select specific planner beyond default. Strategy "
"can set planner property to specify its own planner."
msgstr ""
"Now Watcher strategy can select specific planner beyond default. Strategy "
"can set planner property to specify its own planner."
msgid "Ocata Series Release Notes"
msgstr "Ocata Series Release Notes"
@@ -702,77 +429,12 @@ msgstr ""
"resources will be called \"Audit scope\" and will be defined in each audit "
"template (which contains the audit settings)."
msgid ""
"Python 2.7 support has been dropped. Last release of Watcher to support "
"py2.7 is OpenStack Train. The minimum version of Python now supported by "
"Watcher is Python 3.6."
msgstr ""
"Python 2.7 support has been dropped. Last release of Watcher to support "
"py2.7 is OpenStack Train. The minimum version of Python now supported by "
"Watcher is Python 3.6."
msgid "Queens Series Release Notes"
msgstr "Queens Series Release Notes"
msgid "Rocky Series Release Notes"
msgstr "Rocky Series Release Notes"
msgid ""
"Several strategies have changed the `node` parameter to `compute_node` to be "
"better aligned with terminology. These strategies include "
"`basic_consolidation` and `workload_stabilzation`. The `node` parameter will "
"remain supported during Train release and will be removed in the subsequent "
"release."
msgstr ""
"Several strategies have changed the `node` parameter to `compute_node` to be "
"better aligned with terminology. These strategies include "
"`basic_consolidation` and `workload_stabilzation`. The `node` parameter will "
"remain supported during Train release and will be removed in the subsequent "
"release."
msgid ""
"Specific strategies can override this order and use datasources which are "
"not listed in the global preference."
msgstr ""
"Specific strategies can override this order and use datasources which are "
"not listed in the global preference."
msgid "Stein Series Release Notes"
msgstr "Stein Series Release Notes"
msgid ""
"The building of the compute (Nova) data model will be done using the "
"decision engine threadpool, thereby, significantly reducing the total time "
"required to build it."
msgstr ""
"The building of the compute (Nova) data model will be done using the "
"decision engine threadpool, thereby, significantly reducing the total time "
"required to build it."
msgid ""
"The configuration options for query retries in `[gnocchi_client]` are "
"deprecated and the option in `[watcher_datasources]` should now be used."
msgstr ""
"The configuration options for query retries in `[gnocchi_client]` are "
"deprecated and the option in `[watcher_datasources]` should now be used."
msgid ""
"The default value of ``[oslo_policy] policy_file`` config option has been "
"changed from ``policy.json`` to ``policy.yaml``. Operators who are utilizing "
"customized or previously generated static policy JSON files (which are not "
"needed by default), should generate new policy files or convert them in YAML "
"format. Use the `oslopolicy-convert-json-to-yaml <https://docs.openstack.org/"
"oslo.policy/latest/cli/oslopolicy-convert-json-to-yaml.html>`_ tool to "
"convert a JSON to YAML formatted policy file in backward compatible way."
msgstr ""
"The default value of ``[oslo_policy] policy_file`` config option has been "
"changed from ``policy.json`` to ``policy.yaml``. Operators who are utilizing "
"customized or previously generated static policy JSON files (which are not "
"needed by default), should generate new policy files or convert them in YAML "
"format. Use the `oslopolicy-convert-json-to-yaml <https://docs.openstack.org/"
"oslo.policy/latest/cli/oslopolicy-convert-json-to-yaml.html>`_ tool to "
"convert a JSON to YAML formatted policy file in backward compatible way."
msgid ""
"The graph model describes how VMs are associated to compute hosts. This "
"allows for seeing relationships upfront between the entities and hence can "
@@ -793,22 +455,6 @@ msgstr ""
"was fixed. Before fixing, it booted an instance in the service project as a "
"migrated instance."
msgid ""
"The minimum required version of the ``[nova_client]/api_version`` value is "
"now enforced to be ``2.56`` which is available since the Queens version of "
"the nova compute service."
msgstr ""
"The minimum required version of the ``[nova_client]/api_version`` value is "
"now enforced to be ``2.56`` which is available since the Queens version of "
"the Nova compute service."
msgid ""
"The new strategy baseclass has significant changes in method parameters and "
"any out-of-tree strategies will have to be adopted."
msgstr ""
"The new strategy baseclass has significant changes in method parameters and "
"any out-of-tree strategies will have to be adopted."
msgid ""
"There is new ability to create Watcher continuous audits with cron interval. "
"It means you may use, for example, optional argument '--interval \"\\*/5 \\* "
@@ -822,45 +468,9 @@ msgstr ""
"best effort basis and therefore, we recommend you to use a minimal cron "
"interval of at least one minute."
msgid "Train Series Release Notes"
msgstr "Train Series Release Notes"
msgid "Upgrade Notes"
msgstr "Upgrade Notes"
msgid ""
"Use of JSON policy files was deprecated by the ``oslo.policy`` library "
"during the Victoria development cycle. As a result, this deprecation is "
"being noted in the Wallaby cycle with an anticipated future removal of "
"support by ``oslo.policy``. As such operators will need to convert to YAML "
"policy files. Please see the upgrade notes for details on migration of any "
"custom policy files."
msgstr ""
"Use of JSON policy files was deprecated by the ``oslo.policy`` library "
"during the Victoria development cycle. As a result, this deprecation is "
"being noted in the Wallaby cycle with an anticipated future removal of "
"support by ``oslo.policy``. As such operators will need to convert to YAML "
"policy files. Please see the upgrade notes for details on migration of any "
"custom policy files."
msgid ""
"Using ``watcher/api/app.wsgi`` script is deprecated and it will be removed "
"in U release. Please switch to automatically generated ``watcher-api-wsgi`` "
"script instead."
msgstr ""
"Using ``watcher/api/app.wsgi`` script is deprecated and it will be removed "
"in U release. Please switch to automatically generated ``watcher-api-wsgi`` "
"script instead."
msgid "Ussuri Series Release Notes"
msgstr "Ussuri Series Release Notes"
msgid "Victoria Series Release Notes"
msgstr "Victoria Series Release Notes"
msgid "Wallaby Series Release Notes"
msgstr "Wallaby Series Release Notes"
msgid ""
"Watcher can continuously optimize the OpenStack cloud for a specific "
"strategy or goal by triggering an audit periodically which generates an "
@@ -870,15 +480,6 @@ msgstr ""
"strategy or goal by triggering an audit periodically which generates an "
"action plan and run it automatically."
msgid ""
"Watcher can get resource information such as total, allocation ratio and "
"reserved information from Placement API. Now we add some new fields to the "
"Watcher Data Model:"
msgstr ""
"Watcher can get resource information such as total, allocation ratio and "
"reserved information from Placement API. Now we add some new fields to the "
"Watcher Data Model:"
msgid ""
"Watcher can now run specific actions in parallel improving the performances "
"dramatically when executing an action plan."
@@ -916,15 +517,6 @@ msgstr ""
"includes all instances. It filters excluded instances when migration during "
"the audit."
msgid ""
"Watcher now supports configuring which datasource to use and in which order. "
"This configuration is done by specifying datasources in the "
"watcher_datasources section:"
msgstr ""
"Watcher now supports configuring which datasource to use and in which order. "
"This configuration is done by specifying datasources in the "
"watcher_datasources section:"
msgid ""
"Watcher removes the support to Nova legacy notifications because of Nova "
"will deprecate them."
@@ -965,24 +557,9 @@ msgstr ""
"Watcher supports multiple metrics backend and relies on Ceilometer and "
"Monasca."
msgid "We also add some new propeties:"
msgstr "We also add some new properties:"
msgid "Welcome to watcher's Release Notes documentation!"
msgstr "Welcome to watcher's Release Notes documentation!"
msgid "Xena Series Release Notes"
msgstr "Xena Series Release Notes"
msgid "Yoga Series Release Notes"
msgstr "Yoga Series Release Notes"
msgid "Zed Series Release Notes"
msgstr "Zed Series Release Notes"
msgid "``[watcher_datasources] datasources = gnocchi,monasca,ceilometer``"
msgstr "``[watcher_datasources] datasources = gnocchi,monasca,ceilometer``"
msgid ""
"all Watcher objects have been refactored to support OVO (oslo."
"versionedobjects) which was a prerequisite step in order to implement "
@@ -992,21 +569,6 @@ msgstr ""
"versionedobjects) which was a prerequisite step in order to implement "
"versioned notifications."
msgid ""
"disk_gb_capacity: The amount of disk, take allocation ratio into account, "
"but do not include reserved."
msgstr ""
"disk_gb_capacity: The amount of disk, take allocation ratio into account, "
"but do not include reserved."
msgid ""
"disk_gb_reserved: The amount of disk a node has reserved for its own use."
msgstr ""
"disk_gb_reserved: The amount of disk a node has reserved for its own use."
msgid "disk_ratio: Disk allocation ratio."
msgstr "disk_ratio: Disk allocation ratio."
msgid "instance.create.end"
msgstr "instance.create.end"
@@ -1073,21 +635,6 @@ msgstr "instance.unshelve.end"
msgid "instance.update"
msgstr "instance.update"
msgid ""
"memory_mb_capacity: The amount of memory, take allocation ratio into "
"account, but do not include reserved."
msgstr ""
"memory_mb_capacity: The amount of memory, take allocation ratio into "
"account, but do not include reserved."
msgid ""
"memory_mb_reserved: The amount of memory a node has reserved for its own use."
msgstr ""
"memory_mb_reserved: The amount of memory a node has reserved for its own use."
msgid "memory_ratio: Memory allocation ratio."
msgstr "memory_ratio: Memory allocation ratio."
msgid "new:"
msgstr "new:"
@@ -1102,16 +649,3 @@ msgstr "service.delete"
msgid "service.update"
msgstr "service.update"
msgid ""
"vcpu_capacity: The amount of vcpu, take allocation ratio into account, but "
"do not include reserved."
msgstr ""
"vcpu_capacity: The amount of vcpu, take allocation ratio into account, but "
"do not include reserved."
msgid "vcpu_ratio: CPU allocation ratio."
msgstr "vcpu_ratio: CPU allocation ratio."
msgid "vcpu_reserved: The amount of cpu a node has reserved for its own use."
msgstr "vcpu_reserved: The amount of CPU a node has reserved for its own use."

View File

@@ -1,6 +0,0 @@
=============================
Victoria Series Release Notes
=============================
.. release-notes::
:branch: stable/victoria

View File

@@ -1,6 +0,0 @@
============================
Wallaby Series Release Notes
============================
.. release-notes::
:branch: stable/wallaby

View File

@@ -1,6 +0,0 @@
=========================
Xena Series Release Notes
=========================
.. release-notes::
:branch: stable/xena

View File

@@ -1,6 +0,0 @@
=========================
Yoga Series Release Notes
=========================
.. release-notes::
:branch: stable/yoga

View File

@@ -1,6 +0,0 @@
========================
Zed Series Release Notes
========================
.. release-notes::
:branch: stable/zed

View File

@@ -1,4 +1,4 @@
# The order of packages is significant, because pip processes them in the order
# The order of packages is significant, because pip processes them in the order
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
@@ -12,24 +12,25 @@ croniter>=0.3.20 # MIT License
os-resource-classes>=0.4.0
oslo.concurrency>=3.26.0 # Apache-2.0
oslo.cache>=1.29.0 # Apache-2.0
oslo.config>=6.8.0 # Apache-2.0
oslo.config>=5.2.0 # Apache-2.0
oslo.context>=2.21.0 # Apache-2.0
oslo.db>=4.44.0 # Apache-2.0
oslo.i18n>=3.20.0 # Apache-2.0
oslo.log>=3.37.0 # Apache-2.0
oslo.messaging>=14.1.0 # Apache-2.0
oslo.policy>=3.6.0 # Apache-2.0
oslo.messaging>=8.1.2 # Apache-2.0
oslo.policy>=1.34.0 # Apache-2.0
oslo.reports>=1.27.0 # Apache-2.0
oslo.serialization>=2.25.0 # Apache-2.0
oslo.service>=1.30.0 # Apache-2.0
oslo.upgradecheck>=1.3.0 # Apache-2.0
oslo.upgradecheck>=0.1.0 # Apache-2.0
oslo.utils>=3.36.0 # Apache-2.0
oslo.versionedobjects>=1.32.0 # Apache-2.0
PasteDeploy>=1.5.2 # MIT
pbr>=3.1.1 # Apache-2.0
pecan>=1.3.2 # BSD
PrettyTable>=0.7.2 # BSD
PrettyTable<0.8,>=0.7.2 # BSD
gnocchiclient>=7.0.1 # Apache-2.0
python-ceilometerclient>=2.9.0 # Apache-2.0
python-cinderclient>=3.5.0 # Apache-2.0
python-glanceclient>=2.9.1 # Apache-2.0
python-keystoneclient>=3.15.0 # Apache-2.0

View File

@@ -1,12 +1,12 @@
[metadata]
name = python-watcher
summary = OpenStack Watcher provides a flexible and scalable resource optimization service for multi-tenant OpenStack-based clouds.
description_file =
description-file =
README.rst
author = OpenStack
author_email = openstack-discuss@lists.openstack.org
home_page = https://docs.openstack.org/watcher/latest/
python_requires = >=3.8
author-email = openstack-discuss@lists.openstack.org
home-page = https://docs.openstack.org/watcher/latest/
python-requires = >=3.6
classifier =
Environment :: OpenStack
Intended Audience :: Information Technology
@@ -17,10 +17,9 @@ classifier =
Programming Language :: Python :: Implementation :: CPython
Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
[files]
packages =

View File

@@ -7,9 +7,9 @@ doc8>=0.8.0 # Apache-2.0
freezegun>=0.3.10 # Apache-2.0
hacking>=3.0.1,<3.1.0 # Apache-2.0
oslotest>=3.3.0 # Apache-2.0
os-testr>=1.0.0 # Apache-2.0
testscenarios>=0.5.0 # Apache-2.0/BSD
testtools>=2.3.0 # MIT
stestr>=2.0.0 # Apache-2.0
os-api-ref>=1.4.0 # Apache-2.0
bandit>=1.6.0 # Apache-2.0
WebTest>=2.0.27 # MIT

78
tox.ini
View File

@@ -1,41 +1,37 @@
[tox]
minversion = 3.18.0
envlist = py3,pep8
minversion = 2.0
envlist = py36,py37,pep8
skipsdist = True
ignore_basepython_conflict = True
[testenv]
basepython = python3
usedevelop = True
allowlist_externals = find
whitelist_externals = find
rm
install_command = pip install -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/2024.2} {opts} {packages}
install_command = pip install {opts} {packages}
setenv =
VIRTUAL_ENV={envdir}
deps =
-c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/victoria}
-r{toxinidir}/test-requirements.txt
-r{toxinidir}/requirements.txt
python-libmaas>=0.6.8
commands =
rm -f .testrepository/times.dbm
find . -type f -name "*.py[c|o]" -delete
stestr run {posargs}
passenv =
http_proxy
HTTP_PROXY
https_proxy
HTTPS_PROXY
no_proxy
NO_PROXY
passenv = http_proxy HTTP_PROXY https_proxy HTTPS_PROXY no_proxy NO_PROXY
[testenv:pep8]
commands =
doc8 doc/source/ CONTRIBUTING.rst HACKING.rst README.rst
flake8
#bandit -r watcher -x watcher/tests/* -n5 -ll -s B320
bandit -r watcher -x watcher/tests/* -n5 -ll -s B320,B322
[testenv:venv]
setenv = PYTHONHASHSEED=0
deps =
-c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/victoria}
-r{toxinidir}/doc/requirements.txt
-r{toxinidir}/test-requirements.txt
-r{toxinidir}/requirements.txt
@@ -53,15 +49,14 @@ commands =
[testenv:docs]
setenv = PYTHONHASHSEED=0
deps =
-r{toxinidir}/doc/requirements.txt
deps = -r{toxinidir}/doc/requirements.txt
commands =
rm -fr doc/build doc/source/api/ .autogenerated
sphinx-build -W --keep-going -b html doc/source doc/build/html
[testenv:api-ref]
deps = -r{toxinidir}/doc/requirements.txt
allowlist_externals = bash
whitelist_externals = bash
commands =
bash -c 'rm -rf api-ref/build'
sphinx-build -W --keep-going -b html -d api-ref/build/doctrees api-ref/source api-ref/build/html
@@ -78,28 +73,6 @@ commands =
commands =
oslopolicy-sample-generator --config-file etc/watcher/oslo-policy-generator/watcher-policy-generator.conf
[testenv:wheel]
commands = python setup.py bdist_wheel
[testenv:pdf-docs]
envdir = {toxworkdir}/docs
deps = {[testenv:docs]deps}
allowlist_externals =
rm
make
commands =
rm -rf doc/build/pdf
sphinx-build -W --keep-going -b latex doc/source doc/build/pdf
make -C doc/build/pdf
[testenv:releasenotes]
deps = -r{toxinidir}/doc/requirements.txt
commands = sphinx-build -a -W -E -d releasenotes/build/doctrees --keep-going -b html releasenotes/source releasenotes/build/html
[testenv:bandit]
deps = -r{toxinidir}/test-requirements.txt
commands = bandit -r watcher -x watcher/tests/* -n5 -ll -s B320
[flake8]
filename = *.py,app.wsgi
show-source=True
@@ -109,6 +82,9 @@ builtins= _
enable-extensions = H106,H203,H904
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,*sqlalchemy/alembic/versions/*,demo/,releasenotes
[testenv:wheel]
commands = python setup.py bdist_wheel
[hacking]
import_exceptions = watcher._i18n
@@ -132,7 +108,33 @@ extension =
N366 = checks:import_stock_mock
paths = ./watcher/hacking
[doc8]
extension=.rst
# todo: stop ignoring doc/source/man when https://bugs.launchpad.net/doc8/+bug/1502391 is fixed
ignore-path=doc/source/image_src,doc/source/man,doc/source/api
[testenv:pdf-docs]
envdir = {toxworkdir}/docs
deps = {[testenv:docs]deps}
whitelist_externals =
rm
make
commands =
rm -rf doc/build/pdf
sphinx-build -W --keep-going -b latex doc/source doc/build/pdf
make -C doc/build/pdf
[testenv:releasenotes]
deps = -r{toxinidir}/doc/requirements.txt
commands = sphinx-build -a -W -E -d releasenotes/build/doctrees --keep-going -b html releasenotes/source releasenotes/build/html
[testenv:bandit]
deps = -r{toxinidir}/test-requirements.txt
commands = bandit -r watcher -x watcher/tests/* -n5 -ll -s B320
[testenv:lower-constraints]
deps =
-c{toxinidir}/lower-constraints.txt
-r{toxinidir}/test-requirements.txt
-r{toxinidir}/requirements.txt

View File

@@ -57,7 +57,6 @@ are dynamically loaded by Watcher at launch time.
import datetime
from http import HTTPStatus
import pecan
from pecan import rest
import wsme
@@ -363,7 +362,7 @@ class ActionsController(rest.RestController):
return Action.convert_with_links(action)
@wsme_pecan.wsexpose(Action, body=Action, status_code=HTTPStatus.CREATED)
@wsme_pecan.wsexpose(Action, body=Action, status_code=201)
def post(self, action):
"""Create a new action(forbidden).
@@ -423,7 +422,7 @@ class ActionsController(rest.RestController):
action_to_update.save()
return Action.convert_with_links(action_to_update)
@wsme_pecan.wsexpose(None, types.uuid, status_code=HTTPStatus.NO_CONTENT)
@wsme_pecan.wsexpose(None, types.uuid, status_code=204)
def delete(self, action_uuid):
"""Delete a action(forbidden).

View File

@@ -56,7 +56,6 @@ state machine <action_plan_state_machine>`.
import datetime
from http import HTTPStatus
from oslo_log import log
import pecan
from pecan import rest
@@ -461,7 +460,7 @@ class ActionPlansController(rest.RestController):
return ActionPlan.convert_with_links(action_plan)
@wsme_pecan.wsexpose(None, types.uuid, status_code=HTTPStatus.NO_CONTENT)
@wsme_pecan.wsexpose(None, types.uuid, status_code=204)
def delete(self, action_plan_uuid):
"""Delete an action plan.

View File

@@ -32,7 +32,6 @@ states, visit :ref:`the Audit State machine <audit_state_machine>`.
import datetime
from dateutil import tz
from http import HTTPStatus
import pecan
from pecan import rest
import wsme
@@ -596,8 +595,7 @@ 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=201)
def post(self, audit_p):
"""Create a new audit.
@@ -719,7 +717,7 @@ class AuditsController(rest.RestController):
audit_to_update.save()
return Audit.convert_with_links(audit_to_update)
@wsme_pecan.wsexpose(None, wtypes.text, status_code=HTTPStatus.NO_CONTENT)
@wsme_pecan.wsexpose(None, wtypes.text, status_code=204)
def delete(self, audit):
"""Delete an audit.

View File

@@ -45,7 +45,6 @@ will be launched automatically or will need a manual confirmation from the
import datetime
from http import HTTPStatus
import pecan
from pecan import rest
import wsme
@@ -619,7 +618,7 @@ class AuditTemplatesController(rest.RestController):
@wsme.validate(types.uuid, AuditTemplatePostType)
@wsme_pecan.wsexpose(AuditTemplate, body=AuditTemplatePostType,
status_code=HTTPStatus.CREATED)
status_code=201)
def post(self, audit_template_postdata):
"""Create a new audit template.
@@ -695,7 +694,7 @@ class AuditTemplatesController(rest.RestController):
audit_template_to_update.save()
return AuditTemplate.convert_with_links(audit_template_to_update)
@wsme_pecan.wsexpose(None, wtypes.text, status_code=HTTPStatus.NO_CONTENT)
@wsme_pecan.wsexpose(None, wtypes.text, status_code=204)
def delete(self, audit_template):
"""Delete a audit template.

View File

@@ -19,6 +19,8 @@ Service mechanism provides ability to monitor Watcher services state.
"""
import datetime
import six
from oslo_config import cfg
from oslo_log import log
from oslo_utils import timeutils
@@ -68,7 +70,7 @@ class Service(base.APIBase):
service = objects.Service.get(pecan.request.context, id)
last_heartbeat = (service.last_seen_up or service.updated_at or
service.created_at)
if isinstance(last_heartbeat, str):
if isinstance(last_heartbeat, six.string_types):
# NOTE(russellb) If this service came in over rpc via
# conductor, then the timestamp will be a string and needs to be
# converted back to a datetime.

View File

@@ -15,6 +15,7 @@
from oslo_serialization import jsonutils
from oslo_utils import strutils
import six
import wsme
from wsme import types as wtypes
@@ -131,7 +132,7 @@ class JsonType(wtypes.UserType):
def __str__(self):
# These are the json serializable native types
return ' | '.join(map(str, (wtypes.text, int, float,
return ' | '.join(map(str, (wtypes.text, six.integer_types, float,
BooleanType, list, dict, None)))
@staticmethod

View File

@@ -14,7 +14,6 @@
Webhook endpoint for Watcher v1 REST API.
"""
from http import HTTPStatus
from oslo_log import log
import pecan
from pecan import rest
@@ -37,7 +36,7 @@ class WebhookController(rest.RestController):
self.dc_client = rpcapi.DecisionEngineAPI()
@wsme_pecan.wsexpose(None, wtypes.text, body=types.jsontype,
status_code=HTTPStatus.ACCEPTED)
status_code=202)
def post(self, audit_ident, body):
"""Trigger the given audit.

View File

@@ -15,7 +15,7 @@
# under the License.
from http import HTTPStatus
from http import client as http_client
from oslo_config import cfg
from pecan import hooks
@@ -91,8 +91,8 @@ 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 (http_client.OK <= state.response.status_int <
http_client.BAD_REQUEST):
return
json_body = state.response.json

View File

@@ -17,17 +17,17 @@
# limitations under the License.
#
import enum
import time
from oslo_log import log
from watcher._i18n import _
from watcher.applier.actions import base
from watcher.common import exception
from watcher.common.metal_helper import constants as metal_constants
from watcher.common.metal_helper import factory as metal_helper_factory
LOG = log.getLogger(__name__)
class NodeState(enum.Enum):
POWERON = 'on'
POWEROFF = 'off'
class ChangeNodePowerState(base.BaseAction):
@@ -43,8 +43,8 @@ class ChangeNodePowerState(base.BaseAction):
'state': str,
})
The `resource_id` references a baremetal node id (list of available
ironic nodes is returned by this command: ``ironic node-list``).
The `resource_id` references a ironic node id (list of available
ironic node is returned by this command: ``ironic node-list``).
The `state` value should either be `on` or `off`.
"""
@@ -59,14 +59,10 @@ class ChangeNodePowerState(base.BaseAction):
'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': [NodeState.POWERON.value,
NodeState.POWEROFF.value]
}
},
'required': ['resource_id', 'state'],
@@ -86,10 +82,10 @@ class ChangeNodePowerState(base.BaseAction):
return self._node_manage_power(target_state)
def revert(self):
if self.state == metal_constants.PowerState.ON.value:
target_state = metal_constants.PowerState.OFF.value
elif self.state == metal_constants.PowerState.OFF.value:
target_state = metal_constants.PowerState.ON.value
if self.state == NodeState.POWERON.value:
target_state = NodeState.POWEROFF.value
elif self.state == NodeState.POWEROFF.value:
target_state = NodeState.POWERON.value
return self._node_manage_power(target_state)
def _node_manage_power(self, state, retry=60):
@@ -97,32 +93,30 @@ class ChangeNodePowerState(base.BaseAction):
raise exception.IllegalArgumentException(
message=_("The target state is not defined"))
metal_helper = metal_helper_factory.get_helper(self.osc)
node = metal_helper.get_node(self.node_uuid)
current_state = node.get_power_state()
if state == current_state.value:
ironic_client = self.osc.ironic()
nova_client = self.osc.nova()
current_state = ironic_client.node.get(self.node_uuid).power_state
# power state: 'power on' or 'power off', if current node state
# is the same as state, just return True
if state in current_state:
return True
if state == metal_constants.PowerState.OFF.value:
compute_node = node.get_hypervisor_node().to_dict()
if state == NodeState.POWEROFF.value:
node_info = ironic_client.node.get(self.node_uuid).to_dict()
compute_node_id = node_info['extra']['compute_node_id']
compute_node = nova_client.hypervisors.get(compute_node_id)
compute_node = compute_node.to_dict()
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'])
return False
ironic_client.node.set_power_state(
self.node_uuid, state)
else:
node.set_power_state(state)
ironic_client.node.set_power_state(self.node_uuid, state)
node = metal_helper.get_node(self.node_uuid)
while node.get_power_state() == current_state and retry:
ironic_node = ironic_client.node.get(self.node_uuid)
while ironic_node.power_state == current_state and retry:
time.sleep(10)
retry -= 1
node = metal_helper.get_node(self.node_uuid)
ironic_node = ironic_client.node.get(self.node_uuid)
if retry > 0:
return True
else:
@@ -136,4 +130,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.")

View File

@@ -17,11 +17,14 @@ import jsonschema
from oslo_log import log
from cinderclient import client as cinder_client
from watcher._i18n import _
from watcher.applier.actions import base
from watcher.common import cinder_helper
from watcher.common import exception
from watcher.common import keystone_helper
from watcher.common import nova_helper
from watcher.common import utils
from watcher import conf
CONF = conf.CONF
@@ -67,6 +70,8 @@ class VolumeMigrate(base.BaseAction):
def __init__(self, config, osc=None):
super(VolumeMigrate, self).__init__(config)
self.temp_username = utils.random_string(10)
self.temp_password = utils.random_string(10)
self.cinder_util = cinder_helper.CinderHelper(osc=self.osc)
self.nova_util = nova_helper.NovaHelper(osc=self.osc)
@@ -129,42 +134,83 @@ class VolumeMigrate(base.BaseAction):
def _can_swap(self, volume):
"""Judge volume can be swapped"""
# TODO(sean-k-mooney): rename this to _can_migrate and update
# tests to reflect that.
# cinder volume migration can migrate volumes that are not
# attached to instances or nova can migrate the data for cinder
# if the volume is in-use. If the volume has no attachments
# allow cinder to decided if it can be migrated.
if not volume.attachments:
LOG.debug(f"volume: {volume.id} has no attachments")
return True
# since it has attachments we need to validate nova's constraints
return False
instance_id = volume.attachments[0]['server_id']
instance_status = self.nova_util.find_instance(instance_id).status
LOG.debug(
f"volume: {volume.id} is attached to instance: {instance_id} "
f"in instance status: {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'))
if (volume.status == 'in-use' and
instance_status in ('ACTIVE', 'PAUSED', 'RESIZED')):
return True
return False
def _create_user(self, volume, user):
"""Create user with volume attribute and user information"""
keystone_util = keystone_helper.KeystoneHelper(osc=self.osc)
project_id = getattr(volume, 'os-vol-tenant-attr:tenant_id')
user['project'] = project_id
user['domain'] = keystone_util.get_project(project_id).domain_id
user['roles'] = ['admin']
return keystone_util.create_user(user)
def _get_cinder_client(self, session):
"""Get cinder client by session"""
return cinder_client.Client(
CONF.cinder_client.api_version,
session=session,
endpoint_type=CONF.cinder_client.endpoint_type)
def _swap_volume(self, volume, dest_type):
"""Swap volume to dest_type
Limitation note: only for compute libvirt driver
"""
if not dest_type:
raise exception.Invalid(
message=(_("destination type is required when "
"migration type is swap")))
if not self._can_swap(volume):
raise exception.Invalid(
message=(_("Invalid state for swapping volume")))
user_info = {
'name': self.temp_username,
'password': self.temp_password}
user = self._create_user(volume, user_info)
keystone_util = keystone_helper.KeystoneHelper(osc=self.osc)
try:
session = keystone_util.create_session(
user.id, self.temp_password)
temp_cinder = self._get_cinder_client(session)
# swap volume
new_volume = self.cinder_util.create_volume(
temp_cinder, volume, dest_type)
self.nova_util.swap_volume(volume, new_volume)
# delete old volume
self.cinder_util.delete_volume(volume)
finally:
keystone_util.delete_user(user)
return True
def _migrate(self, volume_id, dest_node, dest_type):
try:
volume = self.cinder_util.get_volume(volume_id)
# for backward compatibility map swap to migrate.
if self.migration_type in (self.SWAP, self.MIGRATE):
if not self._can_swap(volume):
raise exception.Invalid(
message=(_("Invalid state for swapping volume")))
return self.cinder_util.migrate(volume, dest_node)
if self.migration_type == self.SWAP:
if dest_node:
LOG.warning("dest_node is ignored")
return self._swap_volume(volume, dest_type)
elif self.migration_type == self.RETYPE:
return self.cinder_util.retype(volume, dest_type)
elif self.migration_type == self.MIGRATE:
return self.cinder_util.migrate(volume, dest_node)
else:
raise exception.Invalid(
message=(_("Migration of type '%(migration_type)s' is not "

View File

@@ -14,7 +14,6 @@
import sys
from oslo_upgradecheck import common_checks
from oslo_upgradecheck import upgradecheck
from watcher._i18n import _
@@ -44,10 +43,6 @@ class Checks(upgradecheck.UpgradeCommands):
_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})),
)

View File

@@ -17,7 +17,7 @@ import time
from oslo_log import log
from cinderclient import exceptions as cinder_exception
from cinderclient.v3.volumes import Volume
from cinderclient.v2.volumes import Volume
from watcher._i18n import _
from watcher.common import clients
from watcher.common import exception

View File

@@ -25,7 +25,6 @@ from novaclient import api_versions as nova_api_versions
from novaclient import client as nvclient
from watcher.common import exception
from watcher.common import utils
try:
from ceilometerclient import client as ceclient
@@ -33,12 +32,6 @@ try:
except ImportError:
HAS_CEILCLIENT = False
try:
from maas import client as maas_client
except ImportError:
maas_client = None
CONF = cfg.CONF
_CLIENTS_AUTH_GROUP = 'watcher_clients_auth'
@@ -81,7 +74,6 @@ class OpenStackClients(object):
self._monasca = None
self._neutron = None
self._ironic = None
self._maas = None
self._placement = None
def _get_keystone_session(self):
@@ -273,23 +265,6 @@ class OpenStackClients(object):
session=self.session)
return self._ironic
def maas(self):
if self._maas:
return self._maas
if not maas_client:
raise exception.UnsupportedError(
"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)
return self._maas
@exception.wrap_keystone_exception
def placement(self):
if self._placement:

View File

@@ -11,15 +11,12 @@
# under the License.
from oslo_context import context
from oslo_db.sqlalchemy import enginefacade
from oslo_log import log
from oslo_utils import timeutils
LOG = log.getLogger(__name__)
@enginefacade.transaction_context_provider
class RequestContext(context.RequestContext):
"""Extends security contexts from the OpenStack common library."""

View File

@@ -25,7 +25,6 @@ SHOULD include dedicated exception logging.
import functools
import sys
from http import HTTPStatus
from keystoneclient import exceptions as keystone_exceptions
from oslo_config import cfg
from oslo_log import log
@@ -63,7 +62,7 @@ class WatcherException(Exception):
"""
msg_fmt = _("An unknown exception occurred")
code = HTTPStatus.INTERNAL_SERVER_ERROR
code = 500
headers = {}
safe = False
@@ -115,12 +114,12 @@ class UnsupportedError(WatcherException):
class NotAuthorized(WatcherException):
msg_fmt = _("Not authorized")
code = HTTPStatus.FORBIDDEN
code = 403
class NotAcceptable(WatcherException):
msg_fmt = _("Request not acceptable.")
code = HTTPStatus.NOT_ACCEPTABLE
code = 406
class PolicyNotAuthorized(NotAuthorized):
@@ -133,7 +132,7 @@ class OperationNotPermitted(NotAuthorized):
class Invalid(WatcherException, ValueError):
msg_fmt = _("Unacceptable parameters")
code = HTTPStatus.BAD_REQUEST
code = 400
class ObjectNotFound(WatcherException):
@@ -142,12 +141,12 @@ class ObjectNotFound(WatcherException):
class Conflict(WatcherException):
msg_fmt = _('Conflict')
code = HTTPStatus.CONFLICT
code = 409
class ResourceNotFound(ObjectNotFound):
msg_fmt = _("The %(name)s resource %(id)s could not be found")
code = HTTPStatus.NOT_FOUND
code = 404
class InvalidParameter(Invalid):

View File

@@ -15,6 +15,8 @@
from oslo_log import log
from keystoneauth1.exceptions import http as ks_exceptions
from keystoneauth1 import loading
from keystoneauth1 import session
from watcher._i18n import _
from watcher.common import clients
from watcher.common import exception
@@ -88,3 +90,35 @@ class KeystoneHelper(object):
message=(_("Domain name seems ambiguous: %s") %
name_or_id))
return domains[0]
def create_session(self, user_id, password):
user = self.get_user(user_id)
loader = loading.get_plugin_loader('password')
auth = loader.load_from_options(
auth_url=CONF.watcher_clients_auth.auth_url,
password=password,
user_id=user_id,
project_id=user.default_project_id)
return session.Session(auth=auth)
def create_user(self, user):
project = self.get_project(user['project'])
domain = self.get_domain(user['domain'])
_user = self.keystone.users.create(
user['name'],
password=user['password'],
domain=domain,
project=project,
)
for role in user['roles']:
role = self.get_role(role)
self.keystone.roles.grant(
role.id, user=_user.id, project=project.id)
return _user
def delete_user(self, user):
try:
user = self.get_user(user)
self.keystone.users.delete(user)
except exception.Invalid:
pass

View File

@@ -1,81 +0,0 @@
# Copyright 2023 Cloudbase Solutions
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import abc
from watcher.common import exception
from watcher.common.metal_helper import constants as metal_constants
class BaseMetalNode(abc.ABC):
hv_up_when_powered_off = False
def __init__(self, nova_node=None):
self._nova_node = nova_node
def get_hypervisor_node(self):
if not self._nova_node:
raise exception.Invalid(message="No associated hypervisor.")
return self._nova_node
def get_hypervisor_hostname(self):
return self.get_hypervisor_node().hypervisor_hostname
@abc.abstractmethod
def get_power_state(self):
# TODO(lpetrut): document the following methods
pass
@abc.abstractmethod
def get_id(self):
"""Return the node id provided by the bare metal service."""
pass
@abc.abstractmethod
def power_on(self):
pass
@abc.abstractmethod
def power_off(self):
pass
def set_power_state(self, state):
state = metal_constants.PowerState(state)
if state == metal_constants.PowerState.ON:
self.power_on()
elif state == metal_constants.PowerState.OFF:
self.power_off()
else:
raise exception.UnsupportedActionType(
"Cannot set power state: %s" % state)
class BaseMetalHelper(abc.ABC):
def __init__(self, osc):
self._osc = osc
@property
def nova_client(self):
if not getattr(self, "_nova_client", None):
self._nova_client = self._osc.nova()
return self._nova_client
@abc.abstractmethod
def list_compute_nodes(self):
pass
@abc.abstractmethod
def get_node(self, node_id):
pass

View File

@@ -1,23 +0,0 @@
# Copyright 2023 Cloudbase Solutions
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import enum
class PowerState(str, enum.Enum):
ON = "on"
OFF = "off"
UNKNOWN = "unknown"
ERROR = "error"

View File

@@ -1,33 +0,0 @@
# Copyright 2023 Cloudbase Solutions
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from oslo_config import cfg
from watcher.common import clients
from watcher.common.metal_helper import ironic
from watcher.common.metal_helper import maas
CONF = cfg.CONF
def get_helper(osc=None):
# TODO(lpetrut): consider caching this client.
if not osc:
osc = clients.OpenStackClients()
if CONF.maas_client.url:
return maas.MaasHelper(osc)
else:
return ironic.IronicHelper(osc)

View File

@@ -1,94 +0,0 @@
# Copyright 2023 Cloudbase Solutions
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from oslo_log import log
from watcher.common.metal_helper import base
from watcher.common.metal_helper import constants as metal_constants
LOG = log.getLogger(__name__)
POWER_STATES_MAP = {
'power on': metal_constants.PowerState.ON,
'power off': metal_constants.PowerState.OFF,
# For now, we only use ON/OFF states
'rebooting': metal_constants.PowerState.ON,
'soft power off': metal_constants.PowerState.OFF,
'soft reboot': metal_constants.PowerState.ON,
}
class IronicNode(base.BaseMetalNode):
hv_up_when_powered_off = True
def __init__(self, ironic_node, nova_node, ironic_client):
super().__init__(nova_node)
self._ironic_client = ironic_client
self._ironic_node = ironic_node
def get_power_state(self):
return POWER_STATES_MAP.get(self._ironic_node.power_state,
metal_constants.PowerState.UNKNOWN)
def get_id(self):
return self._ironic_node.uuid
def power_on(self):
self._ironic_client.node.set_power_state(self.get_id(), "on")
def power_off(self):
self._ironic_client.node.set_power_state(self.get_id(), "off")
class IronicHelper(base.BaseMetalHelper):
@property
def _client(self):
if not getattr(self, "_cached_client", None):
self._cached_client = self._osc.ironic()
return self._cached_client
def list_compute_nodes(self):
out_list = []
# TODO(lpetrut): consider using "detailed=True" instead of making
# an additional GET request per node
node_list = self._client.node.list()
for node in node_list:
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)
continue
hypervisor_node = self.nova_client.hypervisors.get(hypervisor_id)
if hypervisor_node is None:
LOG.warning('Cannot find hypervisor %s', hypervisor_id)
continue
out_node = IronicNode(node, hypervisor_node, self._client)
out_list.append(out_node)
return out_list
def get_node(self, node_id):
ironic_node = self._client.node.get(node_id)
compute_node_id = ironic_node.extra.get('compute_node_id')
if compute_node_id:
compute_node = self.nova_client.hypervisors.get(compute_node_id)
else:
compute_node = None
return IronicNode(ironic_node, compute_node, self._client)

View File

@@ -1,125 +0,0 @@
# Copyright 2023 Cloudbase Solutions
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from oslo_config import cfg
from oslo_log import log
from watcher.common import exception
from watcher.common.metal_helper import base
from watcher.common.metal_helper import constants as metal_constants
from watcher.common import utils
CONF = cfg.CONF
LOG = log.getLogger(__name__)
try:
from maas.client import enum as maas_enum
except ImportError:
maas_enum = None
class MaasNode(base.BaseMetalNode):
hv_up_when_powered_off = False
def __init__(self, maas_node, nova_node, maas_client):
super().__init__(nova_node)
self._maas_client = maas_client
self._maas_node = maas_node
def get_power_state(self):
maas_state = utils.async_compat_call(
self._maas_node.query_power_state,
timeout=CONF.maas_client.timeout)
# python-libmaas may not be available, so we'll avoid a global
# variable.
power_states_map = {
maas_enum.PowerState.ON: metal_constants.PowerState.ON,
maas_enum.PowerState.OFF: metal_constants.PowerState.OFF,
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)
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)
utils.async_compat_call(
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)
utils.async_compat_call(
self._maas_node.power_off,
timeout=CONF.maas_client.timeout)
class MaasHelper(base.BaseMetalHelper):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not maas_enum:
raise exception.UnsupportedError(
"MAAS client unavailable. Please install python-libmaas.")
@property
def _client(self):
if not getattr(self, "_cached_client", None):
self._cached_client = self._osc.maas()
return self._cached_client
def list_compute_nodes(self):
out_list = []
node_list = utils.async_compat_call(
self._client.machines.list,
timeout=CONF.maas_client.timeout)
compute_nodes = self.nova_client.hypervisors.list()
compute_node_map = dict()
for compute_node in compute_nodes:
compute_node_map[compute_node.hypervisor_hostname] = compute_node
for node in node_list:
hypervisor_node = compute_node_map.get(node.fqdn)
if not hypervisor_node:
LOG.info('Cannot find hypervisor %s', node.fqdn)
continue
out_node = MaasNode(node, hypervisor_node, self._client)
out_list.append(out_node)
return out_list
def _get_compute_node_by_hostname(self, hostname):
compute_nodes = self.nova_client.hypervisors.search(
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)
compute_node = self._get_compute_node_by_hostname(maas_node.fqdn)
return MaasNode(maas_node, compute_node, self._client)

View File

@@ -11,7 +11,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from http import HTTPStatus
from oslo_config import cfg
from oslo_log import log as logging
@@ -54,7 +53,7 @@ class PlacementHelper(object):
if rp_name:
url += '?name=%s' % rp_name
resp = self.get(url)
if resp.status_code == HTTPStatus.OK:
if resp.status_code == 200:
json_resp = resp.json()
return json_resp['resource_providers']
@@ -78,7 +77,7 @@ class PlacementHelper(object):
"""
url = '/resource_providers/%s/inventories' % rp_uuid
resp = self.get(url)
if resp.status_code == HTTPStatus.OK:
if resp.status_code == 200:
json = resp.json()
return json['inventories']
msg = ("Failed to get resource provider %(rp_uuid)s inventories. "
@@ -98,7 +97,7 @@ class PlacementHelper(object):
"""
resp = self.get("/resource_providers/%s/traits" % rp_uuid)
if resp.status_code == HTTPStatus.OK:
if resp.status_code == 200:
json = resp.json()
return json['traits']
msg = ("Failed to get resource provider %(rp_uuid)s traits. "
@@ -119,7 +118,7 @@ class PlacementHelper(object):
"""
url = '/allocations/%s' % consumer_uuid
resp = self.get(url)
if resp.status_code == HTTPStatus.OK:
if resp.status_code == 200:
json = resp.json()
return json['allocations']
msg = ("Failed to get allocations for consumer %(c_uuid). "
@@ -140,7 +139,7 @@ class PlacementHelper(object):
"""
url = '/resource_providers/%s/usages' % rp_uuid
resp = self.get(url)
if resp.status_code == HTTPStatus.OK:
if resp.status_code == 200:
json = resp.json()
return json['usages']
msg = ("Failed to get resource provider %(rp_uuid)s usages. "
@@ -165,7 +164,7 @@ class PlacementHelper(object):
"""
url = "/allocation_candidates?%s" % resources
resp = self.get(url)
if resp.status_code == HTTPStatus.OK:
if resp.status_code == 200:
data = resp.json()
return data['provider_summaries']

View File

@@ -18,7 +18,6 @@
import sys
from oslo_config import cfg
from oslo_policy import opts
from oslo_policy import policy
from watcher.common import exception
@@ -27,12 +26,6 @@ from watcher.common import policies
_ENFORCER = None
CONF = cfg.CONF
# TODO(gmann): Remove setting the default value of config policy_file
# once oslo_policy change the default value to 'policy.yaml'.
# https://github.com/openstack/oslo.policy/blob/a626ad12fe5a3abd49d70e3e5b95589d279ab578/oslo_policy/opts.py#L49
DEFAULT_POLICY_FILE = 'policy.yaml'
opts.set_defaults(CONF, DEFAULT_POLICY_FILE)
# we can get a policy enforcer by this init.
# oslo policy support change policy rule dynamically.

View File

@@ -121,40 +121,22 @@ class RequestContextSerializer(messaging.Serializer):
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
)
return messaging.RPCClient(TRANSPORT,
target,
version_cap=version_cap,
serializer=serializer)
def get_server(target, endpoints, serializer=None):
assert TRANSPORT is not None
access_policy = dispatcher.DefaultRPCAccessPolicy
serializer = RequestContextSerializer(serializer)
return messaging.get_rpc_server(
TRANSPORT,
target,
endpoints,
executor='eventlet',
serializer=serializer,
access_policy=access_policy
)
def get_notification_listener(targets, endpoints, serializer=None, pool=None):
assert NOTIFICATION_TRANSPORT is not None
serializer = RequestContextSerializer(serializer)
return messaging.get_notification_listener(
NOTIFICATION_TRANSPORT,
targets,
endpoints,
allow_requeue=False,
executor='eventlet',
pool=pool,
serializer=serializer
)
return messaging.get_rpc_server(TRANSPORT,
target,
endpoints,
executor='eventlet',
serializer=serializer,
access_policy=access_policy)
def get_notifier(publisher_id):

View File

@@ -21,12 +21,14 @@ from oslo_concurrency import processutils
from oslo_config import cfg
from oslo_log import _options
from oslo_log import log
import oslo_messaging as messaging
import oslo_messaging as om
from oslo_reports import guru_meditation_report as gmr
from oslo_reports import opts as gmr_opts
from oslo_service import service
from oslo_service import wsgi
from oslo_messaging.rpc import dispatcher
from watcher._i18n import _
from watcher.api import app
from watcher.common import config
@@ -181,6 +183,11 @@ class Service(service.ServiceBase):
]
self.notification_endpoints = self.manager.notification_endpoints
self.serializer = rpc.RequestContextSerializer(
base.WatcherObjectSerializer())
self._transport = None
self._notification_transport = None
self._conductor_client = None
self.conductor_topic_handler = None
@@ -194,17 +201,27 @@ class Service(service.ServiceBase):
self.notification_topics, self.notification_endpoints
)
@property
def transport(self):
if self._transport is None:
self._transport = om.get_rpc_transport(CONF)
return self._transport
@property
def notification_transport(self):
if self._notification_transport is None:
self._notification_transport = om.get_notification_transport(CONF)
return self._notification_transport
@property
def conductor_client(self):
if self._conductor_client is None:
target = messaging.Target(
target = om.Target(
topic=self.conductor_topic,
version=self.API_VERSION,
)
self._conductor_client = rpc.get_client(
target,
serializer=base.WatcherObjectSerializer()
)
self._conductor_client = om.RPCClient(
self.transport, target, serializer=self.serializer)
return self._conductor_client
@conductor_client.setter
@@ -212,18 +229,21 @@ class Service(service.ServiceBase):
self.conductor_client = c
def build_topic_handler(self, topic_name, endpoints=()):
target = messaging.Target(
access_policy = dispatcher.DefaultRPCAccessPolicy
serializer = rpc.RequestContextSerializer(rpc.JsonPayloadSerializer())
target = om.Target(
topic=topic_name,
# For compatibility, we can override it with 'host' opt
server=CONF.host or socket.gethostname(),
version=self.api_version,
)
return rpc.get_server(
target, endpoints,
serializer=rpc.JsonPayloadSerializer()
)
return om.get_rpc_server(
self.transport, target, endpoints,
executor='eventlet', serializer=serializer,
access_policy=access_policy)
def build_notification_handler(self, topic_names, endpoints=()):
serializer = rpc.RequestContextSerializer(rpc.JsonPayloadSerializer())
targets = []
for topic in topic_names:
kwargs = {}
@@ -231,13 +251,11 @@ class Service(service.ServiceBase):
exchange, topic = topic.split('.')
kwargs['exchange'] = exchange
kwargs['topic'] = topic
targets.append(messaging.Target(**kwargs))
return rpc.get_notification_listener(
targets, endpoints,
serializer=rpc.JsonPayloadSerializer(),
pool=CONF.host
)
targets.append(om.Target(**kwargs))
return om.get_notification_listener(
self.notification_transport, targets, endpoints,
executor='eventlet', serializer=serializer,
allow_requeue=False, pool=CONF.host)
def start(self):
LOG.debug("Connecting to '%s'", CONF.transport_url)

View File

@@ -16,14 +16,12 @@
"""Utilities and helper functions."""
import asyncio
import datetime
import inspect
import random
import re
import string
from croniter import croniter
import eventlet
from eventlet import tpool
from jsonschema import validators
from oslo_config import cfg
@@ -158,39 +156,9 @@ def extend_with_strict_schema(validator_class):
StrictDefaultValidatingDraft4Validator = extend_with_default(
extend_with_strict_schema(validators.Draft4Validator))
Draft4Validator = validators.Draft4Validator
# Some clients (e.g. MAAS) use asyncio, which isn't compatible with Eventlet.
# As a workaround, we're delegating such calls to a native thread.
def async_compat_call(f, *args, **kwargs):
timeout = kwargs.pop('timeout', None)
async def async_wrapper():
ret = f(*args, **kwargs)
if inspect.isawaitable(ret):
return await asyncio.wait_for(ret, timeout)
return ret
def tpool_wrapper():
# This will run in a separate native thread. Ideally, there should be
# a single thread permanently running an asyncio loop, but for
# convenience we'll use eventlet.tpool, which leverages a thread pool.
#
# That being considered, we're setting up a temporary asyncio loop to
# handle this call.
loop = asyncio.new_event_loop()
try:
asyncio.set_event_loop(loop)
return loop.run_until_complete(async_wrapper())
finally:
loop.close()
# We'll use eventlet timeouts as an extra precaution and asyncio timeouts
# 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("Timeout: %ss" % timeout)):
return tpool.execute(tpool_wrapper)
def random_string(n):
return ''.join([random.choice(
string.ascii_letters + string.digits) for i in range(n)])

View File

@@ -35,7 +35,6 @@ from watcher.conf import grafana_client
from watcher.conf import grafana_translators
from watcher.conf import ironic_client
from watcher.conf import keystone_client
from watcher.conf import maas_client
from watcher.conf import monasca_client
from watcher.conf import neutron_client
from watcher.conf import nova_client
@@ -55,7 +54,6 @@ db.register_opts(CONF)
planner.register_opts(CONF)
applier.register_opts(CONF)
decision_engine.register_opts(CONF)
maas_client.register_opts(CONF)
monasca_client.register_opts(CONF)
nova_client.register_opts(CONF)
glance_client.register_opts(CONF)

View File

@@ -134,13 +134,7 @@ GRAFANA_CLIENT_OPTS = [
"InfluxDB this will be the retention period. "
"These queries will need to be constructed using tools "
"such as Postman. Example: SELECT cpu FROM {4}."
"cpu_percent WHERE host == '{1}' AND time > now()-{2}s"),
cfg.IntOpt('http_timeout',
min=0,
default=60,
mutable=True,
help='Timeout for Grafana request')
]
"cpu_percent WHERE host == '{1}' AND time > now()-{2}s")]
def register_opts(conf):

View File

@@ -1,38 +0,0 @@
# Copyright 2023 Cloudbase Solutions
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from oslo_config import cfg
maas_client = cfg.OptGroup(name='maas_client',
title='Configuration Options for MaaS')
MAAS_CLIENT_OPTS = [
cfg.StrOpt('url',
help='MaaS URL, example: http://1.2.3.4:5240/MAAS'),
cfg.StrOpt('api_key',
help='MaaS API authentication key.'),
cfg.IntOpt('timeout',
default=60,
help='MaaS client operation timeout in seconds.')]
def register_opts(conf):
conf.register_group(maas_client)
conf.register_opts(MAAS_CLIENT_OPTS, group=maas_client)
def list_opts():
return [(maas_client, MAAS_CLIENT_OPTS)]

View File

@@ -13,8 +13,8 @@
from logging import config as log_config
from alembic import context
from oslo_db.sqlalchemy import enginefacade
from watcher.db.sqlalchemy import api as sqla_api
from watcher.db.sqlalchemy import models
# this is the Alembic Config object, which provides
@@ -43,7 +43,7 @@ def run_migrations_online():
and associate a connection with the context.
"""
engine = enginefacade.writer.get_engine()
engine = sqla_api.get_engine()
with engine.connect() as connection:
context.configure(connection=connection,
target_metadata=target_metadata)

View File

@@ -6,7 +6,6 @@ Create Date: 2017-03-24 11:21:29.036532
"""
from alembic import op
from sqlalchemy import inspect
import sqlalchemy as sa
from watcher.db.sqlalchemy import models
@@ -15,20 +14,11 @@ from watcher.db.sqlalchemy import models
revision = '0f6042416884'
down_revision = '001'
def _table_exists(table_name):
bind = op.get_context().bind
insp = inspect(bind)
names = insp.get_table_names()
return any(t == table_name for t in names)
def upgrade():
if _table_exists('apscheduler_jobs'):
return
op.create_table(
'apscheduler_jobs',
sa.Column('id', sa.Unicode(191),
sa.Column('id', sa.Unicode(191, _warn_on_bytestring=False),
nullable=False),
sa.Column('next_run_time', sa.Float(25), index=True),
sa.Column('job_state', sa.LargeBinary, nullable=False),

View File

@@ -19,12 +19,10 @@
import collections
import datetime
import operator
import threading
from oslo_config import cfg
from oslo_db import api as oslo_db_api
from oslo_db import exception as db_exc
from oslo_db.sqlalchemy import enginefacade
from oslo_db.sqlalchemy import session as db_session
from oslo_db.sqlalchemy import utils as db_utils
from oslo_utils import timeutils
from sqlalchemy.inspection import inspect
@@ -40,7 +38,24 @@ from watcher import objects
CONF = cfg.CONF
_CONTEXT = threading.local()
_FACADE = None
def _create_facade_lazily():
global _FACADE
if _FACADE is None:
_FACADE = db_session.EngineFacade.from_config(CONF)
return _FACADE
def get_engine():
facade = _create_facade_lazily()
return facade.get_engine()
def get_session(**kwargs):
facade = _create_facade_lazily()
return facade.get_session(**kwargs)
def get_backend():
@@ -48,15 +63,14 @@ def get_backend():
return Connection()
def _session_for_read():
return enginefacade.reader.using(_CONTEXT)
def model_query(model, *args, **kwargs):
"""Query helper for simpler session usage.
# NOTE(tylerchristie) Please add @oslo_db_api.retry_on_deadlock decorator to
# any new methods using _session_for_write (as deadlocks happen on write), so
# that oslo_db is able to retry in case of deadlocks.
def _session_for_write():
return enginefacade.writer.using(_CONTEXT)
:param session: if present, the session to use
"""
session = kwargs.get('session') or get_session()
query = session.query(model, *args)
return query
def add_identity_filter(query, value):
@@ -79,6 +93,8 @@ def add_identity_filter(query, value):
def _paginate_query(model, limit=None, marker=None, sort_key=None,
sort_dir=None, query=None):
if not query:
query = model_query(model)
sort_keys = ['id']
if sort_key and sort_key not in sort_keys:
sort_keys.insert(0, sort_key)
@@ -228,58 +244,52 @@ class Connection(api.BaseConnection):
for relationship in relationships:
if not relationship.uselist:
# We have a One-to-X relationship
query = query.options(joinedload(
getattr(model, relationship.key)))
query = query.options(joinedload(relationship.key))
return query
@oslo_db_api.retry_on_deadlock
def _create(self, model, values):
with _session_for_write() as session:
obj = model()
cleaned_values = {k: v for k, v in values.items()
if k not in self._get_relationships(model)}
obj.update(cleaned_values)
session.add(obj)
session.flush()
return obj
obj = model()
cleaned_values = {k: v for k, v in values.items()
if k not in self._get_relationships(model)}
obj.update(cleaned_values)
obj.save()
return obj
def _get(self, context, model, fieldname, value, eager):
with _session_for_read() as session:
query = session.query(model)
if eager:
query = self._set_eager_options(model, query)
query = model_query(model)
if eager:
query = self._set_eager_options(model, query)
query = query.filter(getattr(model, fieldname) == value)
if not context.show_deleted:
query = query.filter(model.deleted_at.is_(None))
query = query.filter(getattr(model, fieldname) == value)
if not context.show_deleted:
query = query.filter(model.deleted_at.is_(None))
try:
obj = query.one()
except exc.NoResultFound:
raise exception.ResourceNotFound(name=model.__name__, id=value)
try:
obj = query.one()
except exc.NoResultFound:
raise exception.ResourceNotFound(name=model.__name__, id=value)
return obj
return obj
@staticmethod
@oslo_db_api.retry_on_deadlock
def _update(model, id_, values):
with _session_for_write() as session:
query = session.query(model)
session = get_session()
with session.begin():
query = model_query(model, session=session)
query = add_identity_filter(query, id_)
try:
ref = query.with_for_update().one()
ref = query.with_lockmode('update').one()
except exc.NoResultFound:
raise exception.ResourceNotFound(name=model.__name__, id=id_)
ref.update(values)
return ref
return ref
@staticmethod
@oslo_db_api.retry_on_deadlock
def _soft_delete(model, id_):
with _session_for_write() as session:
query = session.query(model)
session = get_session()
with session.begin():
query = model_query(model, session=session)
query = add_identity_filter(query, id_)
try:
row = query.one()
@@ -291,10 +301,10 @@ class Connection(api.BaseConnection):
return row
@staticmethod
@oslo_db_api.retry_on_deadlock
def _destroy(model, id_):
with _session_for_write() as session:
query = session.query(model)
session = get_session()
with session.begin():
query = model_query(model, session=session)
query = add_identity_filter(query, id_)
try:
@@ -307,15 +317,14 @@ class Connection(api.BaseConnection):
def _get_model_list(self, model, add_filters_func, context, filters=None,
limit=None, marker=None, sort_key=None, sort_dir=None,
eager=False):
with _session_for_read() as session:
query = session.query(model)
if eager:
query = self._set_eager_options(model, query)
query = add_filters_func(query, filters)
if not context.show_deleted:
query = query.filter(model.deleted_at.is_(None))
return _paginate_query(model, limit, marker,
sort_key, sort_dir, query)
query = model_query(model)
if eager:
query = self._set_eager_options(model, query)
query = add_filters_func(query, filters)
if not context.show_deleted:
query = query.filter(model.deleted_at.is_(None))
return _paginate_query(model, limit, marker,
sort_key, sort_dir, query)
# NOTE(erakli): _add_..._filters methods should be refactored to have same
# content. join_fieldmap should be filled with JoinMap instead of dict
@@ -410,12 +419,11 @@ class Connection(api.BaseConnection):
plain_fields=plain_fields, join_fieldmap=join_fieldmap)
if 'audit_uuid' in filters:
with _session_for_read() as session:
stmt = session.query(models.ActionPlan).join(
models.Audit,
models.Audit.id == models.ActionPlan.audit_id)\
.filter_by(uuid=filters['audit_uuid']).subquery()
query = query.filter_by(action_plan_id=stmt.c.id)
stmt = model_query(models.ActionPlan).join(
models.Audit,
models.Audit.id == models.ActionPlan.audit_id)\
.filter_by(uuid=filters['audit_uuid']).subquery()
query = query.filter_by(action_plan_id=stmt.c.id)
return query
@@ -593,21 +601,20 @@ class Connection(api.BaseConnection):
if not values.get('uuid'):
values['uuid'] = utils.generate_uuid()
with _session_for_write() as session:
query = session.query(models.AuditTemplate)
query = query.filter_by(name=values.get('name'),
deleted_at=None)
query = model_query(models.AuditTemplate)
query = query.filter_by(name=values.get('name'),
deleted_at=None)
if len(query.all()) > 0:
raise exception.AuditTemplateAlreadyExists(
audit_template=values['name'])
if len(query.all()) > 0:
raise exception.AuditTemplateAlreadyExists(
audit_template=values['name'])
try:
audit_template = self._create(models.AuditTemplate, values)
except db_exc.DBDuplicateEntry:
raise exception.AuditTemplateAlreadyExists(
audit_template=values['name'])
return audit_template
try:
audit_template = self._create(models.AuditTemplate, values)
except db_exc.DBDuplicateEntry:
raise exception.AuditTemplateAlreadyExists(
audit_template=values['name'])
return audit_template
def _get_audit_template(self, context, fieldname, value, eager):
try:
@@ -669,26 +676,25 @@ class Connection(api.BaseConnection):
if not values.get('uuid'):
values['uuid'] = utils.generate_uuid()
with _session_for_write() as session:
query = session.query(models.Audit)
query = query.filter_by(name=values.get('name'),
deleted_at=None)
query = model_query(models.Audit)
query = query.filter_by(name=values.get('name'),
deleted_at=None)
if len(query.all()) > 0:
raise exception.AuditAlreadyExists(
audit=values['name'])
if len(query.all()) > 0:
raise exception.AuditAlreadyExists(
audit=values['name'])
if values.get('state') is None:
values['state'] = objects.audit.State.PENDING
if values.get('state') is None:
values['state'] = objects.audit.State.PENDING
if not values.get('auto_trigger'):
values['auto_trigger'] = False
if not values.get('auto_trigger'):
values['auto_trigger'] = False
try:
audit = self._create(models.Audit, values)
except db_exc.DBDuplicateEntry:
raise exception.AuditAlreadyExists(audit=values['uuid'])
return audit
try:
audit = self._create(models.Audit, values)
except db_exc.DBDuplicateEntry:
raise exception.AuditAlreadyExists(audit=values['uuid'])
return audit
def _get_audit(self, context, fieldname, value, eager):
try:
@@ -712,13 +718,14 @@ class Connection(api.BaseConnection):
def destroy_audit(self, audit_id):
def is_audit_referenced(session, audit_id):
"""Checks whether the audit is referenced by action_plan(s)."""
query = session.query(models.ActionPlan)
query = model_query(models.ActionPlan, session=session)
query = self._add_action_plans_filters(
query, {'audit_id': audit_id})
return query.count() != 0
with _session_for_write() as session:
query = session.query(models.Audit)
session = get_session()
with session.begin():
query = model_query(models.Audit, session=session)
query = add_identity_filter(query, audit_id)
try:
@@ -785,8 +792,9 @@ class Connection(api.BaseConnection):
context, fieldname="uuid", value=action_uuid, eager=eager)
def destroy_action(self, action_id):
with _session_for_write() as session:
query = session.query(models.Action)
session = get_session()
with session.begin():
query = model_query(models.Action, session=session)
query = add_identity_filter(query, action_id)
count = query.delete()
if count != 1:
@@ -802,16 +810,17 @@ class Connection(api.BaseConnection):
@staticmethod
def _do_update_action(action_id, values):
with _session_for_write() as session:
query = session.query(models.Action)
session = get_session()
with session.begin():
query = model_query(models.Action, session=session)
query = add_identity_filter(query, action_id)
try:
ref = query.with_for_update().one()
ref = query.with_lockmode('update').one()
except exc.NoResultFound:
raise exception.ActionNotFound(action=action_id)
ref.update(values)
return ref
return ref
def soft_delete_action(self, action_id):
try:
@@ -855,13 +864,14 @@ class Connection(api.BaseConnection):
def destroy_action_plan(self, action_plan_id):
def is_action_plan_referenced(session, action_plan_id):
"""Checks whether the action_plan is referenced by action(s)."""
query = session.query(models.Action)
query = model_query(models.Action, session=session)
query = self._add_actions_filters(
query, {'action_plan_id': action_plan_id})
return query.count() != 0
with _session_for_write() as session:
query = session.query(models.ActionPlan)
session = get_session()
with session.begin():
query = model_query(models.ActionPlan, session=session)
query = add_identity_filter(query, action_plan_id)
try:
@@ -885,16 +895,17 @@ class Connection(api.BaseConnection):
@staticmethod
def _do_update_action_plan(action_plan_id, values):
with _session_for_write() as session:
query = session.query(models.ActionPlan)
session = get_session()
with session.begin():
query = model_query(models.ActionPlan, session=session)
query = add_identity_filter(query, action_plan_id)
try:
ref = query.with_for_update().one()
ref = query.with_lockmode('update').one()
except exc.NoResultFound:
raise exception.ActionPlanNotFound(action_plan=action_plan_id)
ref.update(values)
return ref
return ref
def soft_delete_action_plan(self, action_plan_id):
try:

View File

@@ -22,7 +22,6 @@ from apscheduler.jobstores.base import ConflictingIdError
from apscheduler.jobstores import sqlalchemy
from apscheduler.util import datetime_to_utc_timestamp
from apscheduler.util import maybe_ref
from apscheduler.util import utc_timestamp_to_datetime
from watcher.common import context
from watcher.common import service
@@ -33,7 +32,7 @@ try:
except ImportError: # pragma: nocover
import pickle
from sqlalchemy import Table, MetaData, select, and_, null
from sqlalchemy import Table, MetaData, select, and_
from sqlalchemy.exc import IntegrityError
@@ -59,7 +58,8 @@ class WatcherJobStore(sqlalchemy.SQLAlchemyJobStore):
super(WatcherJobStore, self).__init__(url, engine, tablename,
metadata, pickle_protocol)
metadata = maybe_ref(metadata) or MetaData()
self.jobs_t = Table(tablename, metadata, autoload_with=engine)
self.jobs_t = Table(tablename, metadata, autoload=True,
autoload_with=engine)
service_ident = service.ServiceHeartbeat.get_service_name()
self.tag = tag or {'host': service_ident[0], 'name': service_ident[1]}
self.service_id = objects.Service.list(context=context.make_context(),
@@ -79,8 +79,7 @@ class WatcherJobStore(sqlalchemy.SQLAlchemyJobStore):
'tag': jsonutils.dumps(self.tag)
})
try:
with self.engine.begin() as conn:
conn.execute(insert)
self.engine.execute(insert)
except IntegrityError:
raise ConflictingIdError(job.id)
@@ -89,36 +88,20 @@ class WatcherJobStore(sqlalchemy.SQLAlchemyJobStore):
self._fix_paused_jobs_sorting(jobs)
return jobs
def get_next_run_time(self):
selectable = select(self.jobs_t.c.next_run_time).\
where(self.jobs_t.c.next_run_time != null()).\
order_by(self.jobs_t.c.next_run_time).limit(1)
with self.engine.begin() as connection:
# NOTE(danms): The apscheduler implementation of this gets a
# decimal.Decimal back from scalar() which causes
# utc_timestamp_to_datetime() to choke since it is expecting a
# python float. Assume this is SQLAlchemy 2.0 stuff, so just
# coerce to a float here.
next_run_time = connection.execute(selectable).scalar()
return utc_timestamp_to_datetime(float(next_run_time)
if next_run_time is not None
else None)
def _get_jobs(self, *conditions):
jobs = []
conditions += (self.jobs_t.c.service_id == self.service_id,)
selectable = select(
self.jobs_t.c.id, self.jobs_t.c.job_state, self.jobs_t.c.tag
[self.jobs_t.c.id, self.jobs_t.c.job_state, self.jobs_t.c.tag]
).order_by(self.jobs_t.c.next_run_time).where(and_(*conditions))
failed_job_ids = set()
with self.engine.begin() as conn:
for row in conn.execute(selectable):
try:
jobs.append(self._reconstitute_job(row.job_state))
except Exception:
self._logger.exception(
'Unable to restore job "%s" -- removing it', row.id)
failed_job_ids.add(row.id)
for row in self.engine.execute(selectable):
try:
jobs.append(self._reconstitute_job(row.job_state))
except Exception:
self._logger.exception(
'Unable to restore job "%s" -- removing it', row.id)
failed_job_ids.add(row.id)
# Remove all the jobs we failed to restore
if failed_job_ids:

View File

@@ -20,9 +20,9 @@ import alembic
from alembic import config as alembic_config
import alembic.migration as alembic_migration
from oslo_db import exception as db_exc
from oslo_db.sqlalchemy import enginefacade
from watcher._i18n import _
from watcher.db.sqlalchemy import api as sqla_api
from watcher.db.sqlalchemy import models
@@ -39,7 +39,7 @@ def version(engine=None):
:rtype: string
"""
if engine is None:
engine = enginefacade.reader.get_engine()
engine = sqla_api.get_engine()
with engine.connect() as conn:
context = alembic_migration.MigrationContext.configure(conn)
return context.get_current_revision()
@@ -63,7 +63,7 @@ def create_schema(config=None, engine=None):
Can be used for initial installation instead of upgrade('head').
"""
if engine is None:
engine = enginefacade.writer.get_engine()
engine = sqla_api.get_engine()
# NOTE(viktors): If we will use metadata.create_all() for non empty db
# schema, it will only add the new tables, but leave

View File

@@ -18,6 +18,7 @@ SQLAlchemy models for watcher service
from oslo_db.sqlalchemy import models
from oslo_serialization import jsonutils
import six.moves.urllib.parse as urlparse
from sqlalchemy import Boolean
from sqlalchemy import Column
from sqlalchemy import DateTime
@@ -32,7 +33,7 @@ from sqlalchemy import String
from sqlalchemy import Text
from sqlalchemy.types import TypeDecorator, TEXT
from sqlalchemy import UniqueConstraint
import urllib.parse as urlparse
from watcher import conf
CONF = conf.CONF
@@ -93,6 +94,14 @@ class WatcherBase(models.SoftDeleteMixin,
d[c.name] = self[c.name]
return d
def save(self, session=None):
import watcher.db.sqlalchemy.api as db_api
if session is None:
session = db_api.get_session()
super(WatcherBase, self).save(session)
Base = declarative_base(cls=WatcherBase)

View File

@@ -52,7 +52,7 @@ class ContinuousAuditHandler(base.AuditHandler):
self._audit_scheduler = scheduling.BackgroundSchedulerService(
jobstores={
'default': job_store.WatcherJobStore(
engine=sq_api.enginefacade.writer.get_engine()),
engine=sq_api.get_engine()),
}
)
return self._audit_scheduler

View File

@@ -19,8 +19,6 @@ import time
from oslo_config import cfg
from oslo_log import log
from watcher.common import exception
CONF = cfg.CONF
LOG = log.getLogger(__name__)
@@ -56,14 +54,7 @@ class DataSourceBase(object):
instance_root_disk_size=None,
)
def _get_meter(self, meter_name):
"""Retrieve the meter from the metric map or raise error"""
meter = self.METRIC_MAP.get(meter_name)
if meter is None:
raise exception.MetricNotAvailable(metric=meter_name)
return meter
def query_retry(self, f, *args, ignored_exc=None, **kwargs):
def query_retry(self, f, *args, **kwargs):
"""Attempts to retrieve metrics from the external service
Attempts to access data from the external service and handles
@@ -71,23 +62,15 @@ class DataSourceBase(object):
to the value of query_max_retries
:param f: The method that performs the actual querying for metrics
:param args: Array of arguments supplied to the method
:param ignored_exc: An exception or tuple of exceptions that shouldn't
be retried, for example "NotFound" exceptions.
:param kwargs: The amount of arguments supplied to the method
:return: The value as retrieved from the external service
"""
num_retries = CONF.watcher_datasources.query_max_retries
timeout = CONF.watcher_datasources.query_timeout
ignored_exc = ignored_exc or tuple()
for i in range(num_retries):
try:
return f(*args, **kwargs)
except ignored_exc as e:
LOG.debug("Got an ignored exception (%s) while calling: %s ",
e, f)
return
except Exception as e:
LOG.exception(e)
self.query_retry_reset(e)
@@ -139,30 +122,6 @@ class DataSourceBase(object):
pass
@abc.abstractmethod
def statistic_series(self, resource=None, resource_type=None,
meter_name=None, start_time=None, end_time=None,
granularity=300):
"""Retrieves metrics based on the specified parameters over a period
:param resource: Resource object as defined in watcher models such as
ComputeNode and Instance
:param resource_type: Indicates which type of object is supplied
to the resource parameter
:param meter_name: The desired metric to retrieve as key from
METRIC_MAP
:param start_time: The datetime to start retrieving metrics for
:type start_time: datetime.datetime
:param end_time: The datetime to limit the retrieval of metrics to
:type end_time: datetime.datetime
:param granularity: Interval between samples in measurements in
seconds
:return: Dictionary of key value pairs with timestamps and metric
values
"""
pass
@abc.abstractmethod
def get_host_cpu_usage(self, resource, period, aggregate,
granularity=None):

View File

@@ -161,7 +161,9 @@ class CeilometerHelper(base.DataSourceBase):
end_time = datetime.datetime.utcnow()
start_time = end_time - datetime.timedelta(seconds=int(period))
meter = self._get_meter(meter_name)
meter = self.METRIC_MAP.get(meter_name)
if meter is None:
raise exception.MetricNotAvailable(metric=meter_name)
if aggregate == 'mean':
aggregate = 'avg'
@@ -192,12 +194,6 @@ class CeilometerHelper(base.DataSourceBase):
item_value *= 10
return item_value
def statistic_series(self, resource=None, resource_type=None,
meter_name=None, start_time=None, end_time=None,
granularity=300):
raise NotImplementedError(
_('Ceilometer helper does not support statistic series method'))
def get_host_cpu_usage(self, resource, period,
aggregate, granularity=None):

View File

@@ -19,11 +19,11 @@
from datetime import datetime
from datetime import timedelta
from gnocchiclient import exceptions as gnc_exc
from oslo_config import cfg
from oslo_log import log
from watcher.common import clients
from watcher.common import exception
from watcher.decision_engine.datasources import base
CONF = cfg.CONF
@@ -39,7 +39,7 @@ class GnocchiHelper(base.DataSourceBase):
host_inlet_temp='hardware.ipmi.node.temperature',
host_airflow='hardware.ipmi.node.airflow',
host_power='hardware.ipmi.node.power',
instance_cpu_usage='cpu',
instance_cpu_usage='cpu_util',
instance_ram_usage='memory.resident',
instance_ram_allocated='memory',
instance_l3_cache_usage='cpu_l3_cache',
@@ -72,7 +72,9 @@ class GnocchiHelper(base.DataSourceBase):
stop_time = datetime.utcnow()
start_time = stop_time - timedelta(seconds=(int(period)))
meter = self._get_meter(meter_name)
meter = self.METRIC_MAP.get(meter_name)
if meter is None:
raise exception.MetricNotAvailable(metric=meter_name)
if aggregate == 'count':
aggregate = 'mean'
@@ -85,9 +87,7 @@ class GnocchiHelper(base.DataSourceBase):
kwargs = dict(query={"=": {"original_resource_id": resource_id}},
limit=1)
resources = self.query_retry(
f=self.gnocchi.resource.search,
ignored_exc=gnc_exc.NotFound,
**kwargs)
f=self.gnocchi.resource.search, **kwargs)
if not resources:
LOG.warning("The {0} resource {1} could not be "
@@ -96,25 +96,6 @@ class GnocchiHelper(base.DataSourceBase):
resource_id = resources[0]['id']
if meter_name == "instance_cpu_usage":
if resource_type != "instance":
LOG.warning("Unsupported resource type for metric "
"'instance_cpu_usage': ", resource_type)
return
# The "cpu_util" gauge (percentage) metric has been removed.
# We're going to obtain the same result by using the rate of change
# aggregate operation.
if aggregate not in ("mean", "rate:mean"):
LOG.warning("Unsupported aggregate for instance_cpu_usage "
"metric: %s. "
"Supported aggregates: mean, rate:mean ",
aggregate)
return
# TODO(lpetrut): consider supporting other aggregates.
aggregate = "rate:mean"
raw_kwargs = dict(
metric=meter,
start=start_time,
@@ -127,9 +108,7 @@ class GnocchiHelper(base.DataSourceBase):
kwargs = {k: v for k, v in raw_kwargs.items() if k and v}
statistics = self.query_retry(
f=self.gnocchi.metric.get_measures,
ignored_exc=gnc_exc.NotFound,
**kwargs)
f=self.gnocchi.metric.get_measures, **kwargs)
return_value = None
if statistics:
@@ -141,67 +120,6 @@ class GnocchiHelper(base.DataSourceBase):
# Airflow from hardware.ipmi.node.airflow is reported as
# 1/10 th of actual CFM
return_value *= 10
if meter_name == "instance_cpu_usage":
# "rate:mean" can return negative values for migrated vms.
return_value = max(0, return_value)
# We're converting the cumulative cpu time (ns) to cpu usage
# percentage.
vcpus = resource.vcpus
if not vcpus:
LOG.warning("instance vcpu count not set, assuming 1")
vcpus = 1
return_value *= 100 / (granularity * 10e+8) / vcpus
return return_value
def statistic_series(self, resource=None, resource_type=None,
meter_name=None, start_time=None, end_time=None,
granularity=300):
meter = self._get_meter(meter_name)
resource_id = resource.uuid
if resource_type == 'compute_node':
resource_id = "%s_%s" % (resource.hostname, resource.hostname)
kwargs = dict(query={"=": {"original_resource_id": resource_id}},
limit=1)
resources = self.query_retry(
f=self.gnocchi.resource.search,
ignored_exc=gnc_exc.NotFound,
**kwargs)
if not resources:
LOG.warning("The {0} resource {1} could not be "
"found".format(self.NAME, resource_id))
return
resource_id = resources[0]['id']
raw_kwargs = dict(
metric=meter,
start=start_time,
stop=end_time,
resource_id=resource_id,
granularity=granularity,
)
kwargs = {k: v for k, v in raw_kwargs.items() if k and v}
statistics = self.query_retry(
f=self.gnocchi.metric.get_measures,
ignored_exc=gnc_exc.NotFound,
**kwargs)
return_value = None
if statistics:
# measure has structure [time, granularity, value]
if meter_name == 'host_airflow':
# Airflow from hardware.ipmi.node.airflow is reported as
# 1/10 th of actual CFM
return_value = {s[0]: s[2]*10 for s in statistics}
else:
return_value = {s[0]: s[2] for s in statistics}
return return_value

View File

@@ -18,11 +18,9 @@
from urllib import parse as urlparse
from http import HTTPStatus
from oslo_config import cfg
from oslo_log import log
from watcher._i18n import _
from watcher.common import clients
from watcher.common import exception
from watcher.decision_engine.datasources import base
@@ -138,13 +136,12 @@ class GrafanaHelper(base.DataSourceBase):
raise exception.DataSourceNotAvailable(self.NAME)
resp = requests.get(self._base_url + str(project_id) + '/query',
params=params, headers=self._headers,
timeout=CONF.grafana_client.http_timeout)
if resp.status_code == HTTPStatus.OK:
params=params, headers=self._headers)
if resp.status_code == 200:
return resp
elif resp.status_code == HTTPStatus.BAD_REQUEST:
elif resp.status_code == 400:
LOG.error("Query for metric is invalid")
elif resp.status_code == HTTPStatus.UNAUTHORIZED:
elif resp.status_code == 401:
LOG.error("Authorization token is invalid")
raise exception.DataSourceNotAvailable(self.NAME)
@@ -191,12 +188,6 @@ class GrafanaHelper(base.DataSourceBase):
return result
def statistic_series(self, resource=None, resource_type=None,
meter_name=None, start_time=None, end_time=None,
granularity=300):
raise NotImplementedError(
_('Grafana helper does not support statistic series method'))
def get_host_cpu_usage(self, resource, period=300,
aggregate="mean", granularity=None):
return self.statistic_aggregation(

View File

@@ -21,6 +21,7 @@ import datetime
from monascaclient import exc
from watcher.common import clients
from watcher.common import exception
from watcher.decision_engine.datasources import base
@@ -89,7 +90,9 @@ class MonascaHelper(base.DataSourceBase):
stop_time = datetime.datetime.utcnow()
start_time = stop_time - datetime.timedelta(seconds=(int(period)))
meter = self._get_meter(meter_name)
meter = self.METRIC_MAP.get(meter_name)
if meter is None:
raise exception.MetricNotAvailable(metric=meter_name)
if aggregate == 'mean':
aggregate = 'avg'
@@ -118,34 +121,6 @@ class MonascaHelper(base.DataSourceBase):
return cpu_usage
def statistic_series(self, resource=None, resource_type=None,
meter_name=None, start_time=None, end_time=None,
granularity=300):
meter = self._get_meter(meter_name)
raw_kwargs = dict(
name=meter,
start_time=start_time.isoformat(),
end_time=end_time.isoformat(),
dimensions={'hostname': resource.uuid},
statistics='avg',
group_by='*',
)
kwargs = {k: v for k, v in raw_kwargs.items() if k and v}
statistics = self.query_retry(
f=self.monasca.metrics.list_statistics, **kwargs)
result = {}
for stat in statistics:
v_index = stat['columns'].index('avg')
t_index = stat['columns'].index('timestamp')
result.update({r[t_index]: r[v_index] for r in stat['statistics']})
return result
def get_host_cpu_usage(self, resource, period,
aggregate, granularity=None):
return self.statistic_aggregation(

View File

@@ -205,7 +205,7 @@ class CinderModelBuilder(base.BaseModelBuilder):
"""Build a storage node from a Cinder storage node
:param node: A storage node
:type node: :py:class:`~cinderclient.v3.services.Service`
:type node: :py:class:`~cinderclient.v2.services.Service`
"""
# node.host is formatted as host@backendname since ocata,
# or may be only host as of ocata
@@ -233,7 +233,7 @@ class CinderModelBuilder(base.BaseModelBuilder):
"""Build a storage pool from a Cinder storage pool
:param pool: A storage pool
:type pool: :py:class:`~cinderclient.v3.pools.Pool`
:type pool: :py:class:`~cinderclient.v2.pools.Pool`
:raises: exception.InvalidPoolAttributeValue
"""
# build up the storage pool.

View File

@@ -81,7 +81,6 @@ class BareMetalModelBuilder(base.BaseModelBuilder):
def __init__(self, osc):
self.osc = osc
self.model = model_root.BaremetalModelRoot()
# TODO(lpetrut): add MAAS support
self.ironic_helper = ironic_helper.IronicHelper(osc=self.osc)
def add_ironic_node(self, node):

View File

@@ -48,7 +48,7 @@ class NovaClusterDataModelCollector(base.BaseClusterDataModelCollector):
"type": "array",
"items": {
"anyOf": [
{"$ref": HOST_AGGREGATES + "host_aggr_id"},
{"$ref": HOST_AGGREGATES + "id"},
{"$ref": HOST_AGGREGATES + "name"},
]
}
@@ -98,8 +98,7 @@ class NovaClusterDataModelCollector(base.BaseClusterDataModelCollector):
"type": "array",
"items": {
"anyOf": [
{"$ref":
HOST_AGGREGATES + "host_aggr_id"},
{"$ref": HOST_AGGREGATES + "id"},
{"$ref": HOST_AGGREGATES + "name"},
]
}
@@ -130,7 +129,7 @@ class NovaClusterDataModelCollector(base.BaseClusterDataModelCollector):
"additionalProperties": False
},
"host_aggregates": {
"host_aggr_id": {
"id": {
"properties": {
"id": {
"oneOf": [

View File

@@ -157,7 +157,7 @@ class ModelRoot(nx.DiGraph, base.Model):
if node_list:
return node_list[0]
else:
raise exception.ComputeNodeNotFound(name=name)
raise exception.ComputeResourceNotFound
except exception.ComputeResourceNotFound:
raise exception.ComputeNodeNotFound(name=name)
@@ -631,7 +631,7 @@ class BaremetalModelRoot(nx.DiGraph, base.Model):
super(BaremetalModelRoot, self).remove_node(node.uuid)
except nx.NetworkXError as exc:
LOG.exception(exc)
raise exception.IronicNodeNotFound(uuid=node.uuid)
raise exception.IronicNodeNotFound(name=node.uuid)
@lockutils.synchronized("baremetal_model")
def get_all_ironic_nodes(self):
@@ -643,7 +643,7 @@ class BaremetalModelRoot(nx.DiGraph, base.Model):
try:
return self._get_by_uuid(uuid)
except exception.BaremetalResourceNotFound:
raise exception.IronicNodeNotFound(uuid=uuid)
raise exception.IronicNodeNotFound(name=uuid)
def _get_by_uuid(self, uuid):
try:

View File

@@ -252,6 +252,9 @@ class BaseStrategy(loadable.Loadable, metaclass=abc.ABCMeta):
if not self.compute_model:
raise exception.ClusterStateNotDefined()
if self.compute_model.stale:
raise exception.ClusterStateStale()
LOG.debug(self.compute_model.to_string())
def execute(self, audit=None):

View File

@@ -18,6 +18,8 @@
#
from oslo_log import log
import six
from watcher._i18n import _
from watcher.common import exception
from watcher.decision_engine.model import element
@@ -101,7 +103,7 @@ class HostMaintenance(base.HostMaintenanceBaseStrategy):
def get_instance_state_str(self, instance):
"""Get instance state in string format"""
if isinstance(instance.state, str):
if isinstance(instance.state, six.string_types):
return instance.state
elif isinstance(instance.state, element.InstanceState):
return instance.state.value
@@ -114,7 +116,7 @@ class HostMaintenance(base.HostMaintenanceBaseStrategy):
def get_node_status_str(self, node):
"""Get node status in string format"""
if isinstance(node.status, str):
if isinstance(node.status, six.string_types):
return node.status
elif isinstance(node.status, element.ServiceState):
return node.status.value

View File

@@ -23,8 +23,6 @@ from oslo_log import log
from watcher._i18n import _
from watcher.common import exception
from watcher.common.metal_helper import constants as metal_constants
from watcher.common.metal_helper import factory as metal_helper_factory
from watcher.decision_engine.strategy.strategies import base
LOG = log.getLogger(__name__)
@@ -83,7 +81,7 @@ class SavingEnergy(base.SavingEnergyBaseStrategy):
def __init__(self, config, osc=None):
super(SavingEnergy, self).__init__(config, osc)
self._metal_helper = None
self._ironic_client = None
self._nova_client = None
self.with_vms_node_pool = []
@@ -93,10 +91,10 @@ class SavingEnergy(base.SavingEnergyBaseStrategy):
self.min_free_hosts_num = 1
@property
def metal_helper(self):
if not self._metal_helper:
self._metal_helper = metal_helper_factory.get_helper(self.osc)
return self._metal_helper
def ironic_client(self):
if not self._ironic_client:
self._ironic_client = self.osc.ironic()
return self._ironic_client
@property
def nova_client(self):
@@ -151,10 +149,10 @@ class SavingEnergy(base.SavingEnergyBaseStrategy):
:return: None
"""
params = {'state': state,
'resource_name': node.get_hypervisor_hostname()}
'resource_name': node.hostname}
self.solution.add_action(
action_type='change_node_power_state',
resource_id=node.get_id(),
resource_id=node.uuid,
input_parameters=params)
def get_hosts_pool(self):
@@ -164,61 +162,57 @@ class SavingEnergy(base.SavingEnergyBaseStrategy):
"""
node_list = self.metal_helper.list_compute_nodes()
node_list = self.ironic_client.node.list()
for node in node_list:
hypervisor_node = node.get_hypervisor_node().to_dict()
node_info = self.ironic_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)
continue
hypervisor_node = self.nova_client.hypervisors.get(hypervisor_id)
if hypervisor_node is None:
LOG.warning(('Cannot find hypervisor %s'), hypervisor_id)
continue
node.hostname = hypervisor_node.hypervisor_hostname
hypervisor_node = hypervisor_node.to_dict()
compute_service = hypervisor_node.get('service', None)
host_name = compute_service.get('host')
LOG.debug("Found hypervisor: %s", hypervisor_node)
try:
self.compute_model.get_node_by_name(host_name)
except exception.ComputeNodeNotFound:
LOG.info("The compute model does not contain the host: %s",
host_name)
continue
if (node.hv_up_when_powered_off and
hypervisor_node.get('state') != 'up'):
# filter nodes that are not in 'up' state
LOG.info("Ignoring node that isn't in 'up' state: %s",
host_name)
if not (hypervisor_node.get('state') == 'up'):
"""filter nodes that are not in 'up' state"""
continue
else:
if (hypervisor_node['running_vms'] == 0):
power_state = node.get_power_state()
if power_state == metal_constants.PowerState.ON:
if (node_info.power_state == 'power on'):
self.free_poweron_node_pool.append(node)
elif power_state == metal_constants.PowerState.OFF:
elif (node_info.power_state == 'power off'):
self.free_poweroff_node_pool.append(node)
else:
LOG.info("Ignoring node %s, unknown state: %s",
node, power_state)
else:
self.with_vms_node_pool.append(node)
def save_energy(self):
need_poweron = int(max(
need_poweron = max(
(len(self.with_vms_node_pool) * self.free_used_percent / 100), (
self.min_free_hosts_num)))
self.min_free_hosts_num))
len_poweron = len(self.free_poweron_node_pool)
len_poweroff = len(self.free_poweroff_node_pool)
LOG.debug("need_poweron: %s, len_poweron: %s, len_poweroff: %s",
need_poweron, len_poweron, len_poweroff)
if len_poweron > need_poweron:
for node in random.sample(self.free_poweron_node_pool,
(len_poweron - need_poweron)):
self.add_action_poweronoff_node(node,
metal_constants.PowerState.OFF)
LOG.info("power off %s", node.get_id())
self.add_action_poweronoff_node(node, 'off')
LOG.debug("power off %s", node.uuid)
elif len_poweron < need_poweron:
diff = need_poweron - len_poweron
for node in random.sample(self.free_poweroff_node_pool,
min(len_poweroff, diff)):
self.add_action_poweronoff_node(node,
metal_constants.PowerState.ON)
LOG.info("power on %s", node.get_id())
self.add_action_poweronoff_node(node, 'on')
LOG.debug("power on %s", node.uuid)
def pre_execute(self):
self._pre_execute()

View File

@@ -18,13 +18,9 @@
# limitations under the License.
#
import collections
from oslo_log import log
import oslo_utils
from watcher._i18n import _
from watcher.applier.actions import migration
from watcher.common import exception
from watcher.decision_engine.model import element
from watcher.decision_engine.strategy.strategies import base
@@ -70,8 +66,7 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
AGGREGATE = 'mean'
DATASOURCE_METRICS = ['instance_ram_allocated', 'instance_cpu_usage',
'instance_ram_usage', 'instance_root_disk_size',
'host_cpu_usage', 'host_ram_usage']
'instance_ram_usage', 'instance_root_disk_size']
MIGRATION = "migrate"
CHANGE_NOVA_SERVICE_STATE = "change_nova_service_state"
@@ -81,11 +76,6 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
self.number_of_migrations = 0
self.number_of_released_nodes = 0
self.datasource_instance_data_cache = dict()
self.datasource_node_data_cache = dict()
# Host metric adjustments that take into account planned
# migrations.
self.host_metric_delta = collections.defaultdict(
lambda: collections.defaultdict(int))
@classmethod
def get_name(cls):
@@ -206,12 +196,12 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
:return: None
"""
instance_state_str = self.get_instance_state_str(instance)
if instance_state_str in (element.InstanceState.ACTIVE.value,
element.InstanceState.PAUSED.value):
migration_type = migration.Migrate.LIVE_MIGRATION
elif instance_state_str == element.InstanceState.STOPPED.value:
migration_type = migration.Migrate.COLD_MIGRATION
else:
if instance_state_str not in (element.InstanceState.ACTIVE.value,
element.InstanceState.PAUSED.value):
# Watcher currently only supports live VM migration and block live
# VM migration which both requires migrated VM to be active.
# When supported, the cold migration may be used as a fallback
# migration mechanism to move non active VMs.
LOG.error(
'Cannot live migrate: instance_uuid=%(instance_uuid)s, '
'state=%(instance_state)s.', dict(
@@ -219,6 +209,8 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
instance_state=instance_state_str))
return
migration_type = 'live'
# Here will makes repeated actions to enable the same compute node,
# when migrating VMs to the destination node which is disabled.
# Whether should we remove the same actions in the solution???
@@ -236,18 +228,6 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
destination_node)
self.number_of_migrations += 1
instance_util = self.get_instance_utilization(instance)
self.host_metric_delta[source_node.hostname]['cpu'] -= (
instance_util['cpu'])
# We'll deduce the vm allocated memory.
self.host_metric_delta[source_node.hostname]['ram'] -= (
instance.memory)
self.host_metric_delta[destination_node.hostname]['cpu'] += (
instance_util['cpu'])
self.host_metric_delta[destination_node.hostname]['ram'] += (
instance.memory)
def disable_unused_nodes(self):
"""Generate actions for disabling unused nodes.
@@ -310,21 +290,6 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
disk=instance_disk_util)
return self.datasource_instance_data_cache.get(instance.uuid)
def _get_node_total_utilization(self, node):
if node.hostname in self.datasource_node_data_cache:
return self.datasource_node_data_cache[node.hostname]
cpu = self.datasource_backend.get_host_cpu_usage(
node, self.period, self.AGGREGATE,
self.granularity)
ram = self.datasource_backend.get_host_ram_usage(
node, self.period, self.AGGREGATE,
self.granularity)
self.datasource_node_data_cache[node.hostname] = dict(
cpu=cpu, ram=ram)
return self.datasource_node_data_cache[node.hostname]
def get_node_utilization(self, node):
"""Collect cpu, ram and disk utilization statistics of a node.
@@ -342,36 +307,8 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
node_cpu_util += instance_util['cpu']
node_ram_util += instance_util['ram']
node_disk_util += instance_util['disk']
LOG.debug("instance utilization: %s %s",
instance, instance_util)
total_node_util = self._get_node_total_utilization(node)
total_node_cpu_util = total_node_util['cpu'] or 0
if total_node_cpu_util:
total_node_cpu_util = total_node_cpu_util * node.vcpus / 100
# account for planned migrations
total_node_cpu_util += self.host_metric_delta[node.hostname]['cpu']
total_node_ram_util = total_node_util['ram'] or 0
if total_node_ram_util:
total_node_ram_util /= oslo_utils.units.Ki
total_node_ram_util += self.host_metric_delta[node.hostname]['ram']
LOG.debug(
"node utilization: %s. "
"total instance cpu: %s, "
"total instance ram: %s, "
"total instance disk: %s, "
"total host cpu: %s, "
"total host ram: %s, "
"node delta usage: %s.",
node,
node_cpu_util, node_ram_util, node_disk_util,
total_node_cpu_util, total_node_ram_util,
self.host_metric_delta[node.hostname])
return dict(cpu=max(node_cpu_util, total_node_cpu_util),
ram=max(node_ram_util, total_node_ram_util),
return dict(cpu=node_cpu_util, ram=node_ram_util,
disk=node_disk_util)
def get_node_capacity(self, node):
@@ -451,15 +388,8 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
instance_utilization = self.get_instance_utilization(instance)
metrics = ['cpu', 'ram', 'disk']
for m in metrics:
fits = (instance_utilization[m] + node_utilization[m] <=
node_capacity[m] * cc[m])
LOG.debug(
"Instance fits: %s, metric: %s, instance: %s, "
"node: %s, instance utilization: %s, "
"node utilization: %s, node capacity: %s, cc: %s",
fits, m, instance, node, instance_utilization[m],
node_utilization[m], node_capacity[m], cc[m])
if not fits:
if (instance_utilization[m] + node_utilization[m] >
node_capacity[m] * cc[m]):
return False
return True
@@ -494,9 +424,6 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
for a in actions:
self.solution.actions.remove(a)
self.number_of_migrations -= 1
LOG.info("Optimized migrations: %s. "
"Source: %s, destination: %s", actions,
src_name, dst_name)
src_node = self.compute_model.get_node_by_name(src_name)
dst_node = self.compute_model.get_node_by_name(dst_name)
instance = self.compute_model.get_instance_by_uuid(
@@ -533,8 +460,6 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
key=lambda x: self.get_instance_utilization(
x)['cpu']
):
LOG.info("Node %s overloaded, attempting to reduce load.",
node)
# skip exclude instance when migrating
if instance.watcher_exclude:
LOG.debug("Instance is excluded by scope, "
@@ -543,19 +468,11 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
for destination_node in reversed(sorted_nodes):
if self.instance_fits(
instance, destination_node, cc):
LOG.info("Offload: found fitting "
"destination (%s) for instance: %s. "
"Planning migration.",
destination_node, instance.uuid)
self.add_migration(instance, node,
destination_node)
break
if not self.is_overloaded(node, cc):
LOG.info("Node %s no longer overloaded.", node)
break
else:
LOG.info("Node still overloaded (%s), "
"continuing offload phase.", node)
def consolidation_phase(self, cc):
"""Perform consolidation phase.
@@ -591,10 +508,6 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
break
if self.instance_fits(
instance, destination_node, cc):
LOG.info("Consolidation: found fitting "
"destination (%s) for instance: %s. "
"Planning migration.",
destination_node, instance.uuid)
self.add_migration(instance, node,
destination_node)
break

View File

@@ -295,7 +295,7 @@ class WorkloadBalance(base.WorkloadStabilizationBaseStrategy):
self.threshold)
return self.solution
# choose the server with largest cpu usage
# choose the server with largest cpu_util
source_nodes = sorted(source_nodes,
reverse=True,
key=lambda x: (x[self._meter]))

View File

@@ -16,7 +16,7 @@ from dateutil.parser import parse
from oslo_log import log
from cinderclient.v3.volumes import Volume
from cinderclient.v2.volumes import Volume
from novaclient.v2.servers import Server
from watcher._i18n import _
from watcher.common import cinder_helper
@@ -57,19 +57,8 @@ class ZoneMigration(base.ZoneMigrationBaseStrategy):
self.planned_cold_count = 0
self.volume_count = 0
self.planned_volume_count = 0
# TODO(sean-n-mooney) This is backward compatibility
# for calling the swap code paths. Swap is now an alias
# for migrate, we should clean this up in a future
# cycle.
@property
def volume_update_count(self):
return self.volume_count
# same as above clean up later.
@property
def planned_volume_update_count(self):
return self.planned_volume_count
self.volume_update_count = 0
self.planned_volume_update_count = 0
@classmethod
def get_name(cls):
@@ -323,8 +312,8 @@ class ZoneMigration(base.ZoneMigrationBaseStrategy):
planned_cold_migrate_instance_count=self.planned_cold_count,
volume_migrate_count=self.volume_count,
planned_volume_migrate_count=self.planned_volume_count,
volume_update_count=self.volume_count,
planned_volume_update_count=self.planned_volume_count
volume_update_count=self.volume_update_count,
planned_volume_update_count=self.planned_volume_update_count
)
def set_migration_count(self, targets):
@@ -339,7 +328,10 @@ class ZoneMigration(base.ZoneMigrationBaseStrategy):
elif self.is_cold(instance):
self.cold_count += 1
for volume in targets.get('volume', []):
self.volume_count += 1
if self.is_available(volume):
self.volume_count += 1
elif self.is_in_use(volume):
self.volume_update_count += 1
def is_live(self, instance):
status = getattr(instance, 'status')
@@ -358,7 +350,7 @@ class ZoneMigration(base.ZoneMigrationBaseStrategy):
def is_in_use(self, volume):
return getattr(volume, 'status') == IN_USE
def instances_no_attached(self, instances):
def instances_no_attached(instances):
return [i for i in instances
if not getattr(i, "os-extended-volumes:volumes_attached")]
@@ -412,16 +404,19 @@ class ZoneMigration(base.ZoneMigrationBaseStrategy):
LOG.debug(src_type)
LOG.debug("%s %s", dst_pool, dst_type)
if src_type == dst_type:
self._volume_migrate(volume, dst_pool)
else:
self._volume_retype(volume, dst_type)
if self.is_available(volume):
if src_type == dst_type:
self._volume_migrate(volume, dst_pool)
else:
self._volume_retype(volume, dst_type)
elif self.is_in_use(volume):
self._volume_update(volume, dst_type)
# if with_attached_volume is True, migrate attaching instances
if self.with_attached_volume:
instances = [self.nova.find_instance(dic.get('server_id'))
for dic in volume.attachments]
self.instances_migration(instances, action_counter)
# if with_attached_volume is True, migrate attaching instances
if self.with_attached_volume:
instances = [self.nova.find_instance(dic.get('server_id'))
for dic in volume.attachments]
self.instances_migration(instances, action_counter)
action_counter.add_pool(pool)
@@ -469,6 +464,16 @@ class ZoneMigration(base.ZoneMigrationBaseStrategy):
input_parameters=parameters)
self.planned_cold_count += 1
def _volume_update(self, volume, dst_type):
parameters = {"migration_type": "swap",
"destination_type": dst_type,
"resource_name": volume.name}
self.solution.add_action(
action_type="volume_migrate",
resource_id=volume.id,
input_parameters=parameters)
self.planned_volume_update_count += 1
def _volume_migrate(self, volume, dst_pool):
parameters = {"migration_type": "migrate",
"destination_node": dst_pool,

View File

@@ -128,20 +128,22 @@ def check_assert_called_once_with(logical_line, filename):
@flake8ext
def check_python3_xrange(logical_line):
if re.search(r"\bxrange\s*\(", logical_line):
yield(0, "N325: Do not use xrange. Use range for large loops.")
yield(0, "N325: Do not use xrange. Use range, or six.moves.range for "
"large loops.")
@flake8ext
def check_no_basestring(logical_line):
if re.search(r"\bbasestring\b", logical_line):
msg = ("N326: basestring is not Python3-compatible, use str instead.")
msg = ("N326: basestring is not Python3-compatible, use "
"six.string_types instead.")
yield(0, msg)
@flake8ext
def check_python3_no_iteritems(logical_line):
if re.search(r".*\.iteritems\(\)", logical_line):
msg = ("N327: Use dict.items() instead of dict.iteritems().")
msg = ("N327: Use six.iteritems() instead of dict.iteritems().")
yield(0, msg)

View File

@@ -1,16 +1,14 @@
# Andi Chandler <andi@gowling.com>, 2017. #zanata
# Andi Chandler <andi@gowling.com>, 2018. #zanata
# Andi Chandler <andi@gowling.com>, 2020. #zanata
# Andi Chandler <andi@gowling.com>, 2022. #zanata
msgid ""
msgstr ""
"Project-Id-Version: watcher VERSION\n"
"Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n"
"POT-Creation-Date: 2022-08-29 03:03+0000\n"
"POT-Creation-Date: 2020-04-26 02:09+0000\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"PO-Revision-Date: 2022-05-31 08:38+0000\n"
"PO-Revision-Date: 2018-11-07 06:14+0000\n"
"Last-Translator: Andi Chandler <andi@gowling.com>\n"
"Language-Team: English (United Kingdom)\n"
"Language: en_GB\n"
@@ -189,18 +187,10 @@ msgstr "Audit Templates"
msgid "Audit parameter %(parameter)s are not allowed"
msgstr "Audit parameter %(parameter)s are not allowed"
#, python-format
msgid "Audit state %(state)s is disallowed."
msgstr "Audit state %(state)s is disallowed."
#, python-format
msgid "Audit type %(audit_type)s could not be found"
msgstr "Audit type %(audit_type)s could not be found"
#, python-format
msgid "Audit type %(audit_type)s is disallowed."
msgstr "Audit type %(audit_type)s is disallowed."
#, python-format
msgid "AuditTemplate %(audit_template)s could not be found"
msgstr "AuditTemplate %(audit_template)s could not be found"
@@ -253,9 +243,6 @@ msgstr "Cannot overwrite UUID for an existing efficacy indicator."
msgid "Cannot remove 'goal' attribute from an audit template"
msgstr "Cannot remove 'goal' attribute from an audit template"
msgid "Ceilometer helper does not support statistic series method"
msgstr "Ceilometer helper does not support statistic series method"
msgid "Cluster Maintaining"
msgstr "Cluster Maintaining"
@@ -382,9 +369,6 @@ msgstr "Goal %(goal)s is invalid"
msgid "Goals"
msgstr "Goals"
msgid "Grafana helper does not support statistic series method"
msgstr "Grafana helper does not support statistic series method"
msgid "Hardware Maintenance"
msgstr "Hardware Maintenance"
@@ -450,17 +434,10 @@ msgstr "Limit should be positive"
msgid "Maximum time since last check-in for up service."
msgstr "Maximum time since last check-in for up service."
#, python-format
msgid "Metric: %(metric)s not available"
msgstr "Metric: %(metric)s not available"
#, python-format
msgid "Migration of type '%(migration_type)s' is not supported."
msgstr "Migration of type '%(migration_type)s' is not supported."
msgid "Minimum Nova API Version"
msgstr "Minimum Nova API Version"
msgid ""
"Name of this node. This can be an opaque identifier. It is not necessarily a "
"hostname, FQDN, or IP address. However, the node name must be valid within "
@@ -474,16 +451,10 @@ msgstr ""
msgid "No %(metric)s metric for %(host)s found."
msgstr "No %(metric)s metric for %(host)s found."
msgid "No datasources available"
msgstr "No datasources available"
#, python-format
msgid "No strategy could be found to achieve the '%(goal)s' goal."
msgstr "No strategy could be found to achieve the '%(goal)s' goal."
msgid "Node Resource Consolidation strategy"
msgstr "Node Resource Consolidation strategy"
msgid "Noisy Neighbor"
msgstr "Noisy Neighbour"
@@ -508,9 +479,6 @@ msgstr ""
msgid "Plugins"
msgstr "Plugins"
msgid "Policy File JSON to YAML Migration"
msgstr "Policy File JSON to YAML Migration"
#, python-format
msgid "Policy doesn't allow %(action)s to be performed."
msgstr "Policy doesn't allow %(action)s to be performed."
@@ -638,10 +606,6 @@ msgstr "Strategy %(strategy)s could not be found"
msgid "Strategy %(strategy)s is invalid"
msgstr "Strategy %(strategy)s is invalid"
#, python-format
msgid "The %(data_model_type)s data model could not be found"
msgstr "The %(data_model_type)s data model could not be found"
#, python-format
msgid "The %(name)s %(id)s could not be found"
msgstr "The %(name)s %(id)s could not be found"
@@ -711,13 +675,6 @@ msgstr "The instance '%(name)s' could not be found"
msgid "The ironic node %(uuid)s could not be found"
msgstr "The Ironic node %(uuid)s could not be found"
#, python-format
msgid "The mapped compute node for instance '%(uuid)s' could not be found."
msgstr "The mapped compute node for instance '%(uuid)s' could not be found."
msgid "The node status is not defined"
msgstr "The node status is not defined"
msgid "The number of VM migrations to be performed."
msgstr "The number of VM migrations to be performed."
@@ -781,10 +738,6 @@ msgstr "The total number of audited instances in strategy."
msgid "The total number of enabled compute nodes."
msgstr "The total number of enabled compute nodes."
#, python-format
msgid "The value %(value)s for parameter %(parameter)s is invalid"
msgstr "The value %(value)s for parameter %(parameter)s is invalid"
msgid "The value of original standard deviation."
msgstr "The value of original standard deviation."

View File

@@ -13,8 +13,6 @@
# License for the specific language governing permissions and limitations
# under the License.
from http import HTTPStatus
from watcher.tests.api import base
@@ -27,6 +25,6 @@ class TestBase(base.FunctionalTest):
response = self.get_json('/bad/path',
expect_errors=True,
headers={"Accept": "application/json"})
self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
self.assertEqual(404, response.status_int)
self.assertEqual("application/json", response.content_type)
self.assertTrue(response.json['error_message'])

View File

@@ -14,11 +14,14 @@
"""Tests for the Pecan API hooks."""
from http import client as http_client
from unittest import mock
from oslo_config import cfg
import oslo_messaging as messaging
from oslo_serialization import jsonutils
from unittest import mock
import six
from six.moves import http_client
from watcher.api.controllers import root
from watcher.api import hooks
from watcher.common import context
@@ -141,7 +144,7 @@ class TestNoExceptionTracebackHook(base.FunctionalTest):
# we don't care about this garbage.
expected_msg = ("Remote error: %s %s"
% (test_exc_type, self.MSG_WITHOUT_TRACE) +
"\n['")
("\n[u'" if six.PY2 else "\n['"))
actual_msg = jsonutils.loads(
response.json['error_message'])['faultstring']
self.assertEqual(expected_msg, actual_msg)

View File

@@ -14,7 +14,6 @@ import datetime
import itertools
from unittest import mock
from http import HTTPStatus
from oslo_config import cfg
from oslo_serialization import jsonutils
from wsme import types as wtypes
@@ -103,7 +102,7 @@ class TestListAction(api_base.FunctionalTest):
response = self.get_json('/actions/%s' % action['uuid'],
expect_errors=True)
self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
self.assertEqual(404, response.status_int)
def test_detail(self):
action = obj_utils.create_test_action(self.context, parents=None)
@@ -126,7 +125,7 @@ class TestListAction(api_base.FunctionalTest):
action = obj_utils.create_test_action(self.context, parents=None)
response = self.get_json('/actions/%s/detail' % action['uuid'],
expect_errors=True)
self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
self.assertEqual(404, response.status_int)
def test_many(self):
action_list = []
@@ -267,7 +266,7 @@ class TestListAction(api_base.FunctionalTest):
url = '/actions?action_plan_uuid=%s&audit_uuid=%s' % (
action_plan.uuid, self.audit.uuid)
response = self.get_json(url, expect_errors=True)
self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual(400, response.status_int)
def test_many_with_sort_key_uuid(self):
action_plan = obj_utils.create_test_action_plan(
@@ -328,7 +327,7 @@ class TestListAction(api_base.FunctionalTest):
response = self.get_json(
'/actions?sort_key=%s' % 'bad_name',
expect_errors=True)
self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual(400, response.status_int)
def test_many_with_soft_deleted_action_plan_uuid(self):
action_plan1 = obj_utils.create_test_action_plan(
@@ -489,7 +488,7 @@ class TestPatch(api_base.FunctionalTest):
[{'path': '/state', 'value': new_state, 'op': 'replace'}],
expect_errors=True)
self.assertEqual('application/json', response.content_type)
self.assertEqual(HTTPStatus.FORBIDDEN, response.status_int)
self.assertEqual(403, response.status_int)
self.assertTrue(response.json['error_message'])
@@ -517,7 +516,7 @@ class TestDelete(api_base.FunctionalTest):
mock_utcnow.return_value = test_time
response = self.delete('/actions/%s' % self.action.uuid,
expect_errors=True)
self.assertEqual(HTTPStatus.FORBIDDEN, response.status_int)
self.assertEqual(403, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
@@ -537,7 +536,7 @@ class TestActionPolicyEnforcement(api_base.FunctionalTest):
"default": "rule:admin_api",
rule: "rule:defaut"})
response = func(*arg, **kwarg)
self.assertEqual(HTTPStatus.FORBIDDEN, response.status_int)
self.assertEqual(403, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(
"Policy doesn't allow %s to be performed." % rule,

View File

@@ -14,7 +14,6 @@ import datetime
import itertools
from unittest import mock
from http import HTTPStatus
from oslo_config import cfg
from oslo_serialization import jsonutils
@@ -88,7 +87,7 @@ class TestListActionPlan(api_base.FunctionalTest):
response = self.get_json('/action_plans/%s' % action_plan['uuid'],
expect_errors=True)
self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
self.assertEqual(404, response.status_int)
def test_detail(self):
action_plan = obj_utils.create_test_action_plan(self.context)
@@ -114,7 +113,7 @@ class TestListActionPlan(api_base.FunctionalTest):
response = self.get_json(
'/action_plan/%s/detail' % action_plan['uuid'],
expect_errors=True)
self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
self.assertEqual(404, response.status_int)
def test_many(self):
action_plan_list = []
@@ -261,7 +260,7 @@ class TestListActionPlan(api_base.FunctionalTest):
response = self.get_json(
'/action_plans?sort_key=%s' % 'bad_name',
expect_errors=True)
self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual(400, response.status_int)
def test_links(self):
uuid = utils.generate_uuid()
@@ -318,7 +317,7 @@ class TestDelete(api_base.FunctionalTest):
def test_delete_action_plan_without_action(self):
response = self.delete('/action_plans/%s' % self.action_plan.uuid,
expect_errors=True)
self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual(400, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
self.action_plan.state = objects.action_plan.State.SUCCEEDED
@@ -326,7 +325,7 @@ class TestDelete(api_base.FunctionalTest):
self.delete('/action_plans/%s' % self.action_plan.uuid)
response = self.get_json('/action_plans/%s' % self.action_plan.uuid,
expect_errors=True)
self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
self.assertEqual(404, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
@@ -346,20 +345,20 @@ class TestDelete(api_base.FunctionalTest):
expect_errors=True)
# The action plan does not exist anymore
self.assertEqual(HTTPStatus.NOT_FOUND, ap_response.status_int)
self.assertEqual(404, ap_response.status_int)
self.assertEqual('application/json', ap_response.content_type)
self.assertTrue(ap_response.json['error_message'])
# Nor does the action
self.assertEqual(0, len(acts_response['actions']))
self.assertEqual(HTTPStatus.NOT_FOUND, act_response.status_int)
self.assertEqual(404, act_response.status_int)
self.assertEqual('application/json', act_response.content_type)
self.assertTrue(act_response.json['error_message'])
def test_delete_action_plan_not_found(self):
uuid = utils.generate_uuid()
response = self.delete('/action_plans/%s' % uuid, expect_errors=True)
self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
self.assertEqual(404, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
@@ -389,7 +388,7 @@ class TestStart(api_base.FunctionalTest):
uuid = utils.generate_uuid()
response = self.post('/v1/action_plans/%s/%s' %
(uuid, 'start'), expect_errors=True)
self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
self.assertEqual(404, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
@@ -402,11 +401,11 @@ class TestStart(api_base.FunctionalTest):
response = self.post('/v1/action_plans/%s/%s/'
% (self.action_plan.uuid, 'start'),
expect_errors=True)
self.assertEqual(HTTPStatus.OK, response.status_int)
self.assertEqual(200, response.status_int)
act_response = self.get_json(
'/actions/%s' % action.uuid,
expect_errors=True)
self.assertEqual(HTTPStatus.OK, act_response.status_int)
self.assertEqual(200, act_response.status_int)
self.assertEqual('PENDING', act_response.json['state'])
self.assertEqual('application/json', act_response.content_type)
@@ -446,7 +445,7 @@ class TestPatch(api_base.FunctionalTest):
expect_errors=True)
self.assertEqual('application/json', response.content_type)
self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual(400, response.status_int)
self.assertTrue(response.json['error_message'])
def test_replace_non_existent_action_plan_denied(self):
@@ -456,7 +455,7 @@ class TestPatch(api_base.FunctionalTest):
'value': objects.action_plan.State.PENDING,
'op': 'replace'}],
expect_errors=True)
self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
self.assertEqual(404, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
@@ -466,7 +465,7 @@ class TestPatch(api_base.FunctionalTest):
[{'path': '/foo', 'value': 'bar', 'op': 'add'}],
expect_errors=True)
self.assertEqual('application/json', response.content_type)
self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual(400, response.status_int)
self.assertTrue(response.json['error_message'])
def test_remove_denied(self):
@@ -481,7 +480,7 @@ class TestPatch(api_base.FunctionalTest):
expect_errors=True)
self.assertEqual('application/json', response.content_type)
self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual(400, response.status_int)
self.assertTrue(response.json['error_message'])
def test_remove_uuid_denied(self):
@@ -489,7 +488,7 @@ class TestPatch(api_base.FunctionalTest):
'/action_plans/%s' % self.action_plan.uuid,
[{'path': '/uuid', 'op': 'remove'}],
expect_errors=True)
self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual(400, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
@@ -498,7 +497,7 @@ class TestPatch(api_base.FunctionalTest):
'/action_plans/%s' % self.action_plan.uuid,
[{'path': '/non-existent', 'op': 'remove'}],
expect_errors=True)
self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_code)
self.assertEqual(400, response.status_code)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
@@ -513,7 +512,7 @@ class TestPatch(api_base.FunctionalTest):
[{'path': '/state', 'value': new_state,
'op': 'replace'}])
self.assertEqual('application/json', response.content_type)
self.assertEqual(HTTPStatus.OK, response.status_code)
self.assertEqual(200, response.status_code)
applier_mock.assert_called_once_with(mock.ANY,
self.action_plan.uuid)
@@ -580,7 +579,7 @@ class TestPatchStateTransitionDenied(api_base.FunctionalTest):
self.assertNotEqual(self.new_state, initial_ap['state'])
self.assertEqual(self.original_state, updated_ap['state'])
self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_code)
self.assertEqual(400, response.status_code)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
@@ -619,7 +618,7 @@ class TestPatchStateTransitionOk(api_base.FunctionalTest):
self.assertNotEqual(self.new_state, initial_ap['state'])
self.assertEqual(self.new_state, updated_ap['state'])
self.assertEqual('application/json', response.content_type)
self.assertEqual(HTTPStatus.OK, response.status_code)
self.assertEqual(200, response.status_code)
class TestActionPlanPolicyEnforcement(api_base.FunctionalTest):
@@ -636,7 +635,7 @@ class TestActionPlanPolicyEnforcement(api_base.FunctionalTest):
"default": "rule:admin_api",
rule: "rule:defaut"})
response = func(*arg, **kwarg)
self.assertEqual(HTTPStatus.FORBIDDEN, response.status_int)
self.assertEqual(403, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(
"Policy doesn't allow %s to be performed." % rule,

View File

@@ -15,7 +15,6 @@ import itertools
from unittest import mock
from urllib import parse as urlparse
from http import HTTPStatus
from oslo_config import cfg
from oslo_serialization import jsonutils
from oslo_utils import timeutils
@@ -127,7 +126,7 @@ class TestListAuditTemplate(FunctionalTestWithSetup):
response = self.get_json(
'/audit_templates/%s' % audit_template['uuid'],
expect_errors=True)
self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
self.assertEqual(404, response.status_int)
def test_detail(self):
audit_template = obj_utils.create_test_audit_template(self.context)
@@ -153,7 +152,7 @@ class TestListAuditTemplate(FunctionalTestWithSetup):
response = self.get_json(
'/audit_templates/%s/detail' % audit_template['uuid'],
expect_errors=True)
self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
self.assertEqual(404, response.status_int)
def test_many(self):
audit_template_list = []
@@ -337,7 +336,7 @@ class TestListAuditTemplate(FunctionalTestWithSetup):
response = self.get_json(
'/audit_templates?sort_key=%s' % 'goal_bad_name',
expect_errors=True)
self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual(400, response.status_int)
class TestPatch(FunctionalTestWithSetup):
@@ -363,7 +362,7 @@ class TestPatch(FunctionalTestWithSetup):
[{'path': '/goal', 'value': new_goal_uuid,
'op': 'replace'}])
self.assertEqual('application/json', response.content_type)
self.assertEqual(HTTPStatus.OK, response.status_code)
self.assertEqual(200, response.status_code)
response = self.get_json(
'/audit_templates/%s' % self.audit_template.uuid)
@@ -387,7 +386,7 @@ class TestPatch(FunctionalTestWithSetup):
[{'path': '/goal', 'value': new_goal_uuid,
'op': 'replace'}])
self.assertEqual('application/json', response.content_type)
self.assertEqual(HTTPStatus.OK, response.status_code)
self.assertEqual(200, response.status_code)
response = self.get_json(
'/audit_templates/%s' % self.audit_template.name)
@@ -402,7 +401,7 @@ class TestPatch(FunctionalTestWithSetup):
[{'path': '/goal', 'value': self.fake_goal1.uuid,
'op': 'replace'}],
expect_errors=True)
self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
self.assertEqual(404, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
@@ -417,7 +416,7 @@ class TestPatch(FunctionalTestWithSetup):
[{'path': '/goal', 'value': utils.generate_uuid(),
'op': 'replace'}],
expect_errors=True)
self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual(400, response.status_int)
assert not cn_mock.called
def test_add_goal_uuid(self):
@@ -427,7 +426,7 @@ class TestPatch(FunctionalTestWithSetup):
'value': self.fake_goal2.uuid,
'op': 'add'}])
self.assertEqual('application/json', response.content_type)
self.assertEqual(HTTPStatus.OK, response.status_int)
self.assertEqual(200, response.status_int)
response = self.get_json(
'/audit_templates/%s' % self.audit_template.uuid)
@@ -440,7 +439,7 @@ class TestPatch(FunctionalTestWithSetup):
'value': self.fake_strategy1.uuid,
'op': 'add'}])
self.assertEqual('application/json', response.content_type)
self.assertEqual(HTTPStatus.OK, response.status_int)
self.assertEqual(200, response.status_int)
response = self.get_json(
'/audit_templates/%s' % self.audit_template.uuid)
@@ -453,7 +452,7 @@ class TestPatch(FunctionalTestWithSetup):
'value': self.fake_strategy2['uuid'],
'op': 'replace'}])
self.assertEqual('application/json', response.content_type)
self.assertEqual(HTTPStatus.OK, response.status_int)
self.assertEqual(200, response.status_int)
response = self.get_json(
'/audit_templates/%s' % self.audit_template.uuid)
@@ -467,7 +466,7 @@ class TestPatch(FunctionalTestWithSetup):
'value': utils.generate_uuid(), # Does not exist
'op': 'replace'}], expect_errors=True)
self.assertEqual('application/json', response.content_type)
self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual(400, response.status_int)
self.assertTrue(response.json['error_message'])
def test_add_non_existent_property(self):
@@ -476,7 +475,7 @@ class TestPatch(FunctionalTestWithSetup):
[{'path': '/foo', 'value': 'bar', 'op': 'add'}],
expect_errors=True)
self.assertEqual('application/json', response.content_type)
self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual(400, response.status_int)
self.assertTrue(response.json['error_message'])
def test_remove_strategy(self):
@@ -493,7 +492,7 @@ class TestPatch(FunctionalTestWithSetup):
'/audit_templates/%s' % self.audit_template.uuid,
[{'path': '/strategy', 'op': 'remove'}])
self.assertEqual('application/json', response.content_type)
self.assertEqual(HTTPStatus.OK, response.status_code)
self.assertEqual(200, response.status_code)
def test_remove_goal(self):
response = self.get_json(
@@ -504,7 +503,7 @@ class TestPatch(FunctionalTestWithSetup):
'/audit_templates/%s' % self.audit_template.uuid,
[{'path': '/goal', 'op': 'remove'}],
expect_errors=True)
self.assertEqual(HTTPStatus.FORBIDDEN, response.status_code)
self.assertEqual(403, response.status_code)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
@@ -513,7 +512,7 @@ class TestPatch(FunctionalTestWithSetup):
'/audit_templates/%s' % self.audit_template.uuid,
[{'path': '/uuid', 'op': 'remove'}],
expect_errors=True)
self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual(400, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
@@ -522,7 +521,7 @@ class TestPatch(FunctionalTestWithSetup):
'/audit_templates/%s' % self.audit_template.uuid,
[{'path': '/non-existent', 'op': 'remove'}],
expect_errors=True)
self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_code)
self.assertEqual(400, response.status_code)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
@@ -539,7 +538,7 @@ class TestPost(FunctionalTestWithSetup):
response = self.post_json('/audit_templates', audit_template_dict)
self.assertEqual('application/json', response.content_type)
self.assertEqual(HTTPStatus.CREATED, response.status_int)
self.assertEqual(201, response.status_int)
# Check location header
self.assertIsNotNone(response.location)
expected_location = \
@@ -566,7 +565,7 @@ class TestPost(FunctionalTestWithSetup):
response = self.post_json('/audit_templates', audit_template_dict)
self.assertEqual('application/json', response.content_type)
self.assertEqual(HTTPStatus.CREATED, response.status_int)
self.assertEqual(201, response.status_int)
# Check location header
self.assertIsNotNone(response.location)
expected_location = \
@@ -614,8 +613,7 @@ class TestPost(FunctionalTestWithSetup):
strategy=self.fake_strategy1.uuid, scope=scope)
response = self.post_json('/audit_templates',
audit_template_dict, expect_errors=True)
self.assertEqual(HTTPStatus.INTERNAL_SERVER_ERROR,
response.status_int)
self.assertEqual(500, response.status_int)
def test_create_audit_template_does_autogenerate_id(self):
audit_template_dict = post_get_test_audit_template(
@@ -637,7 +635,7 @@ class TestPost(FunctionalTestWithSetup):
response = self.post_json('/audit_templates', audit_template_dict)
self.assertEqual('application/json', response.content_type)
self.assertEqual(HTTPStatus.CREATED, response.status_int)
self.assertEqual(201, response.status_int)
self.assertTrue(utils.is_uuid_like(response.json['uuid']))
def test_create_audit_template_with_invalid_goal(self):
@@ -650,7 +648,7 @@ class TestPost(FunctionalTestWithSetup):
goal_uuid=utils.generate_uuid())
response = self.post_json('/audit_templates',
audit_template_dict, expect_errors=True)
self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual(400, response.status_int)
assert not cn_mock.called
def test_create_audit_template_with_invalid_strategy(self):
@@ -664,7 +662,7 @@ class TestPost(FunctionalTestWithSetup):
strategy_uuid=utils.generate_uuid())
response = self.post_json('/audit_templates',
audit_template_dict, expect_errors=True)
self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual(400, response.status_int)
assert not cn_mock.called
def test_create_audit_template_with_unrelated_strategy(self):
@@ -678,7 +676,7 @@ class TestPost(FunctionalTestWithSetup):
strategy=self.fake_strategy2['uuid'])
response = self.post_json('/audit_templates',
audit_template_dict, expect_errors=True)
self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual(400, response.status_int)
assert not cn_mock.called
def test_create_audit_template_with_uuid(self):
@@ -691,7 +689,7 @@ class TestPost(FunctionalTestWithSetup):
response = self.post_json('/audit_templates', audit_template_dict,
expect_errors=True)
self.assertEqual('application/json', response.content_type)
self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual(400, response.status_int)
assert not cn_mock.called
def test_create_audit_template_with_old_scope(self):
@@ -712,7 +710,7 @@ class TestPost(FunctionalTestWithSetup):
strategy=self.fake_strategy1.uuid, scope=scope)
response = self.post_json('/audit_templates',
audit_template_dict)
self.assertEqual(HTTPStatus.CREATED, response.status_int)
self.assertEqual(201, response.status_int)
class TestDelete(api_base.FunctionalTest):
@@ -732,7 +730,7 @@ class TestDelete(api_base.FunctionalTest):
response = self.get_json(
urlparse.quote('/audit_templates/%s' % self.audit_template.uuid),
expect_errors=True)
self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
self.assertEqual(404, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
@@ -755,7 +753,7 @@ class TestDelete(api_base.FunctionalTest):
response = self.get_json(
urlparse.quote('/audit_templates/%s' % self.audit_template.name),
expect_errors=True)
self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
self.assertEqual(404, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
@@ -773,7 +771,7 @@ class TestDelete(api_base.FunctionalTest):
uuid = utils.generate_uuid()
response = self.delete(
'/audit_templates/%s' % uuid, expect_errors=True)
self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
self.assertEqual(404, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
@@ -786,7 +784,7 @@ class TestAuditTemplatePolicyEnforcement(api_base.FunctionalTest):
"default": "rule:admin_api",
rule: "rule:defaut"})
response = func(*arg, **kwarg)
self.assertEqual(HTTPStatus.FORBIDDEN, response.status_int)
self.assertEqual(403, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(
"Policy doesn't allow %s to be performed." % rule,

View File

@@ -16,7 +16,6 @@ import itertools
from unittest import mock
from urllib import parse as urlparse
from http import HTTPStatus
from oslo_config import cfg
from oslo_serialization import jsonutils
from oslo_utils import timeutils
@@ -131,7 +130,7 @@ class TestListAudit(api_base.FunctionalTest):
response = self.get_json('/audits/%s' % audit['uuid'],
expect_errors=True)
self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
self.assertEqual(404, response.status_int)
def test_detail(self):
audit = obj_utils.create_test_audit(self.context)
@@ -154,7 +153,7 @@ class TestListAudit(api_base.FunctionalTest):
audit = obj_utils.create_test_audit(self.context)
response = self.get_json('/audits/%s/detail' % audit['uuid'],
expect_errors=True)
self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
self.assertEqual(404, response.status_int)
def test_many(self):
audit_list = []
@@ -226,7 +225,7 @@ class TestListAudit(api_base.FunctionalTest):
response = self.get_json(
'/audits?sort_key=%s' % 'bad_name',
expect_errors=True)
self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual(400, response.status_int)
def test_links(self):
uuid = utils.generate_uuid()
@@ -296,7 +295,7 @@ class TestPatch(api_base.FunctionalTest):
[{'path': '/state', 'value': new_state,
'op': 'replace'}])
self.assertEqual('application/json', response.content_type)
self.assertEqual(HTTPStatus.OK, response.status_code)
self.assertEqual(200, response.status_code)
response = self.get_json('/audits/%s' % self.audit.uuid)
self.assertEqual(new_state, response['state'])
@@ -309,7 +308,7 @@ class TestPatch(api_base.FunctionalTest):
'/audits/%s' % utils.generate_uuid(),
[{'path': '/state', 'value': objects.audit.State.SUCCEEDED,
'op': 'replace'}], expect_errors=True)
self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
self.assertEqual(404, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
@@ -319,7 +318,7 @@ class TestPatch(api_base.FunctionalTest):
'/audits/%s' % self.audit.uuid,
[{'path': '/state', 'value': new_state, 'op': 'add'}])
self.assertEqual('application/json', response.content_type)
self.assertEqual(HTTPStatus.OK, response.status_int)
self.assertEqual(200, response.status_int)
response = self.get_json('/audits/%s' % self.audit.uuid)
self.assertEqual(new_state, response['state'])
@@ -330,7 +329,7 @@ class TestPatch(api_base.FunctionalTest):
[{'path': '/foo', 'value': 'bar', 'op': 'add'}],
expect_errors=True)
self.assertEqual('application/json', response.content_type)
self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual(400, response.status_int)
self.assertTrue(response.json['error_message'])
def test_remove_ok(self):
@@ -340,7 +339,7 @@ class TestPatch(api_base.FunctionalTest):
response = self.patch_json('/audits/%s' % self.audit.uuid,
[{'path': '/interval', 'op': 'remove'}])
self.assertEqual('application/json', response.content_type)
self.assertEqual(HTTPStatus.OK, response.status_code)
self.assertEqual(200, response.status_code)
response = self.get_json('/audits/%s' % self.audit.uuid)
self.assertIsNone(response['interval'])
@@ -349,7 +348,7 @@ class TestPatch(api_base.FunctionalTest):
response = self.patch_json('/audits/%s' % self.audit.uuid,
[{'path': '/uuid', 'op': 'remove'}],
expect_errors=True)
self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual(400, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
@@ -358,7 +357,7 @@ class TestPatch(api_base.FunctionalTest):
'/audits/%s' % self.audit.uuid,
[{'path': '/non-existent', 'op': 'remove'}],
expect_errors=True)
self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_code)
self.assertEqual(400, response.status_code)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
@@ -416,7 +415,7 @@ class TestPatchStateTransitionDenied(api_base.FunctionalTest):
'op': 'replace'}],
expect_errors=True)
self.assertEqual('application/json', response.content_type)
self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_code)
self.assertEqual(400, response.status_code)
self.assertTrue(response.json['error_message'])
response = self.get_json('/audits/%s' % self.audit.uuid)
@@ -463,7 +462,7 @@ class TestPatchStateTransitionOk(api_base.FunctionalTest):
[{'path': '/state', 'value': self.new_state,
'op': 'replace'}])
self.assertEqual('application/json', response.content_type)
self.assertEqual(HTTPStatus.OK, response.status_code)
self.assertEqual(200, response.status_code)
response = self.get_json('/audits/%s' % self.audit.uuid)
self.assertEqual(self.new_state, response['state'])
@@ -503,7 +502,7 @@ class TestPost(api_base.FunctionalTest):
response = self.post_json('/audits', audit_dict)
self.assertEqual('application/json', response.content_type)
self.assertEqual(HTTPStatus.CREATED, response.status_int)
self.assertEqual(201, response.status_int)
# Check location header
self.assertIsNotNone(response.location)
expected_location = '/v1/audits/%s' % response.json['uuid']
@@ -528,7 +527,7 @@ class TestPost(api_base.FunctionalTest):
audit_dict = post_get_test_audit(state=objects.audit.State.SUCCEEDED)
response = self.post_json('/audits', audit_dict, expect_errors=True)
self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual(400, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
@@ -543,7 +542,7 @@ class TestPost(api_base.FunctionalTest):
'next_run_time', 'hostname'])
response = self.post_json('/audits', audit_dict, expect_errors=True)
self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual(400, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
@@ -558,7 +557,7 @@ class TestPost(api_base.FunctionalTest):
response = self.post_json('/audits', audit_dict)
self.assertEqual('application/json', response.content_type)
self.assertEqual(HTTPStatus.CREATED, response.status_int)
self.assertEqual(201, response.status_int)
self.assertEqual(objects.audit.State.PENDING,
response.json['state'])
self.assertTrue(utils.is_uuid_like(response.json['uuid']))
@@ -574,7 +573,7 @@ class TestPost(api_base.FunctionalTest):
response = self.post_json('/audits', audit_dict)
self.assertEqual('application/json', response.content_type)
self.assertEqual(HTTPStatus.CREATED, response.status_int)
self.assertEqual(201, response.status_int)
self.assertEqual(objects.audit.State.PENDING,
response.json['state'])
self.assertTrue(utils.is_uuid_like(response.json['uuid']))
@@ -591,7 +590,7 @@ class TestPost(api_base.FunctionalTest):
response = self.post_json('/audits', audit_dict)
self.assertEqual('application/json', response.content_type)
self.assertEqual(HTTPStatus.CREATED, response.status_int)
self.assertEqual(201, response.status_int)
self.assertEqual(objects.audit.State.PENDING,
response.json['state'])
self.assertTrue(utils.is_uuid_like(response.json['uuid']))
@@ -609,7 +608,7 @@ class TestPost(api_base.FunctionalTest):
'01234567-8910-1112-1314-151617181920')
response = self.post_json('/audits', audit_dict, expect_errors=True)
self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual(400, response.status_int)
self.assertEqual("application/json", response.content_type)
expected_error_msg = ('The audit template UUID or name specified is '
'invalid')
@@ -644,7 +643,7 @@ class TestPost(api_base.FunctionalTest):
response = self.post_json('/audits', audit_dict)
self.assertEqual('application/json', response.content_type)
self.assertEqual(HTTPStatus.CREATED, response.status_int)
self.assertEqual(201, response.status_int)
self.assertEqual(objects.audit.State.PENDING,
response.json['state'])
self.assertTrue(utils.is_uuid_like(response.json['uuid']))
@@ -661,7 +660,7 @@ class TestPost(api_base.FunctionalTest):
response = self.post_json('/audits', audit_dict)
self.assertEqual('application/json', response.content_type)
self.assertEqual(HTTPStatus.CREATED, response.status_int)
self.assertEqual(201, response.status_int)
self.assertEqual(objects.audit.State.PENDING,
response.json['state'])
self.assertEqual(audit_dict['interval'], response.json['interval'])
@@ -680,7 +679,7 @@ class TestPost(api_base.FunctionalTest):
response = self.post_json('/audits', audit_dict)
self.assertEqual('application/json', response.content_type)
self.assertEqual(HTTPStatus.CREATED, response.status_int)
self.assertEqual(201, response.status_int)
self.assertEqual(objects.audit.State.PENDING,
response.json['state'])
self.assertEqual(audit_dict['interval'], response.json['interval'])
@@ -699,9 +698,9 @@ class TestPost(api_base.FunctionalTest):
response = self.post_json('/audits', audit_dict, expect_errors=True)
self.assertEqual('application/json', response.content_type)
self.assertEqual(HTTPStatus.INTERNAL_SERVER_ERROR, response.status_int)
self.assertEqual(500, response.status_int)
expected_error_msg = ('Exactly 5 or 6 columns has to be '
'specified for iterator expression.')
'specified for iteratorexpression.')
self.assertTrue(response.json['error_message'])
self.assertIn(expected_error_msg, response.json['error_message'])
@@ -715,7 +714,7 @@ class TestPost(api_base.FunctionalTest):
audit_dict['audit_type'] = objects.audit.AuditType.CONTINUOUS.value
response = self.post_json('/audits', audit_dict, expect_errors=True)
self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual(400, response.status_int)
self.assertEqual('application/json', response.content_type)
expected_error_msg = ('Interval of audit must be specified '
'for CONTINUOUS.')
@@ -732,7 +731,7 @@ class TestPost(api_base.FunctionalTest):
audit_dict['audit_type'] = objects.audit.AuditType.ONESHOT.value
response = self.post_json('/audits', audit_dict, expect_errors=True)
self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual(400, response.status_int)
self.assertEqual('application/json', response.content_type)
expected_error_msg = 'Interval of audit must not be set for ONESHOT.'
self.assertTrue(response.json['error_message'])
@@ -756,7 +755,7 @@ class TestPost(api_base.FunctionalTest):
del audit_dict['scope']
response = self.post_json('/audits', audit_dict, expect_errors=True)
self.assertEqual('application/json', response.content_type)
self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual(400, response.status_int)
assert not mock_trigger_audit.called
@mock.patch.object(deapi.DecisionEngineAPI, 'trigger_audit')
@@ -770,7 +769,7 @@ class TestPost(api_base.FunctionalTest):
response = self.post_json('/audits', audit_dict, expect_errors=True)
self.assertEqual('application/json', response.content_type)
self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual(400, response.status_int)
expected_error_msg = ('Specify parameters but no predefined '
'strategy for audit, or no '
'parameter spec in predefined strategy')
@@ -793,7 +792,7 @@ class TestPost(api_base.FunctionalTest):
response = self.post_json('/audits', audit_dict, expect_errors=True)
self.assertEqual('application/json', response.content_type)
self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual(400, response.status_int)
expected_error_msg = ('Specify parameters but no predefined '
'strategy for audit, or no '
'parameter spec in predefined strategy')
@@ -817,7 +816,7 @@ class TestPost(api_base.FunctionalTest):
del audit_dict[k]
response = self.post_json('/audits', audit_dict, expect_errors=True)
self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual(400, response.status_int)
self.assertEqual("application/json", response.content_type)
expected_error_msg = 'Audit parameter fake2 are not allowed'
self.assertTrue(response.json['error_message'])
@@ -876,13 +875,13 @@ class TestPost(api_base.FunctionalTest):
audit_dict['name'] = normal_name
response = self.post_json('/audits', audit_dict)
self.assertEqual('application/json', response.content_type)
self.assertEqual(HTTPStatus.CREATED, response.status_int)
self.assertEqual(201, response.status_int)
self.assertEqual(normal_name, response.json['name'])
audit_dict['name'] = long_name
response = self.post_json('/audits', audit_dict)
self.assertEqual('application/json', response.content_type)
self.assertEqual(HTTPStatus.CREATED, response.status_int)
self.assertEqual(201, response.status_int)
self.assertNotEqual(long_name, response.json['name'])
@mock.patch.object(deapi.DecisionEngineAPI, 'trigger_audit')
@@ -906,7 +905,7 @@ class TestPost(api_base.FunctionalTest):
audit_dict,
headers={'OpenStack-API-Version': 'infra-optim 1.1'})
self.assertEqual('application/json', response.content_type)
self.assertEqual(HTTPStatus.CREATED, response.status_int)
self.assertEqual(201, response.status_int)
self.assertEqual(objects.audit.State.PENDING,
response.json['state'])
self.assertEqual(audit_dict['interval'], response.json['interval'])
@@ -945,7 +944,7 @@ class TestPost(api_base.FunctionalTest):
headers={'OpenStack-API-Version': 'infra-optim 1.0'},
expect_errors=True)
self.assertEqual('application/json', response.content_type)
self.assertEqual(HTTPStatus.NOT_ACCEPTABLE, response.status_int)
self.assertEqual(406, response.status_int)
expected_error_msg = 'Request not acceptable.'
self.assertTrue(response.json['error_message'])
self.assertIn(expected_error_msg, response.json['error_message'])
@@ -964,7 +963,7 @@ class TestPost(api_base.FunctionalTest):
audit_dict,
headers={'OpenStack-API-Version': 'infra-optim 1.2'})
self.assertEqual('application/json', response.content_type)
self.assertEqual(HTTPStatus.CREATED, response.status_int)
self.assertEqual(201, response.status_int)
self.assertFalse(response.json['force'])
@mock.patch.object(deapi.DecisionEngineAPI, 'trigger_audit')
@@ -981,7 +980,7 @@ class TestPost(api_base.FunctionalTest):
audit_dict,
headers={'OpenStack-API-Version': 'infra-optim 1.2'})
self.assertEqual('application/json', response.content_type)
self.assertEqual(HTTPStatus.CREATED, response.status_int)
self.assertEqual(201, response.status_int)
self.assertTrue(response.json['force'])
@@ -1014,7 +1013,7 @@ class TestDelete(api_base.FunctionalTest):
'op': 'replace'}])
response = self.delete('/audits/%s' % self.audit.uuid,
expect_errors=True)
self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual(400, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
@@ -1026,7 +1025,7 @@ class TestDelete(api_base.FunctionalTest):
self.delete('/audits/%s' % self.audit.uuid)
response = self.get_json('/audits/%s' % self.audit.uuid,
expect_errors=True)
self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
self.assertEqual(404, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
@@ -1042,7 +1041,7 @@ class TestDelete(api_base.FunctionalTest):
def test_delete_audit_not_found(self):
uuid = utils.generate_uuid()
response = self.delete('/audits/%s' % uuid, expect_errors=True)
self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
self.assertEqual(404, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
@@ -1059,7 +1058,7 @@ class TestAuditPolicyEnforcement(api_base.FunctionalTest):
"default": "rule:admin_api",
rule: "rule:defaut"})
response = func(*arg, **kwarg)
self.assertEqual(HTTPStatus.FORBIDDEN, response.status_int)
self.assertEqual(403, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(
"Policy doesn't allow %s to be performed." % rule,

View File

@@ -15,7 +15,6 @@
from unittest import mock
from http import HTTPStatus
from oslo_serialization import jsonutils
from watcher.decision_engine import rpcapi as deapi
@@ -43,7 +42,7 @@ class TestListDataModel(api_base.FunctionalTest):
'/data_model/?data_model_type=compute',
headers={'OpenStack-API-Version': 'infra-optim 1.2'},
expect_errors=True)
self.assertEqual(HTTPStatus.NOT_ACCEPTABLE, response.status_int)
self.assertEqual(406, response.status_int)
class TestDataModelPolicyEnforcement(api_base.FunctionalTest):
@@ -60,7 +59,7 @@ class TestDataModelPolicyEnforcement(api_base.FunctionalTest):
"default": "rule:admin_api",
rule: "rule:defaut"})
response = func(*arg, **kwarg)
self.assertEqual(HTTPStatus.FORBIDDEN, response.status_int)
self.assertEqual(403, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(
"Policy doesn't allow %s to be performed." % rule,

View File

@@ -10,7 +10,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from http import HTTPStatus
from oslo_config import cfg
from oslo_serialization import jsonutils
from urllib import parse as urlparse
@@ -60,7 +59,7 @@ class TestListGoal(api_base.FunctionalTest):
response = self.get_json(
'/goals/%s' % goal['uuid'],
expect_errors=True)
self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
self.assertEqual(404, response.status_int)
def test_detail(self):
goal = obj_utils.create_test_goal(self.context)
@@ -72,7 +71,7 @@ class TestListGoal(api_base.FunctionalTest):
goal = obj_utils.create_test_goal(self.context)
response = self.get_json('/goals/%s/detail' % goal.uuid,
expect_errors=True)
self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
self.assertEqual(404, response.status_int)
def test_many(self):
goal_list = []
@@ -140,7 +139,7 @@ class TestListGoal(api_base.FunctionalTest):
response = self.get_json(
'/goals?sort_key=%s' % 'bad_name',
expect_errors=True)
self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual(400, response.status_int)
class TestGoalPolicyEnforcement(api_base.FunctionalTest):
@@ -151,7 +150,7 @@ class TestGoalPolicyEnforcement(api_base.FunctionalTest):
"default": "rule:admin_api",
rule: "rule:default"})
response = func(*arg, **kwarg)
self.assertEqual(HTTPStatus.FORBIDDEN, response.status_int)
self.assertEqual(403, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(
"Policy doesn't allow %s to be performed." % rule,

View File

@@ -10,8 +10,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from http import HTTPStatus
from watcher.api.controllers.v1 import versions
from watcher.tests.api import base as api_base
@@ -40,7 +38,7 @@ class TestMicroversions(api_base.FunctionalTest):
'10'])},
expect_errors=True, return_json=False)
self.assertEqual('application/json', response.content_type)
self.assertEqual(HTTPStatus.NOT_ACCEPTABLE, response.status_int)
self.assertEqual(406, response.status_int)
expected_error_msg = ('Invalid value for'
' OpenStack-API-Version header')
self.assertTrue(response.json['error_message'])
@@ -100,7 +98,7 @@ class TestMicroversions(api_base.FunctionalTest):
headers={'OpenStack-API-Version': ' '.join([SERVICE_TYPE,
'1.999'])},
expect_errors=True)
self.assertEqual(HTTPStatus.NOT_ACCEPTABLE, response.status_int)
self.assertEqual(406, response.status_int)
self.assertEqual(response.headers[H_MIN_VER], MIN_VER)
self.assertEqual(response.headers[H_MAX_VER], MAX_VER)
expected_error_msg = ('Version 1.999 was requested but the minor '

View File

@@ -10,7 +10,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from http import HTTPStatus
from oslo_config import cfg
from oslo_serialization import jsonutils
from watcher.common import utils
@@ -45,7 +44,7 @@ class TestListScoringEngine(api_base.FunctionalTest):
response = self.get_json(
'/scoring_engines/%s' % scoring_engine['name'],
expect_errors=True)
self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
self.assertEqual(404, response.status_int)
def test_detail(self):
obj_utils.create_test_goal(self.context)
@@ -64,7 +63,7 @@ class TestListScoringEngine(api_base.FunctionalTest):
response = self.get_json(
'/scoring_engines/%s/detail' % scoring_engine.id,
expect_errors=True)
self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
self.assertEqual(404, response.status_int)
def test_many(self):
scoring_engine_list = []
@@ -132,7 +131,7 @@ class TestListScoringEngine(api_base.FunctionalTest):
response = self.get_json(
'/goals?sort_key=%s' % 'bad_name',
expect_errors=True)
self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual(400, response.status_int)
class TestScoringEnginePolicyEnforcement(api_base.FunctionalTest):
@@ -143,7 +142,7 @@ class TestScoringEnginePolicyEnforcement(api_base.FunctionalTest):
"default": "rule:admin_api",
rule: "rule:default"})
response = func(*arg, **kwarg)
self.assertEqual(HTTPStatus.FORBIDDEN, response.status_int)
self.assertEqual(403, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(
"Policy doesn't allow %s to be performed." % rule,

View File

@@ -10,7 +10,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from http import HTTPStatus
from oslo_config import cfg
from oslo_serialization import jsonutils
from urllib import parse as urlparse
@@ -58,7 +57,7 @@ class TestListService(api_base.FunctionalTest):
response = self.get_json(
'/services/%s' % service['id'],
expect_errors=True)
self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
self.assertEqual(404, response.status_int)
def test_detail(self):
service = obj_utils.create_test_service(self.context)
@@ -75,7 +74,7 @@ class TestListService(api_base.FunctionalTest):
service = obj_utils.create_test_service(self.context)
response = self.get_json('/services/%s/detail' % service.id,
expect_errors=True)
self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
self.assertEqual(404, response.status_int)
def test_many(self):
service_list = []
@@ -150,7 +149,7 @@ class TestListService(api_base.FunctionalTest):
response = self.get_json(
'/services?sort_key=%s' % 'bad_name',
expect_errors=True)
self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual(400, response.status_int)
class TestServicePolicyEnforcement(api_base.FunctionalTest):
@@ -161,7 +160,7 @@ class TestServicePolicyEnforcement(api_base.FunctionalTest):
"default": "rule:admin_api",
rule: "rule:default"})
response = func(*arg, **kwarg)
self.assertEqual(HTTPStatus.FORBIDDEN, response.status_int)
self.assertEqual(403, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(
"Policy doesn't allow %s to be performed." % rule,

View File

@@ -13,7 +13,6 @@
from unittest import mock
from urllib import parse as urlparse
from http import HTTPStatus
from oslo_config import cfg
from oslo_serialization import jsonutils
@@ -89,7 +88,7 @@ class TestListStrategy(api_base.FunctionalTest):
response = self.get_json(
'/strategies/%s' % strategy['uuid'],
expect_errors=True)
self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
self.assertEqual(404, response.status_int)
def test_detail(self):
strategy = obj_utils.create_test_strategy(self.context)
@@ -105,7 +104,7 @@ class TestListStrategy(api_base.FunctionalTest):
strategy = obj_utils.create_test_strategy(self.context)
response = self.get_json('/strategies/%s/detail' % strategy.uuid,
expect_errors=True)
self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
self.assertEqual(404, response.status_int)
def test_many(self):
strategy_list = []
@@ -241,7 +240,7 @@ class TestListStrategy(api_base.FunctionalTest):
response = self.get_json(
'/strategies?sort_key=%s' % 'bad_name',
expect_errors=True)
self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual(400, response.status_int)
class TestStrategyPolicyEnforcement(api_base.FunctionalTest):
@@ -257,7 +256,7 @@ class TestStrategyPolicyEnforcement(api_base.FunctionalTest):
"default": "rule:admin_api",
rule: "rule:defaut"})
response = func(*arg, **kwarg)
self.assertEqual(HTTPStatus.FORBIDDEN, response.status_int)
self.assertEqual(403, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(
"Policy doesn't allow %s to be performed." % rule,

View File

@@ -18,8 +18,6 @@ import webtest
import wsme
from wsme import types as wtypes
from http import HTTPStatus
from watcher.api.controllers.v1 import types
from watcher.common import exception
from watcher.common import utils
@@ -122,68 +120,68 @@ class TestJsonPatchType(base.TestCase):
{'path': '/dict', 'op': 'add',
'value': {'cat': 'meow'}}]
ret = self._patch_json(valid_patches, False)
self.assertEqual(HTTPStatus.OK, ret.status_int)
self.assertEqual(200, ret.status_int)
self.assertEqual(valid_patches, ret.json)
def test_cannot_update_internal_attr(self):
patch = [{'path': '/internal', 'op': 'replace', 'value': 'foo'}]
ret = self._patch_json(patch, True)
self.assertEqual(HTTPStatus.BAD_REQUEST, ret.status_int)
self.assertEqual(400, ret.status_int)
self.assertTrue(ret.json['faultstring'])
def test_cannot_update_internal_dict_attr(self):
patch = [{'path': '/internal', 'op': 'replace',
'value': 'foo'}]
ret = self._patch_json(patch, True)
self.assertEqual(HTTPStatus.BAD_REQUEST, ret.status_int)
self.assertEqual(400, ret.status_int)
self.assertTrue(ret.json['faultstring'])
def test_mandatory_attr(self):
patch = [{'op': 'replace', 'path': '/mandatory', 'value': 'foo'}]
ret = self._patch_json(patch, False)
self.assertEqual(HTTPStatus.OK, ret.status_int)
self.assertEqual(200, ret.status_int)
self.assertEqual(patch, ret.json)
def test_cannot_remove_mandatory_attr(self):
patch = [{'op': 'remove', 'path': '/mandatory'}]
ret = self._patch_json(patch, True)
self.assertEqual(HTTPStatus.BAD_REQUEST, ret.status_int)
self.assertEqual(400, ret.status_int)
self.assertTrue(ret.json['faultstring'])
def test_missing_required_fields_path(self):
missing_path = [{'op': 'remove'}]
ret = self._patch_json(missing_path, True)
self.assertEqual(HTTPStatus.BAD_REQUEST, ret.status_int)
self.assertEqual(400, ret.status_int)
self.assertTrue(ret.json['faultstring'])
def test_missing_required_fields_op(self):
missing_op = [{'path': '/foo'}]
ret = self._patch_json(missing_op, True)
self.assertEqual(HTTPStatus.BAD_REQUEST, ret.status_int)
self.assertEqual(400, ret.status_int)
self.assertTrue(ret.json['faultstring'])
def test_invalid_op(self):
patch = [{'path': '/foo', 'op': 'invalid'}]
ret = self._patch_json(patch, True)
self.assertEqual(HTTPStatus.BAD_REQUEST, ret.status_int)
self.assertEqual(400, ret.status_int)
self.assertTrue(ret.json['faultstring'])
def test_invalid_path(self):
patch = [{'path': 'invalid-path', 'op': 'remove'}]
ret = self._patch_json(patch, True)
self.assertEqual(HTTPStatus.BAD_REQUEST, ret.status_int)
self.assertEqual(400, ret.status_int)
self.assertTrue(ret.json['faultstring'])
def test_cannot_add_with_no_value(self):
patch = [{'path': '/extra/foo', 'op': 'add'}]
ret = self._patch_json(patch, True)
self.assertEqual(HTTPStatus.BAD_REQUEST, ret.status_int)
self.assertEqual(400, ret.status_int)
self.assertTrue(ret.json['faultstring'])
def test_cannot_replace_with_no_value(self):
patch = [{'path': '/foo', 'op': 'replace'}]
ret = self._patch_json(patch, True)
self.assertEqual(HTTPStatus.BAD_REQUEST, ret.status_int)
self.assertEqual(400, ret.status_int)
self.assertTrue(ret.json['faultstring'])

View File

@@ -12,8 +12,6 @@
from unittest import mock
from http import HTTPStatus
from watcher.decision_engine import rpcapi as deapi
from watcher import objects
from watcher.tests.api import base as api_base
@@ -36,7 +34,7 @@ class TestPost(api_base.FunctionalTest):
response = self.post_json(
'/webhooks/%s' % audit['uuid'], {},
headers={'OpenStack-API-Version': 'infra-optim 1.4'})
self.assertEqual(HTTPStatus.ACCEPTED, response.status_int)
self.assertEqual(202, response.status_int)
mock_trigger_audit.assert_called_once_with(
mock.ANY, audit['uuid'])
@@ -45,7 +43,7 @@ class TestPost(api_base.FunctionalTest):
'/webhooks/no-audit', {},
headers={'OpenStack-API-Version': 'infra-optim 1.4'},
expect_errors=True)
self.assertEqual(HTTPStatus.NOT_FOUND, response.status_int)
self.assertEqual(404, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
@@ -55,7 +53,7 @@ class TestPost(api_base.FunctionalTest):
'/webhooks/%s' % audit['uuid'], {},
headers={'OpenStack-API-Version': 'infra-optim 1.4'},
expect_errors=True)
self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual(400, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
@@ -68,6 +66,6 @@ class TestPost(api_base.FunctionalTest):
'/webhooks/%s' % audit['uuid'], {},
headers={'OpenStack-API-Version': 'infra-optim 1.4'},
expect_errors=True)
self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
self.assertEqual(400, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])

Some files were not shown because too many files have changed in this diff Show More