Compare commits
75 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ecb0e218a9 | ||
|
|
4228647d15 | ||
|
|
4f1d758a40 | ||
|
|
b4433db20a | ||
|
|
e03f56e7c7 | ||
|
|
d925166a25 | ||
|
|
4a5d8cf709 | ||
|
|
e3c6db11c6 | ||
|
|
840d422b01 | ||
|
|
e9d8a2882f | ||
|
|
6e09cdb5ac | ||
|
|
edd3d219d5 | ||
|
|
d433d6b3c8 | ||
|
|
c5d4f9cb40 | ||
|
|
41f579d464 | ||
|
|
1a2fa9addf | ||
|
|
9e5ca76116 | ||
|
|
fa63b2a2b3 | ||
|
|
22cad5651e | ||
|
|
a912977336 | ||
|
|
2d7a375338 | ||
|
|
45b82e1898 | ||
|
|
926dbc8392 | ||
|
|
0e440d37ee | ||
|
|
66934f29d3 | ||
|
|
7039a9d247 | ||
|
|
bbbae0b105 | ||
|
|
1d08d2eea1 | ||
|
|
23442a4da6 | ||
|
|
ed88b436af | ||
|
|
a48a16596f | ||
|
|
8f4a856bd2 | ||
|
|
85a46ce4d4 | ||
|
|
35066dfe60 | ||
|
|
fc9eb6e995 | ||
|
|
aaf6fd7959 | ||
|
|
5e077f37ea | ||
|
|
2ec8bc10cd | ||
|
|
89cea83c85 | ||
|
|
2dd00a2037 | ||
|
|
59e13fd1f3 | ||
|
|
4235ef7c24 | ||
|
|
a015af1bd2 | ||
|
|
dad90b63fd | ||
|
|
c6e5f98008 | ||
|
|
1341c0ee02 | ||
|
|
6e99fcffc3 | ||
|
|
a57f54ab8f | ||
|
|
d1490e3fa7 | ||
|
|
1324baf9f5 | ||
|
|
0adc7d91e6 | ||
|
|
0d7ded0bb3 | ||
|
|
80dfbd6334 | ||
|
|
7d40b3d4c3 | ||
|
|
cedf70559e | ||
|
|
cc561c528f | ||
|
|
87b494d52a | ||
|
|
d0bca1f2ab | ||
|
|
068178f12a | ||
|
|
99e6c4aebb | ||
|
|
b446f8afd2 | ||
|
|
7783ebfb71 | ||
|
|
22cfc495f4 | ||
|
|
360b0119d6 | ||
|
|
48fc90d7b6 | ||
|
|
c5ff387ae9 | ||
|
|
1bc6b0e605 | ||
|
|
345083e090 | ||
|
|
1a17c4b7ac | ||
|
|
c4dfbd5855 | ||
|
|
1981f3964e | ||
|
|
d792e3cfae | ||
|
|
f66eb463ca | ||
|
|
6a323ed54f | ||
|
|
d252d47cc0 |
@@ -129,6 +129,7 @@ function create_watcher_conf {
|
||||
iniset $WATCHER_CONF oslo_messaging_notifications driver "messaging"
|
||||
|
||||
iniset $NOVA_CONF oslo_messaging_notifications topics "notifications,watcher_notifications"
|
||||
iniset $NOVA_CONF notifications notify_on_state_change "vm_and_task_state"
|
||||
|
||||
configure_auth_token_middleware $WATCHER_CONF watcher $WATCHER_AUTH_CACHE_DIR
|
||||
configure_auth_token_middleware $WATCHER_CONF watcher $WATCHER_AUTH_CACHE_DIR "watcher_clients_auth"
|
||||
|
||||
@@ -44,3 +44,6 @@ LOGDAYS=2
|
||||
[[post-config|$NOVA_CONF]]
|
||||
[DEFAULT]
|
||||
compute_monitors=cpu.virt_driver
|
||||
notify_on_state_change = vm_and_task_state
|
||||
[notifications]
|
||||
notify_on_state_change = vm_and_task_state
|
||||
|
||||
@@ -45,3 +45,6 @@ LOGDAYS=2
|
||||
[[post-config|$NOVA_CONF]]
|
||||
[DEFAULT]
|
||||
compute_monitors=cpu.virt_driver
|
||||
notify_on_state_change = vm_and_task_state
|
||||
[notifications]
|
||||
notify_on_state_change = vm_and_task_state
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# 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
|
||||
|
||||
@@ -15,7 +15,7 @@ Service overview
|
||||
================
|
||||
|
||||
The Watcher system is a collection of services that provides support to
|
||||
optimize your IAAS plateform. The Watcher service may, depending upon
|
||||
optimize your IAAS platform. The Watcher service may, depending upon
|
||||
configuration, interact with several other OpenStack services. This includes:
|
||||
|
||||
- the OpenStack Identity service (`keystone`_) for request authentication and
|
||||
@@ -37,7 +37,7 @@ The Watcher service includes the following components:
|
||||
- `watcher-dashboard`_: An Horizon plugin for interacting with the Watcher
|
||||
service.
|
||||
|
||||
Additionally, the Bare Metal service has certain external dependencies, which
|
||||
Additionally, the Watcher service has certain external dependencies, which
|
||||
are very similar to other OpenStack services:
|
||||
|
||||
- A database to store audit and action plan information and state. You can set
|
||||
@@ -86,7 +86,6 @@ Configure the Identity service for the Watcher service
|
||||
--tenant=KEYSTONE_SERVICE_PROJECT_NAME
|
||||
$ keystone user-role-add --user=watcher \
|
||||
--tenant=KEYSTONE_SERVICE_PROJECT_NAME --role=admin
|
||||
$ keystone user-role-add --user=watcher --tenant=admin --role=admin
|
||||
|
||||
or (by using python-openstackclient 1.8.0+)
|
||||
|
||||
@@ -97,7 +96,6 @@ Configure the Identity service for the Watcher service
|
||||
--project=KEYSTONE_SERVICE_PROJECT_NAME
|
||||
$ openstack role add --project KEYSTONE_SERVICE_PROJECT_NAME \
|
||||
--user watcher admin
|
||||
$ openstack role add --user watcher --project admin admin
|
||||
|
||||
|
||||
#. You must register the Watcher Service with the Identity Service so that
|
||||
@@ -169,7 +167,7 @@ these following commands::
|
||||
|
||||
$ git clone git://git.openstack.org/openstack/watcher
|
||||
$ cd watcher/
|
||||
$ tox -econfig
|
||||
$ tox -e genconfig
|
||||
$ vi etc/watcher/watcher.conf.sample
|
||||
|
||||
|
||||
@@ -368,7 +366,7 @@ Configure Nova compute
|
||||
Please check your hypervisor configuration to correctly handle
|
||||
`instance migration`_.
|
||||
|
||||
.. _`instance migration`: http://docs.openstack.org/admin-guide-cloud/compute-configuring-migrations.html
|
||||
.. _`instance migration`: http://docs.openstack.org/admin-guide/compute-live-migration-usage.html
|
||||
|
||||
Configure Measurements
|
||||
======================
|
||||
|
||||
@@ -193,6 +193,37 @@ must exist in every other compute node's stack user's authorized_keys file and
|
||||
every compute node's public ECDSA key needs to be in every other compute
|
||||
node's root user's known_hosts file.
|
||||
|
||||
Disable serial console
|
||||
----------------------
|
||||
|
||||
Serial console needs to be disabled for live migration to work.
|
||||
|
||||
On both the controller and compute node, in /etc/nova/nova.conf
|
||||
|
||||
[serial_console]
|
||||
enabled = False
|
||||
|
||||
Alternatively, in devstack's local.conf:
|
||||
|
||||
[[post-config|$NOVA_CONF]]
|
||||
[serial_console]
|
||||
#enabled=false
|
||||
|
||||
|
||||
VNC server configuration
|
||||
------------------------
|
||||
|
||||
The VNC server listening parameter needs to be set to any address so
|
||||
that the server can accept connections from all of the compute nodes.
|
||||
|
||||
On both the controller and compute node, in /etc/nova/nova.conf
|
||||
|
||||
vncserver_listen = 0.0.0.0
|
||||
|
||||
Alternatively, in devstack's local.conf:
|
||||
|
||||
VNCSERVER_LISTEN=0.0.0.0
|
||||
|
||||
|
||||
Environment final checkup
|
||||
-------------------------
|
||||
|
||||
@@ -85,6 +85,9 @@ your platform.
|
||||
|
||||
$ sudo yum install openssl-devel libffi-devel mysql-devel
|
||||
|
||||
* CentOS 7::
|
||||
|
||||
$ sudo yum install gcc python-devel libxml2-devel libxslt-devel mariadb-devel
|
||||
|
||||
PyPi Packages and VirtualEnv
|
||||
----------------------------
|
||||
|
||||
@@ -30,12 +30,12 @@ implement:
|
||||
implement. This is the first function to be called by the
|
||||
:ref:`applier <watcher_applier_definition>` before any further processing
|
||||
and its role is to validate the input parameters that were provided to it.
|
||||
- The :py:meth:`~.BaseAction.precondition` is called before the execution of
|
||||
- The :py:meth:`~.BaseAction.pre_condition` is called before the execution of
|
||||
an action. This method is a hook that can be used to perform some
|
||||
initializations or to make some more advanced validation on its input
|
||||
parameters. If you wish to block the execution based on this factor, you
|
||||
simply have to ``raise`` an exception.
|
||||
- The :py:meth:`~.BaseAction.postcondition` is called after the execution of
|
||||
- The :py:meth:`~.BaseAction.post_condition` is called after the execution of
|
||||
an action. As this function is called regardless of whether an action
|
||||
succeeded or not, this can prove itself useful to perform cleanup
|
||||
operations.
|
||||
@@ -71,11 +71,11 @@ Here is an example showing how you can write a plugin called ``DummyAction``:
|
||||
# Does nothing
|
||||
pass
|
||||
|
||||
def precondition(self):
|
||||
def pre_condition(self):
|
||||
# No pre-checks are done here
|
||||
pass
|
||||
|
||||
def postcondition(self):
|
||||
def post_condition(self):
|
||||
# Nothing done here
|
||||
pass
|
||||
|
||||
|
||||
@@ -268,5 +268,5 @@ At this point, you can use your new cluster data model plugin in your
|
||||
# [...]
|
||||
dummy_collector = self.collector_manager.get_cluster_model_collector(
|
||||
"dummy") # "dummy" is the name of the entry point we declared earlier
|
||||
dummy_model = collector.get_latest_cluster_data_model()
|
||||
dummy_model = dummy_collector.get_latest_cluster_data_model()
|
||||
# Do some stuff with this model
|
||||
|
||||
@@ -245,22 +245,30 @@ Querying metrics
|
||||
|
||||
A large set of metrics, generated by OpenStack modules, can be used in your
|
||||
strategy implementation. To collect these metrics, Watcher provides a
|
||||
`Helper`_ to the Ceilometer API, which makes this API reusable and easier
|
||||
to used.
|
||||
`Helper`_ for two data sources which are `Ceilometer`_ and `Monasca`_. If you
|
||||
wish to query metrics from a different data source, you can implement your own
|
||||
and directly use it from within your new strategy. Indeed, strategies in
|
||||
Watcher have the cluster data models decoupled from the data sources which
|
||||
means that you may keep the former while changing the latter.
|
||||
The recommended way for you to support a new data source is to implement a new
|
||||
helper that would encapsulate within separate methods the queries you need to
|
||||
perform. To then use it, you would just have to instantiate it within your
|
||||
strategy.
|
||||
|
||||
If you want to use your own metrics database backend, please refer to the
|
||||
`Ceilometer developer guide`_. Indeed, Ceilometer's pluggable model allows
|
||||
for various types of backends. A list of the available backends is located
|
||||
here_. The Ceilosca project is a good example of how to create your own
|
||||
pluggable backend.
|
||||
If you want to use Ceilometer but with your own metrics database backend,
|
||||
please refer to the `Ceilometer developer guide`_. The list of the available
|
||||
Ceilometer backends is located here_. The `Ceilosca`_ project is a good example
|
||||
of how to create your own pluggable backend. Moreover, if your strategy
|
||||
requires new metrics not covered by Ceilometer, you can add them through a
|
||||
`Ceilometer plugin`_.
|
||||
|
||||
Finally, if your strategy requires new metrics not covered by Ceilometer, you
|
||||
can add them through a Ceilometer `plugin`_.
|
||||
|
||||
.. _`Helper`: https://github.com/openstack/watcher/blob/master/watcher/decision_engine/cluster/history/ceilometer.py
|
||||
.. _`Ceilometer developer guide`: http://docs.openstack.org/developer/ceilometer/architecture.html#storing-the-data
|
||||
.. _`Ceilometer`: http://docs.openstack.org/developer/ceilometer/
|
||||
.. _`Monasca`: https://github.com/openstack/monasca-api/blob/master/docs/monasca-api-spec.md
|
||||
.. _`here`: http://docs.openstack.org/developer/ceilometer/install/dbreco.html#choosing-a-database-backend
|
||||
.. _`plugin`: http://docs.openstack.org/developer/ceilometer/plugins.html
|
||||
.. _`Ceilometer plugin`: http://docs.openstack.org/developer/ceilometer/plugins.html
|
||||
.. _`Ceilosca`: https://github.com/openstack/monasca-ceilometer/blob/master/ceilosca/ceilometer/storage/impl_monasca.py
|
||||
|
||||
|
||||
|
||||
@@ -71,6 +71,9 @@ parameter type default Value description
|
||||
be tried by the strategy while
|
||||
searching for potential candidates.
|
||||
To remove the limit, set it to 0
|
||||
``period`` Number 7200 The time interval in seconds
|
||||
for getting statistic aggregation
|
||||
from metric data source
|
||||
====================== ====== ============= ===================================
|
||||
|
||||
Efficacy Indicator
|
||||
|
||||
@@ -89,7 +89,7 @@ How to use it ?
|
||||
.. code-block:: shell
|
||||
|
||||
$ openstack optimize audittemplate create \
|
||||
at1 vm_consolidation --strategy vm_workload_consolidation
|
||||
at1 server_consolidation --strategy vm_workload_consolidation
|
||||
|
||||
$ openstack optimize audit create -a at1
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
To generate the sample watcher.conf file, run the following
|
||||
command from the top level of the watcher directory:
|
||||
|
||||
tox -econfig
|
||||
tox -e genconfig
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
features:
|
||||
- Add notifications related to Audit object.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
features:
|
||||
- Watcher can continuously optimize the OpenStack cloud for a specific
|
||||
strategy or goal by triggering an audit periodically which generates
|
||||
an action plan and run it automatically.
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
features:
|
||||
- Centralize all configuration options for Watcher.
|
||||
3
releasenotes/notes/db-migration-e1a705a8b54ccdd2.yaml
Normal file
3
releasenotes/notes/db-migration-e1a705a8b54ccdd2.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
features:
|
||||
- Watcher database can now be upgraded thanks to Alembic.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
features:
|
||||
- Provides a generic way to define the scope of an audit. The set of audited
|
||||
resources will be called "Audit scope" and will be defined in each audit
|
||||
template (which contains the audit settings).
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
features:
|
||||
- The graph model describes how VMs are associated to compute hosts.
|
||||
This allows for seeing relationships upfront between the entities and hence
|
||||
can be used to identify hot/cold spots in the data center and influence
|
||||
a strategy decision.
|
||||
4
releasenotes/notes/monasca-support-0b0486b8572ac38b.yaml
Normal file
4
releasenotes/notes/monasca-support-0b0486b8572ac38b.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
features:
|
||||
- Watcher supports multiple metrics backend and relies on Ceilometer and
|
||||
Monasca.
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
features:
|
||||
- Watcher can now run specific actions in parallel improving the performances
|
||||
dramatically when executing an action plan.
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
features:
|
||||
- Add superseded state for an action plan if the cluster data model has
|
||||
changed after it has been created.
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
features:
|
||||
- Provide a notification mechanism into Watcher that supports versioning.
|
||||
Whenever a Watcher object is created, updated or deleted, a versioned
|
||||
notification will, if it's relevant, be automatically sent to notify in order
|
||||
to allow an event-driven style of architecture within Watcher. Moreover, it
|
||||
will also give other services and/or 3rd party softwares (e.g. monitoring
|
||||
solutions or rules engines) the ability to react to such events.
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
features:
|
||||
- Add a service supervisor to watch Watcher deamons.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
features:
|
||||
- all Watcher objects have been refactored to support OVO
|
||||
(oslo.versionedobjects) which was a prerequisite step in order to implement
|
||||
versioned notifications.
|
||||
@@ -1,5 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# watcher documentation build configuration file, created by
|
||||
# sphinx-quickstart on Fri Jun 3 11:37:52 2016.
|
||||
#
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
apscheduler # MIT License
|
||||
enum34;python_version=='2.7' or python_version=='2.6' or python_version=='3.3' # BSD
|
||||
jsonpatch>=1.1 # BSD
|
||||
keystoneauth1>=2.14.0 # Apache-2.0
|
||||
keystonemiddleware!=4.5.0,>=4.2.0 # Apache-2.0
|
||||
lxml>=2.3 # BSD
|
||||
keystoneauth1>=2.18.0 # Apache-2.0
|
||||
keystonemiddleware>=4.12.0 # Apache-2.0
|
||||
lxml!=3.7.0,>=2.3 # BSD
|
||||
oslo.concurrency>=3.8.0 # Apache-2.0
|
||||
oslo.cache>=1.5.0 # Apache-2.0
|
||||
oslo.config!=3.18.0,>=3.14.0 # Apache-2.0
|
||||
oslo.context>=2.9.0 # Apache-2.0
|
||||
oslo.db!=4.13.1,!=4.13.2,>=4.11.0 # Apache-2.0
|
||||
oslo.db>=4.15.0 # Apache-2.0
|
||||
oslo.i18n>=2.1.0 # Apache-2.0
|
||||
oslo.log>=3.11.0 # Apache-2.0
|
||||
oslo.messaging>=5.14.0 # Apache-2.0
|
||||
@@ -31,12 +31,15 @@ python-ceilometerclient>=2.5.0 # Apache-2.0
|
||||
python-cinderclient!=1.7.0,!=1.7.1,>=1.6.0 # Apache-2.0
|
||||
python-glanceclient>=2.5.0 # Apache-2.0
|
||||
python-keystoneclient>=3.8.0 # Apache-2.0
|
||||
python-monascaclient>=1.1.0 # Apache-2.0
|
||||
python-neutronclient>=5.1.0 # Apache-2.0
|
||||
python-novaclient!=2.33.0,>=2.29.0 # Apache-2.0
|
||||
python-novaclient!=7.0.0,>=6.0.0 # Apache-2.0
|
||||
python-openstackclient>=3.3.0 # Apache-2.0
|
||||
six>=1.9.0 # MIT
|
||||
SQLAlchemy<1.1.0,>=1.0.10 # MIT
|
||||
stevedore>=1.17.1 # Apache-2.0
|
||||
taskflow>=1.26.0 # Apache-2.0
|
||||
taskflow>=2.7.0 # Apache-2.0
|
||||
WebOb>=1.6.0 # MIT
|
||||
WSME>=0.8 # MIT
|
||||
networkx>=1.10 # BSD
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@ watcher_scoring_engine_containers =
|
||||
watcher_strategies =
|
||||
dummy = watcher.decision_engine.strategy.strategies.dummy_strategy:DummyStrategy
|
||||
dummy_with_scorer = watcher.decision_engine.strategy.strategies.dummy_with_scorer:DummyWithScorer
|
||||
dummy_with_resize = watcher.decision_engine.strategy.strategies.dummy_with_resize:DummyWithResize
|
||||
basic = watcher.decision_engine.strategy.strategies.basic_consolidation:BasicConsolidation
|
||||
outlet_temperature = watcher.decision_engine.strategy.strategies.outlet_temp_control:OutletTempControl
|
||||
vm_workload_consolidation = watcher.decision_engine.strategy.strategies.vm_workload_consolidation:VMWorkloadConsolidation
|
||||
@@ -76,12 +77,14 @@ watcher_actions =
|
||||
nop = watcher.applier.actions.nop:Nop
|
||||
sleep = watcher.applier.actions.sleep:Sleep
|
||||
change_nova_service_state = watcher.applier.actions.change_nova_service_state:ChangeNovaServiceState
|
||||
resize = watcher.applier.actions.resize:Resize
|
||||
|
||||
watcher_workflow_engines =
|
||||
taskflow = watcher.applier.workflow_engine.default:DefaultWorkFlowEngine
|
||||
|
||||
watcher_planners =
|
||||
default = watcher.decision_engine.planner.default:DefaultPlanner
|
||||
weight = watcher.decision_engine.planner.weight:WeightPlanner
|
||||
workload_stabilization = watcher.decision_engine.planner.workload_stabilization:WorkloadStabilizationPlanner
|
||||
|
||||
watcher_cluster_data_model_collectors =
|
||||
compute = watcher.decision_engine.model.collector.nova:NovaClusterDataModelCollector
|
||||
|
||||
10
tox.ini
10
tox.ini
@@ -6,9 +6,7 @@ skipsdist = True
|
||||
[testenv]
|
||||
usedevelop = True
|
||||
whitelist_externals = find
|
||||
install_command =
|
||||
pip install -U --force-reinstall -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages}
|
||||
|
||||
install_command = pip install -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages}
|
||||
setenv =
|
||||
VIRTUAL_ENV={envdir}
|
||||
deps = -r{toxinidir}/test-requirements.txt
|
||||
@@ -27,7 +25,9 @@ setenv = PYTHONHASHSEED=0
|
||||
commands = {posargs}
|
||||
|
||||
[testenv:cover]
|
||||
commands = python setup.py testr --coverage --testr-args='{posargs}'
|
||||
commands =
|
||||
python setup.py testr --coverage --testr-args='{posargs}'
|
||||
coverage report
|
||||
|
||||
[testenv:docs]
|
||||
setenv = PYTHONHASHSEED=0
|
||||
@@ -38,7 +38,7 @@ commands =
|
||||
[testenv:debug]
|
||||
commands = oslo_debug_helper -t watcher/tests {posargs}
|
||||
|
||||
[testenv:config]
|
||||
[testenv:genconfig]
|
||||
sitepackages = False
|
||||
commands =
|
||||
oslo-config-generator --config-file etc/watcher/watcher-config-generator.conf
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 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
|
||||
|
||||
@@ -88,7 +88,6 @@ class Action(base.APIBase):
|
||||
between the internal object model and the API representation of a action.
|
||||
"""
|
||||
_action_plan_uuid = None
|
||||
_next_uuid = None
|
||||
|
||||
def _get_action_plan_uuid(self):
|
||||
return self._action_plan_uuid
|
||||
@@ -105,22 +104,6 @@ class Action(base.APIBase):
|
||||
except exception.ActionPlanNotFound:
|
||||
self._action_plan_uuid = None
|
||||
|
||||
def _get_next_uuid(self):
|
||||
return self._next_uuid
|
||||
|
||||
def _set_next_uuid(self, value):
|
||||
if value == wtypes.Unset:
|
||||
self._next_uuid = wtypes.Unset
|
||||
elif value and self._next_uuid != value:
|
||||
try:
|
||||
action_next = objects.Action.get(
|
||||
pecan.request.context, value)
|
||||
self._next_uuid = action_next.uuid
|
||||
self.next = action_next.id
|
||||
except exception.ActionNotFound:
|
||||
self.action_next_uuid = None
|
||||
# raise e
|
||||
|
||||
uuid = wtypes.wsattr(types.uuid, readonly=True)
|
||||
"""Unique UUID for this action"""
|
||||
|
||||
@@ -138,10 +121,8 @@ class Action(base.APIBase):
|
||||
input_parameters = types.jsontype
|
||||
"""One or more key/value pairs """
|
||||
|
||||
next_uuid = wsme.wsproperty(types.uuid, _get_next_uuid,
|
||||
_set_next_uuid,
|
||||
mandatory=True)
|
||||
"""This next action UUID"""
|
||||
parents = wtypes.wsattr(types.jsontype, readonly=True)
|
||||
"""UUIDs of parent actions"""
|
||||
|
||||
links = wsme.wsattr([link.Link], readonly=True)
|
||||
"""A list containing a self link and associated action links"""
|
||||
@@ -152,7 +133,6 @@ class Action(base.APIBase):
|
||||
self.fields = []
|
||||
fields = list(objects.Action.fields)
|
||||
fields.append('action_plan_uuid')
|
||||
fields.append('next_uuid')
|
||||
for field in fields:
|
||||
# Skip fields we do not expose.
|
||||
if not hasattr(self, field):
|
||||
@@ -163,15 +143,13 @@ class Action(base.APIBase):
|
||||
self.fields.append('action_plan_id')
|
||||
setattr(self, 'action_plan_uuid', kwargs.get('action_plan_id',
|
||||
wtypes.Unset))
|
||||
setattr(self, 'next_uuid', kwargs.get('next',
|
||||
wtypes.Unset))
|
||||
|
||||
@staticmethod
|
||||
def _convert_with_links(action, url, expand=True):
|
||||
if not expand:
|
||||
action.unset_fields_except(['uuid', 'state', 'next', 'next_uuid',
|
||||
'action_plan_uuid', 'action_plan_id',
|
||||
'action_type'])
|
||||
action.unset_fields_except(['uuid', 'state', 'action_plan_uuid',
|
||||
'action_plan_id', 'action_type',
|
||||
'parents'])
|
||||
|
||||
action.links = [link.Link.make_link('self', url,
|
||||
'actions', action.uuid),
|
||||
@@ -193,9 +171,9 @@ class Action(base.APIBase):
|
||||
state='PENDING',
|
||||
created_at=datetime.datetime.utcnow(),
|
||||
deleted_at=None,
|
||||
updated_at=datetime.datetime.utcnow())
|
||||
updated_at=datetime.datetime.utcnow(),
|
||||
parents=[])
|
||||
sample._action_plan_uuid = '7ae81bb3-dec3-4289-8d6c-da80bd8001ae'
|
||||
sample._next_uuid = '7ae81bb3-dec3-4289-8d6c-da80bd8001ae'
|
||||
return cls._convert_with_links(sample, 'http://localhost:9322', expand)
|
||||
|
||||
|
||||
@@ -216,17 +194,6 @@ class ActionCollection(collection.Collection):
|
||||
collection.actions = [Action.convert_with_links(p, expand)
|
||||
for p in actions]
|
||||
|
||||
if 'sort_key' in kwargs:
|
||||
reverse = False
|
||||
if kwargs['sort_key'] == 'next_uuid':
|
||||
if 'sort_dir' in kwargs:
|
||||
reverse = True if kwargs['sort_dir'] == 'desc' else False
|
||||
collection.actions = sorted(
|
||||
collection.actions,
|
||||
key=lambda action: action.next_uuid or '',
|
||||
reverse=reverse)
|
||||
|
||||
collection.next = collection.get_next(limit, url=url, **kwargs)
|
||||
return collection
|
||||
|
||||
@classmethod
|
||||
@@ -268,10 +235,7 @@ class ActionsController(rest.RestController):
|
||||
if audit_uuid:
|
||||
filters['audit_uuid'] = audit_uuid
|
||||
|
||||
if sort_key == 'next_uuid':
|
||||
sort_db_key = None
|
||||
else:
|
||||
sort_db_key = sort_key
|
||||
sort_db_key = sort_key
|
||||
|
||||
actions = objects.Action.list(pecan.request.context,
|
||||
limit,
|
||||
|
||||
@@ -106,7 +106,7 @@ class ActionPlanPatchType(types.JsonPatchType):
|
||||
|
||||
@staticmethod
|
||||
def mandatory_attrs():
|
||||
return ["audit_id", "state", "first_action_id"]
|
||||
return ["audit_id", "state"]
|
||||
|
||||
|
||||
class ActionPlan(base.APIBase):
|
||||
@@ -120,7 +120,6 @@ class ActionPlan(base.APIBase):
|
||||
_audit_uuid = None
|
||||
_strategy_uuid = None
|
||||
_strategy_name = None
|
||||
_first_action_uuid = None
|
||||
_efficacy_indicators = None
|
||||
|
||||
def _get_audit_uuid(self):
|
||||
@@ -137,21 +136,6 @@ class ActionPlan(base.APIBase):
|
||||
except exception.AuditNotFound:
|
||||
self._audit_uuid = None
|
||||
|
||||
def _get_first_action_uuid(self):
|
||||
return self._first_action_uuid
|
||||
|
||||
def _set_first_action_uuid(self, value):
|
||||
if value == wtypes.Unset:
|
||||
self._first_action_uuid = wtypes.Unset
|
||||
elif value and self._first_action_uuid != value:
|
||||
try:
|
||||
first_action = objects.Action.get(pecan.request.context,
|
||||
value)
|
||||
self._first_action_uuid = first_action.uuid
|
||||
self.first_action_id = first_action.id
|
||||
except exception.ActionNotFound:
|
||||
self._first_action_uuid = None
|
||||
|
||||
def _get_efficacy_indicators(self):
|
||||
if self._efficacy_indicators is None:
|
||||
self._set_efficacy_indicators(wtypes.Unset)
|
||||
@@ -220,11 +204,6 @@ class ActionPlan(base.APIBase):
|
||||
uuid = wtypes.wsattr(types.uuid, readonly=True)
|
||||
"""Unique UUID for this action plan"""
|
||||
|
||||
first_action_uuid = wsme.wsproperty(
|
||||
types.uuid, _get_first_action_uuid, _set_first_action_uuid,
|
||||
mandatory=True)
|
||||
"""The UUID of the first action this action plans links to"""
|
||||
|
||||
audit_uuid = wsme.wsproperty(types.uuid, _get_audit_uuid, _set_audit_uuid,
|
||||
mandatory=True)
|
||||
"""The UUID of the audit this port belongs to"""
|
||||
@@ -263,7 +242,6 @@ class ActionPlan(base.APIBase):
|
||||
setattr(self, field, kwargs.get(field, wtypes.Unset))
|
||||
|
||||
self.fields.append('audit_uuid')
|
||||
self.fields.append('first_action_uuid')
|
||||
self.fields.append('efficacy_indicators')
|
||||
|
||||
setattr(self, 'audit_uuid', kwargs.get('audit_id', wtypes.Unset))
|
||||
@@ -271,16 +249,13 @@ class ActionPlan(base.APIBase):
|
||||
setattr(self, 'strategy_uuid', kwargs.get('strategy_id', wtypes.Unset))
|
||||
fields.append('strategy_name')
|
||||
setattr(self, 'strategy_name', kwargs.get('strategy_id', wtypes.Unset))
|
||||
setattr(self, 'first_action_uuid',
|
||||
kwargs.get('first_action_id', wtypes.Unset))
|
||||
|
||||
@staticmethod
|
||||
def _convert_with_links(action_plan, url, expand=True):
|
||||
if not expand:
|
||||
action_plan.unset_fields_except(
|
||||
['uuid', 'state', 'efficacy_indicators', 'global_efficacy',
|
||||
'updated_at', 'audit_uuid', 'strategy_uuid', 'strategy_name',
|
||||
'first_action_uuid'])
|
||||
'updated_at', 'audit_uuid', 'strategy_uuid', 'strategy_name'])
|
||||
|
||||
action_plan.links = [
|
||||
link.Link.make_link(
|
||||
@@ -305,7 +280,6 @@ class ActionPlan(base.APIBase):
|
||||
created_at=datetime.datetime.utcnow(),
|
||||
deleted_at=None,
|
||||
updated_at=datetime.datetime.utcnow())
|
||||
sample._first_action_uuid = '57eaf9ab-5aaa-4f7e-bdf7-9a140ac7a720'
|
||||
sample._audit_uuid = 'abcee106-14d3-4515-b744-5a26885cf6f6'
|
||||
sample._efficacy_indicators = [{'description': 'Test indicator',
|
||||
'name': 'test_indicator',
|
||||
|
||||
@@ -69,6 +69,8 @@ class AuditPostType(wtypes.Base):
|
||||
|
||||
scope = wtypes.wsattr(types.jsontype, readonly=True)
|
||||
|
||||
auto_trigger = wtypes.wsattr(bool, mandatory=False)
|
||||
|
||||
def as_audit(self, context):
|
||||
audit_type_values = [val.value for val in objects.audit.AuditType]
|
||||
if self.audit_type not in audit_type_values:
|
||||
@@ -115,7 +117,8 @@ class AuditPostType(wtypes.Base):
|
||||
goal_id=self.goal,
|
||||
strategy_id=self.strategy,
|
||||
interval=self.interval,
|
||||
scope=self.scope,)
|
||||
scope=self.scope,
|
||||
auto_trigger=self.auto_trigger)
|
||||
|
||||
|
||||
class AuditPatchType(types.JsonPatchType):
|
||||
@@ -257,6 +260,9 @@ class Audit(base.APIBase):
|
||||
scope = wsme.wsattr(types.jsontype, mandatory=False)
|
||||
"""Audit Scope"""
|
||||
|
||||
auto_trigger = wsme.wsattr(bool, mandatory=False, default=False)
|
||||
"""Autoexecute action plan once audit is succeeded"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.fields = []
|
||||
fields = list(objects.Audit.fields)
|
||||
@@ -313,7 +319,8 @@ class Audit(base.APIBase):
|
||||
deleted_at=None,
|
||||
updated_at=datetime.datetime.utcnow(),
|
||||
interval=7200,
|
||||
scope=[])
|
||||
scope=[],
|
||||
auto_trigger=False)
|
||||
|
||||
sample.goal_id = '7ae81bb3-dec3-4289-8d6c-da80bd8001ae'
|
||||
sample.strategy_id = '7ae81bb3-dec3-4289-8d6c-da80bd8001ff'
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
# Copyright 2013 Red Hat, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
|
||||
106
watcher/applier/actions/resize.py
Normal file
106
watcher/applier/actions/resize.py
Normal file
@@ -0,0 +1,106 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2017 Servionica
|
||||
#
|
||||
# Authors: Alexander Chadin <a.chadin@servionica.ru>
|
||||
#
|
||||
# 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
|
||||
import six
|
||||
import voluptuous
|
||||
|
||||
from watcher._i18n import _, _LC
|
||||
from watcher.applier.actions import base
|
||||
from watcher.common import nova_helper
|
||||
from watcher.common import utils
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class Resize(base.BaseAction):
|
||||
"""Resizes a server with specified flavor.
|
||||
|
||||
This action will allow you to resize a server to another flavor.
|
||||
|
||||
The action schema is::
|
||||
|
||||
schema = Schema({
|
||||
'resource_id': str, # should be a UUID
|
||||
'flavor': str, # should be either ID or Name of Flavor
|
||||
})
|
||||
|
||||
The `resource_id` is the UUID of the server to resize.
|
||||
The `flavor` is the ID or Name of Flavor (Nova accepts either ID or Name
|
||||
of Flavor to resize() function).
|
||||
"""
|
||||
|
||||
# input parameters constants
|
||||
FLAVOR = 'flavor'
|
||||
|
||||
def check_resource_id(self, value):
|
||||
if (value is not None and
|
||||
len(value) > 0 and not
|
||||
utils.is_uuid_like(value)):
|
||||
raise voluptuous.Invalid(_("The parameter "
|
||||
"resource_id is invalid."))
|
||||
|
||||
@property
|
||||
def schema(self):
|
||||
return voluptuous.Schema({
|
||||
voluptuous.Required(self.RESOURCE_ID): self.check_resource_id,
|
||||
voluptuous.Required(self.FLAVOR):
|
||||
voluptuous.All(voluptuous.Any(*six.string_types),
|
||||
voluptuous.Length(min=1)),
|
||||
})
|
||||
|
||||
@property
|
||||
def instance_uuid(self):
|
||||
return self.resource_id
|
||||
|
||||
@property
|
||||
def flavor(self):
|
||||
return self.input_parameters.get(self.FLAVOR)
|
||||
|
||||
def resize(self):
|
||||
nova = nova_helper.NovaHelper(osc=self.osc)
|
||||
LOG.debug("Resize instance %s to %s flavor", self.instance_uuid,
|
||||
self.flavor)
|
||||
instance = nova.find_instance(self.instance_uuid)
|
||||
result = None
|
||||
if instance:
|
||||
try:
|
||||
result = nova.resize_instance(
|
||||
instance_id=self.instance_uuid, flavor=self.flavor)
|
||||
except Exception as exc:
|
||||
LOG.exception(exc)
|
||||
LOG.critical(
|
||||
_LC("Unexpected error occurred. Resizing failed for "
|
||||
"instance %s."), self.instance_uuid)
|
||||
return result
|
||||
|
||||
def execute(self):
|
||||
return self.resize()
|
||||
|
||||
def revert(self):
|
||||
return self.migrate(destination=self.source_node)
|
||||
|
||||
def pre_condition(self):
|
||||
# TODO(jed): check if the instance exists / check if the instance is on
|
||||
# the source_node
|
||||
pass
|
||||
|
||||
def post_condition(self):
|
||||
# TODO(jed): check extra parameters (network response, etc.)
|
||||
pass
|
||||
@@ -15,10 +15,12 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
from oslo_concurrency import processutils
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
from taskflow import engines
|
||||
from taskflow.patterns import graph_flow as gf
|
||||
from taskflow import task
|
||||
from taskflow import task as flow_task
|
||||
|
||||
from watcher._i18n import _LE, _LW, _LC
|
||||
from watcher.applier.workflow_engine import base
|
||||
@@ -48,6 +50,18 @@ class DefaultWorkFlowEngine(base.BaseWorkFlowEngine):
|
||||
# (True to allow v execution or False to not).
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def get_config_opts(cls):
|
||||
return [
|
||||
cfg.IntOpt(
|
||||
'max_workers',
|
||||
default=processutils.get_worker_count(),
|
||||
min=1,
|
||||
required=True,
|
||||
help='Number of workers for taskflow engine '
|
||||
'to execute actions.')
|
||||
]
|
||||
|
||||
def execute(self, actions):
|
||||
try:
|
||||
# NOTE(jed) We want to have a strong separation of concern
|
||||
@@ -56,34 +70,32 @@ class DefaultWorkFlowEngine(base.BaseWorkFlowEngine):
|
||||
# We want to provide the 'taskflow' engine by
|
||||
# default although we still want to leave the possibility for
|
||||
# the users to change it.
|
||||
# todo(jed) we need to change the way the actions are stored.
|
||||
# The current implementation only use a linked list of actions.
|
||||
# The current implementation uses graph with linked actions.
|
||||
# todo(jed) add olso conf for retry and name
|
||||
flow = gf.Flow("watcher_flow")
|
||||
previous = None
|
||||
actions_uuid = {}
|
||||
for a in actions:
|
||||
task = TaskFlowActionContainer(a, self)
|
||||
flow.add(task)
|
||||
if previous is None:
|
||||
previous = task
|
||||
# we have only one Action in the Action Plan
|
||||
if len(actions) == 1:
|
||||
nop = TaskFlowNop()
|
||||
flow.add(nop)
|
||||
flow.link(previous, nop)
|
||||
else:
|
||||
# decider == guard (UML)
|
||||
flow.link(previous, task, decider=self.decider)
|
||||
previous = task
|
||||
actions_uuid[a.uuid] = task
|
||||
|
||||
e = engines.load(flow)
|
||||
for a in actions:
|
||||
for parent_id in a.parents:
|
||||
flow.link(actions_uuid[parent_id], actions_uuid[a.uuid],
|
||||
decider=self.decider)
|
||||
|
||||
e = engines.load(
|
||||
flow, engine='parallel',
|
||||
max_workers=self.config.max_workers)
|
||||
e.run()
|
||||
|
||||
return flow
|
||||
|
||||
except Exception as e:
|
||||
raise exception.WorkflowExecutionException(error=e)
|
||||
|
||||
|
||||
class TaskFlowActionContainer(task.Task):
|
||||
class TaskFlowActionContainer(flow_task.Task):
|
||||
def __init__(self, db_action, engine):
|
||||
name = "action_type:{0} uuid:{1}".format(db_action.action_type,
|
||||
db_action.uuid)
|
||||
@@ -148,7 +160,7 @@ class TaskFlowActionContainer(task.Task):
|
||||
LOG.critical(_LC("Oops! We need a disaster recover plan."))
|
||||
|
||||
|
||||
class TaskFlowNop(task.Task):
|
||||
class TaskFlowNop(flow_task.Task):
|
||||
"""This class is used in case of the workflow have only one Action.
|
||||
|
||||
We need at least two atoms to create a link.
|
||||
|
||||
@@ -15,9 +15,9 @@ from cinderclient import client as ciclient
|
||||
from glanceclient import client as glclient
|
||||
from keystoneauth1 import loading as ka_loading
|
||||
from keystoneclient import client as keyclient
|
||||
from monascaclient import client as monclient
|
||||
from neutronclient.neutron import client as netclient
|
||||
from novaclient import client as nvclient
|
||||
from oslo_config import cfg
|
||||
|
||||
from watcher.common import exception
|
||||
|
||||
@@ -41,12 +41,13 @@ class OpenStackClients(object):
|
||||
self._glance = None
|
||||
self._cinder = None
|
||||
self._ceilometer = None
|
||||
self._monasca = None
|
||||
self._neutron = None
|
||||
|
||||
def _get_keystone_session(self):
|
||||
auth = ka_loading.load_auth_from_conf_options(cfg.CONF,
|
||||
auth = ka_loading.load_auth_from_conf_options(CONF,
|
||||
_CLIENTS_AUTH_GROUP)
|
||||
sess = ka_loading.load_session_from_conf_options(cfg.CONF,
|
||||
sess = ka_loading.load_session_from_conf_options(CONF,
|
||||
_CLIENTS_AUTH_GROUP,
|
||||
auth=auth)
|
||||
return sess
|
||||
@@ -62,7 +63,7 @@ class OpenStackClients(object):
|
||||
return self._session
|
||||
|
||||
def _get_client_option(self, client, option):
|
||||
return getattr(getattr(cfg.CONF, '%s_client' % client), option)
|
||||
return getattr(getattr(CONF, '%s_client' % client), option)
|
||||
|
||||
@exception.wrap_keystone_exception
|
||||
def keystone(self):
|
||||
@@ -112,6 +113,35 @@ class OpenStackClients(object):
|
||||
session=self.session)
|
||||
return self._ceilometer
|
||||
|
||||
@exception.wrap_keystone_exception
|
||||
def monasca(self):
|
||||
if self._monasca:
|
||||
return self._monasca
|
||||
|
||||
monascaclient_version = self._get_client_option(
|
||||
'monasca', 'api_version')
|
||||
token = self.session.get_token()
|
||||
watcher_clients_auth_config = CONF.get(_CLIENTS_AUTH_GROUP)
|
||||
service_type = 'monitoring'
|
||||
monasca_kwargs = {
|
||||
'auth_url': watcher_clients_auth_config.auth_url,
|
||||
'cert_file': watcher_clients_auth_config.certfile,
|
||||
'insecure': watcher_clients_auth_config.insecure,
|
||||
'key_file': watcher_clients_auth_config.keyfile,
|
||||
'keystone_timeout': watcher_clients_auth_config.timeout,
|
||||
'os_cacert': watcher_clients_auth_config.cafile,
|
||||
'service_type': service_type,
|
||||
'token': token,
|
||||
'username': watcher_clients_auth_config.username,
|
||||
'password': watcher_clients_auth_config.password,
|
||||
}
|
||||
endpoint = self.session.get_endpoint(service_type=service_type)
|
||||
|
||||
self._monasca = monclient.Client(
|
||||
monascaclient_version, endpoint, **monasca_kwargs)
|
||||
|
||||
return self._monasca
|
||||
|
||||
@exception.wrap_keystone_exception
|
||||
def neutron(self):
|
||||
if self._neutron:
|
||||
|
||||
@@ -258,6 +258,12 @@ class ActionPlanReferenced(Invalid):
|
||||
"multiple actions")
|
||||
|
||||
|
||||
class ActionPlanIsOngoing(Conflict):
|
||||
msg_fmt = _("Action Plan %(action_plan)s is currently running. "
|
||||
"New Action Plan %(new_action_plan)s will be set as "
|
||||
"SUPERSEDED")
|
||||
|
||||
|
||||
class ActionNotFound(ResourceNotFound):
|
||||
msg_fmt = _("Action %(action)s could not be found")
|
||||
|
||||
@@ -276,6 +282,10 @@ class ActionFilterCombinationProhibited(Invalid):
|
||||
"prohibited")
|
||||
|
||||
|
||||
class UnsupportedActionType(UnsupportedError):
|
||||
msg_fmt = _("Provided %(action_type) is not supported yet")
|
||||
|
||||
|
||||
class EfficacyIndicatorNotFound(ResourceNotFound):
|
||||
msg_fmt = _("Efficacy indicator %(efficacy_indicator)s could not be found")
|
||||
|
||||
@@ -368,6 +378,11 @@ class NoMetricValuesForInstance(WatcherException):
|
||||
msg_fmt = _("No values returned by %(resource_id)s for %(metric_name)s.")
|
||||
|
||||
|
||||
class UnsupportedDataSource(UnsupportedError):
|
||||
msg_fmt = _("Datasource %(datasource)s is not supported "
|
||||
"by strategy %(strategy)s")
|
||||
|
||||
|
||||
class NoSuchMetricForHost(WatcherException):
|
||||
msg_fmt = _("No %(metric)s metric for %(host)s found.")
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import time
|
||||
from oslo_log import log
|
||||
|
||||
import cinderclient.exceptions as ciexceptions
|
||||
import glanceclient.exc as glexceptions
|
||||
import novaclient.exceptions as nvexceptions
|
||||
|
||||
from watcher.common import clients
|
||||
@@ -63,6 +64,15 @@ class NovaHelper(object):
|
||||
LOG.exception(exc)
|
||||
raise exception.ComputeNodeNotFound(name=node_hostname)
|
||||
|
||||
def get_instance_list(self):
|
||||
return self.nova.servers.list(search_opts={'all_tenants': True})
|
||||
|
||||
def get_service(self, service_id):
|
||||
return self.nova.services.find(id=service_id)
|
||||
|
||||
def get_flavor(self, flavor_id):
|
||||
return self.nova.flavors.get(flavor_id)
|
||||
|
||||
def get_aggregate_list(self):
|
||||
return self.nova.aggregates.list()
|
||||
|
||||
@@ -142,7 +152,7 @@ class NovaHelper(object):
|
||||
# We'll use the same name for the new instance.
|
||||
imagedict = getattr(instance, "image")
|
||||
image_id = imagedict["id"]
|
||||
image = self.nova.images.get(image_id)
|
||||
image = self.glance.images.get(image_id)
|
||||
new_image_name = getattr(image, "name")
|
||||
|
||||
instance_name = getattr(instance, "name")
|
||||
@@ -304,6 +314,70 @@ class NovaHelper(object):
|
||||
|
||||
return True
|
||||
|
||||
def resize_instance(self, instance_id, flavor, retry=120):
|
||||
"""This method resizes given instance with specified flavor.
|
||||
|
||||
This method uses the Nova built-in resize()
|
||||
action to do a resize of a given instance.
|
||||
|
||||
It returns True if the resize was successful,
|
||||
False otherwise.
|
||||
|
||||
:param instance_id: the unique id of the instance to resize.
|
||||
:param flavor: the name or ID of the flavor to resize to.
|
||||
"""
|
||||
LOG.debug("Trying a resize of instance %s to flavor '%s'" % (
|
||||
instance_id, flavor))
|
||||
|
||||
# Looking for the instance to resize
|
||||
instance = self.find_instance(instance_id)
|
||||
|
||||
flavor_id = None
|
||||
|
||||
try:
|
||||
flavor_id = self.nova.flavors.get(flavor)
|
||||
except nvexceptions.NotFound:
|
||||
flavor_id = [f.id for f in self.nova.flavors.list() if
|
||||
f.name == flavor][0]
|
||||
except nvexceptions.ClientException as e:
|
||||
LOG.debug("Nova client exception occurred while resizing "
|
||||
"instance %s. Exception: %s", instance_id, e)
|
||||
|
||||
if not flavor_id:
|
||||
LOG.debug("Flavor not found: %s" % flavor)
|
||||
return False
|
||||
|
||||
if not instance:
|
||||
LOG.debug("Instance not found: %s" % instance_id)
|
||||
return False
|
||||
|
||||
instance_status = getattr(instance, 'OS-EXT-STS:vm_state')
|
||||
LOG.debug(
|
||||
"Instance %s is in '%s' status." % (instance_id,
|
||||
instance_status))
|
||||
|
||||
instance.resize(flavor=flavor_id)
|
||||
while getattr(instance,
|
||||
'OS-EXT-STS:vm_state') != 'resized' \
|
||||
and retry:
|
||||
instance = self.nova.servers.get(instance.id)
|
||||
LOG.debug(
|
||||
'Waiting the resize of {0} to {1}'.format(
|
||||
instance, flavor_id))
|
||||
time.sleep(1)
|
||||
retry -= 1
|
||||
|
||||
instance_status = getattr(instance, 'status')
|
||||
if instance_status != 'VERIFY_RESIZE':
|
||||
return False
|
||||
|
||||
instance.confirm_resize()
|
||||
|
||||
LOG.debug("Resizing succeeded : instance %s is now on flavor "
|
||||
"'%s'.", instance_id, flavor_id)
|
||||
|
||||
return True
|
||||
|
||||
def live_migrate_instance(self, instance_id, dest_hostname,
|
||||
block_migration=False, retry=120):
|
||||
"""This method does a live migration of a given instance
|
||||
@@ -575,8 +649,8 @@ class NovaHelper(object):
|
||||
return
|
||||
|
||||
try:
|
||||
image = self.nova.images.get(image_id)
|
||||
except nvexceptions.NotFound:
|
||||
image = self.glance.images.get(image_id)
|
||||
except glexceptions.NotFound:
|
||||
LOG.debug("Image '%s' not found " % image_id)
|
||||
return
|
||||
|
||||
@@ -644,6 +718,16 @@ class NovaHelper(object):
|
||||
|
||||
return network_id
|
||||
|
||||
def get_instance_by_uuid(self, instance_uuid):
|
||||
return [instance for instance in
|
||||
self.nova.servers.list(search_opts={"all_tenants": True,
|
||||
"uuid": instance_uuid})]
|
||||
|
||||
def get_instance_by_name(self, instance_name):
|
||||
return [instance for instance in
|
||||
self.nova.servers.list(search_opts={"all_tenants": True,
|
||||
"name": instance_name})]
|
||||
|
||||
def get_instances_by_node(self, host):
|
||||
return [instance for instance in
|
||||
self.nova.servers.list(search_opts={"all_tenants": True})
|
||||
|
||||
@@ -28,6 +28,7 @@ from watcher.conf import db
|
||||
from watcher.conf import decision_engine
|
||||
from watcher.conf import exception
|
||||
from watcher.conf import glance_client
|
||||
from watcher.conf import monasca_client
|
||||
from watcher.conf import neutron_client
|
||||
from watcher.conf import nova_client
|
||||
from watcher.conf import paths
|
||||
@@ -46,6 +47,7 @@ db.register_opts(CONF)
|
||||
planner.register_opts(CONF)
|
||||
applier.register_opts(CONF)
|
||||
decision_engine.register_opts(CONF)
|
||||
monasca_client.register_opts(CONF)
|
||||
nova_client.register_opts(CONF)
|
||||
glance_client.register_opts(CONF)
|
||||
cinder_client.register_opts(CONF)
|
||||
|
||||
@@ -40,7 +40,7 @@ APPLIER_MANAGER_OPTS = [
|
||||
cfg.StrOpt('workflow_engine',
|
||||
default='taskflow',
|
||||
required=True,
|
||||
help='Select the engine to use to execute the workflow')
|
||||
help='Select the engine to use to execute the workflow'),
|
||||
]
|
||||
|
||||
|
||||
|
||||
36
watcher/conf/monasca_client.py
Normal file
36
watcher/conf/monasca_client.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2016 Intel Corp
|
||||
#
|
||||
# Authors: Prudhvi Rao Shedimbi <prudhvi.rao.shedimbi@intel.com>
|
||||
#
|
||||
# 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
|
||||
|
||||
monasca_client = cfg.OptGroup(name='monasca_client',
|
||||
title='Configuration Options for Monasca')
|
||||
|
||||
MONASCA_CLIENT_OPTS = [
|
||||
cfg.StrOpt('api_version',
|
||||
default='2_0',
|
||||
help='Version of Monasca API to use in monascaclient.')]
|
||||
|
||||
|
||||
def register_opts(conf):
|
||||
conf.register_group(monasca_client)
|
||||
conf.register_opts(MONASCA_CLIENT_OPTS, group=monasca_client)
|
||||
|
||||
|
||||
def list_opts():
|
||||
return [('monasca_client', MONASCA_CLIENT_OPTS)]
|
||||
@@ -22,7 +22,7 @@ watcher_planner = cfg.OptGroup(name='watcher_planner',
|
||||
title='Defines the parameters of '
|
||||
'the planner')
|
||||
|
||||
default_planner = 'default'
|
||||
default_planner = 'weight'
|
||||
|
||||
WATCHER_PLANNER_OPTS = {
|
||||
cfg.StrOpt('planner',
|
||||
|
||||
@@ -165,9 +165,9 @@ class CeilometerHelper(object):
|
||||
values = []
|
||||
for index, sample in enumerate(samples):
|
||||
values.append(
|
||||
{'sample_%s' % index: {'timestamp': sample._info['timestamp'],
|
||||
'value': sample._info[
|
||||
'counter_volume']}})
|
||||
{'sample_%s' % index: {
|
||||
'timestamp': sample._info['timestamp'],
|
||||
'value': sample._info['counter_volume']}})
|
||||
return values
|
||||
|
||||
def get_last_sample_value(self, resource_id, meter_name):
|
||||
124
watcher/datasource/monasca.py
Normal file
124
watcher/datasource/monasca.py
Normal file
@@ -0,0 +1,124 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2016 b<>com
|
||||
#
|
||||
# Authors: Vincent FRANCOISE <vincent.francoise@b-com.com>
|
||||
#
|
||||
# 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 datetime
|
||||
|
||||
from monascaclient import exc
|
||||
|
||||
from watcher.common import clients
|
||||
|
||||
|
||||
class MonascaHelper(object):
|
||||
|
||||
def __init__(self, osc=None):
|
||||
""":param osc: an OpenStackClients instance"""
|
||||
self.osc = osc if osc else clients.OpenStackClients()
|
||||
self.monasca = self.osc.monasca()
|
||||
|
||||
def query_retry(self, f, *args, **kwargs):
|
||||
try:
|
||||
return f(*args, **kwargs)
|
||||
except exc.HTTPUnauthorized:
|
||||
self.osc.reset_clients()
|
||||
self.monasca = self.osc.monasca()
|
||||
return f(*args, **kwargs)
|
||||
except Exception:
|
||||
raise
|
||||
|
||||
def _format_time_params(self, start_time, end_time, period):
|
||||
"""Format time-related params to the correct Monasca format
|
||||
|
||||
:param start_time: Start datetime from which metrics will be used
|
||||
:param end_time: End datetime from which metrics will be used
|
||||
:param period: interval in seconds (int)
|
||||
:return: start ISO time, end ISO time, period
|
||||
"""
|
||||
|
||||
if not period:
|
||||
period = int(datetime.timedelta(hours=3).total_seconds())
|
||||
if not start_time:
|
||||
start_time = (
|
||||
datetime.datetime.utcnow() -
|
||||
datetime.timedelta(seconds=period))
|
||||
|
||||
start_timestamp = None if not start_time else start_time.isoformat()
|
||||
end_timestamp = None if not end_time else end_time.isoformat()
|
||||
|
||||
return start_timestamp, end_timestamp, period
|
||||
|
||||
def statistics_list(self, meter_name, dimensions, start_time=None,
|
||||
end_time=None, period=None,):
|
||||
"""List of statistics."""
|
||||
start_timestamp, end_timestamp, period = self._format_time_params(
|
||||
start_time, end_time, period
|
||||
)
|
||||
raw_kwargs = dict(
|
||||
name=meter_name,
|
||||
start_time=start_timestamp,
|
||||
end_time=end_timestamp,
|
||||
dimensions=dimensions,
|
||||
)
|
||||
|
||||
kwargs = {k: v for k, v in raw_kwargs.items() if k and v}
|
||||
|
||||
statistics = self.query_retry(
|
||||
f=self.monasca.metrics.list_measurements, **kwargs)
|
||||
|
||||
return statistics
|
||||
|
||||
def statistic_aggregation(self,
|
||||
meter_name,
|
||||
dimensions,
|
||||
start_time=None,
|
||||
end_time=None,
|
||||
period=None,
|
||||
aggregate='avg',
|
||||
group_by='*'):
|
||||
"""Representing a statistic aggregate by operators
|
||||
|
||||
:param meter_name: meter names of which we want the statistics
|
||||
:param dimensions: dimensions (dict)
|
||||
:param start_time: Start datetime from which metrics will be used
|
||||
:param end_time: End datetime from which metrics will be used
|
||||
:param period: Sampling `period`: In seconds. If no period is given,
|
||||
only one aggregate statistic is returned. If given, a
|
||||
faceted result will be returned, divided into given
|
||||
periods. Periods with no data are ignored.
|
||||
:param aggregate: Should be either 'avg', 'count', 'min' or 'max'
|
||||
:return: A list of dict with each dict being a distinct result row
|
||||
"""
|
||||
start_timestamp, end_timestamp, period = self._format_time_params(
|
||||
start_time, end_time, period
|
||||
)
|
||||
|
||||
raw_kwargs = dict(
|
||||
name=meter_name,
|
||||
start_time=start_timestamp,
|
||||
end_time=end_timestamp,
|
||||
dimensions=dimensions,
|
||||
period=period,
|
||||
statistics=aggregate,
|
||||
group_by=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)
|
||||
|
||||
return statistics
|
||||
@@ -663,6 +663,9 @@ class Connection(api.BaseConnection):
|
||||
if values.get('state') is None:
|
||||
values['state'] = objects.audit.State.PENDING
|
||||
|
||||
if not values.get('auto_trigger'):
|
||||
values['auto_trigger'] = False
|
||||
|
||||
try:
|
||||
audit = self._create(models.Audit, values)
|
||||
except db_exc.DBDuplicateEntry:
|
||||
@@ -743,6 +746,9 @@ class Connection(api.BaseConnection):
|
||||
if not values.get('uuid'):
|
||||
values['uuid'] = utils.generate_uuid()
|
||||
|
||||
if values.get('state') is None:
|
||||
values['state'] = objects.action.State.PENDING
|
||||
|
||||
try:
|
||||
action = self._create(models.Action, values)
|
||||
except db_exc.DBDuplicateEntry:
|
||||
|
||||
@@ -19,6 +19,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
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
@@ -176,6 +177,7 @@ class Audit(Base):
|
||||
goal_id = Column(Integer, ForeignKey('goals.id'), nullable=False)
|
||||
strategy_id = Column(Integer, ForeignKey('strategies.id'), nullable=True)
|
||||
scope = Column(JSONEncodedList, nullable=True)
|
||||
auto_trigger = Column(Boolean, nullable=False)
|
||||
|
||||
goal = orm.relationship(Goal, foreign_keys=goal_id, lazy=None)
|
||||
strategy = orm.relationship(Strategy, foreign_keys=strategy_id, lazy=None)
|
||||
@@ -191,7 +193,6 @@ class ActionPlan(Base):
|
||||
)
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
uuid = Column(String(36))
|
||||
first_action_id = Column(Integer)
|
||||
audit_id = Column(Integer, ForeignKey('audits.id'), nullable=False)
|
||||
strategy_id = Column(Integer, ForeignKey('strategies.id'), nullable=False)
|
||||
state = Column(String(20), nullable=True)
|
||||
@@ -217,7 +218,7 @@ class Action(Base):
|
||||
action_type = Column(String(255), nullable=False)
|
||||
input_parameters = Column(JSONEncodedDict, nullable=True)
|
||||
state = Column(String(20), nullable=True)
|
||||
next = Column(String(36), nullable=True)
|
||||
parents = Column(JSONEncodedList, nullable=True)
|
||||
|
||||
action_plan = orm.relationship(
|
||||
ActionPlan, foreign_keys=action_plan_id, lazy=None)
|
||||
|
||||
@@ -22,6 +22,8 @@ import six
|
||||
|
||||
from oslo_log import log
|
||||
|
||||
from watcher.applier import rpcapi
|
||||
from watcher.common import exception
|
||||
from watcher.decision_engine.planner import manager as planner_manager
|
||||
from watcher.decision_engine.strategy.context import default as default_context
|
||||
from watcher import notifications
|
||||
@@ -79,11 +81,13 @@ class AuditHandler(BaseAuditHandler):
|
||||
request_context, audit,
|
||||
action=fields.NotificationAction.PLANNER,
|
||||
phase=fields.NotificationPhase.START)
|
||||
self.planner.schedule(request_context, audit.id, solution)
|
||||
action_plan = self.planner.schedule(request_context, audit.id,
|
||||
solution)
|
||||
notifications.audit.send_action_notification(
|
||||
request_context, audit,
|
||||
action=fields.NotificationAction.PLANNER,
|
||||
phase=fields.NotificationPhase.END)
|
||||
return action_plan
|
||||
except Exception:
|
||||
notifications.audit.send_action_notification(
|
||||
request_context, audit,
|
||||
@@ -104,15 +108,30 @@ class AuditHandler(BaseAuditHandler):
|
||||
self.update_audit_state(audit, objects.audit.State.ONGOING)
|
||||
|
||||
def post_execute(self, audit, solution, request_context):
|
||||
self.do_schedule(request_context, audit, solution)
|
||||
# change state of the audit to SUCCEEDED
|
||||
self.update_audit_state(audit, objects.audit.State.SUCCEEDED)
|
||||
action_plan = self.do_schedule(request_context, audit, solution)
|
||||
a_plan_filters = {'state': objects.action_plan.State.ONGOING}
|
||||
ongoing_action_plans = objects.ActionPlan.list(
|
||||
request_context, filters=a_plan_filters)
|
||||
if ongoing_action_plans:
|
||||
action_plan.state = objects.action_plan.State.SUPERSEDED
|
||||
action_plan.save()
|
||||
raise exception.ActionPlanIsOngoing(
|
||||
action_plan=ongoing_action_plans[0].uuid,
|
||||
new_action_plan=action_plan.uuid)
|
||||
elif audit.auto_trigger:
|
||||
applier_client = rpcapi.ApplierAPI()
|
||||
applier_client.launch_action_plan(request_context,
|
||||
action_plan.uuid)
|
||||
|
||||
def execute(self, audit, request_context):
|
||||
try:
|
||||
self.pre_execute(audit, request_context)
|
||||
solution = self.do_execute(audit, request_context)
|
||||
self.post_execute(audit, solution, request_context)
|
||||
except exception.ActionPlanIsOngoing as e:
|
||||
LOG.exception(e)
|
||||
if audit.audit_type == objects.audit.AuditType.ONESHOT.value:
|
||||
self.update_audit_state(audit, objects.audit.State.CANCELLED)
|
||||
except Exception as e:
|
||||
LOG.exception(e)
|
||||
self.update_audit_state(audit, objects.audit.State.FAILED)
|
||||
|
||||
@@ -82,9 +82,6 @@ class ContinuousAuditHandler(base.AuditHandler):
|
||||
if not self._is_audit_inactive(audit):
|
||||
self.execute(audit, request_context)
|
||||
|
||||
def post_execute(self, audit, solution, request_context):
|
||||
self.do_schedule(request_context, audit, solution)
|
||||
|
||||
def launch_audits_periodically(self):
|
||||
audit_context = context.RequestContext(is_admin=True)
|
||||
audit_filters = {
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
# limitations under the License.
|
||||
|
||||
from watcher.decision_engine.audit import base
|
||||
from watcher import objects
|
||||
|
||||
|
||||
class OneShotAuditHandler(base.AuditHandler):
|
||||
@@ -24,3 +25,9 @@ class OneShotAuditHandler(base.AuditHandler):
|
||||
audit, request_context)
|
||||
|
||||
return solution
|
||||
|
||||
def post_execute(self, audit, solution, request_context):
|
||||
super(OneShotAuditHandler, self).post_execute(audit, solution,
|
||||
request_context)
|
||||
# change state of the audit to SUCCEEDED
|
||||
self.update_audit_state(audit, objects.audit.State.SUCCEEDED)
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
#
|
||||
# Authors: Jean-Emile DARTOIS <jean-emile.dartois@b-com.com>
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
"""
|
||||
The :ref:`Cluster History <cluster_history_definition>` contains all the
|
||||
previously collected timestamped data such as metrics and events associated
|
||||
to any :ref:`managed resource <managed_resource_definition>` of the
|
||||
:ref:`Cluster <cluster_definition>`.
|
||||
|
||||
Just like the :ref:`Cluster Data Model <cluster_data_model_definition>`, this
|
||||
history may be used by any :ref:`Strategy <strategy_definition>` in order to
|
||||
find the most optimal :ref:`Solution <solution_definition>` during an
|
||||
:ref:`Audit <audit_definition>`.
|
||||
|
||||
In the Watcher project, a generic
|
||||
:ref:`Cluster History <cluster_history_definition>`
|
||||
API is proposed with some helper classes in order to :
|
||||
|
||||
- share a common measurement (events or metrics) naming based on what is
|
||||
defined in Ceilometer.
|
||||
See `the full list of available measurements <http://docs.openstack.org/admin-guide-cloud/telemetry-measurements.html>`_
|
||||
- share common meter types (Cumulative, Delta, Gauge) based on what is
|
||||
defined in Ceilometer.
|
||||
See `the full list of meter types <http://docs.openstack.org/admin-guide-cloud/telemetry-measurements.html>`_
|
||||
- simplify the development of a new :ref:`Strategy <strategy_definition>`
|
||||
- avoid duplicating the same code in several
|
||||
:ref:`Strategies <strategy_definition>`
|
||||
- have a better consistency between the different
|
||||
:ref:`Strategies <strategy_definition>`
|
||||
- avoid any strong coupling with any external metrics/events storage system
|
||||
(the proposed API and measurement naming system acts as a pivot format)
|
||||
|
||||
Note however that a developer can use his/her own history management system if
|
||||
the Ceilometer system does not fit his/her needs as long as the
|
||||
:ref:`Strategy <strategy_definition>` is able to produce a
|
||||
:ref:`Solution <solution_definition>` for the requested
|
||||
:ref:`Goal <goal_definition>`.
|
||||
|
||||
The :ref:`Cluster History <cluster_history_definition>` data may be persisted
|
||||
in any appropriate storage system (InfluxDB, OpenTSDB, MongoDB,...).
|
||||
""" # noqa
|
||||
|
||||
import abc
|
||||
import six
|
||||
|
||||
"""Work in progress Helper to query metrics"""
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class BaseClusterHistory(object):
|
||||
@abc.abstractmethod
|
||||
def statistic_aggregation(self, resource_id, meter_name, period,
|
||||
aggregate='avg'):
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_last_sample_values(self, resource_id, meter_name, limit=1):
|
||||
raise NotImplementedError()
|
||||
|
||||
def query_sample(self, meter_name, query, limit=1):
|
||||
raise NotImplementedError()
|
||||
|
||||
def statistic_list(self, meter_name, query=None, period=None):
|
||||
raise NotImplementedError()
|
||||
@@ -1,45 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
#
|
||||
# Authors: Jean-Emile DARTOIS <jean-emile.dartois@b-com.com>
|
||||
#
|
||||
# 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 watcher.common import ceilometer_helper
|
||||
|
||||
from watcher.decision_engine.cluster.history import base
|
||||
|
||||
|
||||
class CeilometerClusterHistory(base.BaseClusterHistory):
|
||||
def __init__(self, osc=None):
|
||||
""":param osc: an OpenStackClients instance"""
|
||||
super(CeilometerClusterHistory, self).__init__()
|
||||
self.ceilometer = ceilometer_helper.CeilometerHelper(osc=osc)
|
||||
|
||||
def statistic_list(self, meter_name, query=None, period=None):
|
||||
return self.ceilometer.statistic_list(meter_name, query, period)
|
||||
|
||||
def query_sample(self, meter_name, query, limit=1):
|
||||
return self.ceilometer.query_sample(meter_name, query, limit)
|
||||
|
||||
def get_last_sample_values(self, resource_id, meter_name, limit=1):
|
||||
return self.ceilometer.get_last_sample_values(resource_id, meter_name,
|
||||
limit)
|
||||
|
||||
def statistic_aggregation(self, resource_id, meter_name, period,
|
||||
aggregate='avg'):
|
||||
return self.ceilometer.statistic_aggregation(resource_id, meter_name,
|
||||
period,
|
||||
aggregate)
|
||||
@@ -104,6 +104,20 @@ class MigrationEfficacy(IndicatorSpecification):
|
||||
voluptuous.Range(min=0, max=100), required=True)
|
||||
|
||||
|
||||
class ComputeNodesCount(IndicatorSpecification):
|
||||
def __init__(self):
|
||||
super(ComputeNodesCount, self).__init__(
|
||||
name="compute_nodes_count",
|
||||
description=_("The total number of enabled compute nodes."),
|
||||
unit=None,
|
||||
)
|
||||
|
||||
@property
|
||||
def schema(self):
|
||||
return voluptuous.Schema(
|
||||
voluptuous.Range(min=0), required=True)
|
||||
|
||||
|
||||
class ReleasedComputeNodesCount(IndicatorSpecification):
|
||||
def __init__(self):
|
||||
super(ReleasedComputeNodesCount, self).__init__(
|
||||
|
||||
@@ -33,20 +33,21 @@ class ServerConsolidation(base.EfficacySpecification):
|
||||
|
||||
def get_indicators_specifications(self):
|
||||
return [
|
||||
indicators.ComputeNodesCount(),
|
||||
indicators.ReleasedComputeNodesCount(),
|
||||
indicators.InstanceMigrationsCount(),
|
||||
]
|
||||
|
||||
def get_global_efficacy_indicator(self, indicators_map=None):
|
||||
value = 0
|
||||
if indicators_map and indicators_map.instance_migrations_count > 0:
|
||||
if indicators_map and indicators_map.compute_nodes_count > 0:
|
||||
value = (float(indicators_map.released_compute_nodes_count) /
|
||||
float(indicators_map.instance_migrations_count)) * 100
|
||||
float(indicators_map.compute_nodes_count)) * 100
|
||||
|
||||
return efficacy.Indicator(
|
||||
name="released_nodes_ratio",
|
||||
description=_("Ratio of released compute nodes divided by the "
|
||||
"number of VM migrations."),
|
||||
"total number of enabled compute nodes."),
|
||||
unit='%',
|
||||
value=value,
|
||||
)
|
||||
|
||||
@@ -34,3 +34,7 @@ class Model(object):
|
||||
@abc.abstractmethod
|
||||
def to_string(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def to_xml(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
@@ -108,12 +108,15 @@ import copy
|
||||
import threading
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
import six
|
||||
|
||||
from watcher.common import clients
|
||||
from watcher.common.loader import loadable
|
||||
from watcher.decision_engine.model import model_root
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class BaseClusterDataModelCollector(loadable.LoadableSingleton):
|
||||
@@ -169,6 +172,8 @@ class BaseClusterDataModelCollector(loadable.LoadableSingleton):
|
||||
]
|
||||
|
||||
def get_latest_cluster_data_model(self):
|
||||
LOG.debug("Creating copy")
|
||||
LOG.debug(self.cluster_data_model.to_xml())
|
||||
return copy.deepcopy(self.cluster_data_model)
|
||||
|
||||
def synchronize(self):
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
#
|
||||
# Authors: Jean-Emile DARTOIS <jean-emile.dartois@b-com.com>
|
||||
# Copyright (c) 2017 Intel Innovation and Research Ireland Ltd.
|
||||
#
|
||||
# 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
|
||||
# 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.
|
||||
# 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 import exception
|
||||
from watcher.common import nova_helper
|
||||
from watcher.decision_engine.model.collector import base
|
||||
from watcher.decision_engine.model import element
|
||||
@@ -30,13 +28,12 @@ LOG = log.getLogger(__name__)
|
||||
class NovaClusterDataModelCollector(base.BaseClusterDataModelCollector):
|
||||
"""Nova cluster data model collector
|
||||
|
||||
The Nova cluster data model collector creates an in-memory
|
||||
representation of the resources exposed by the compute service.
|
||||
The Nova cluster data model collector creates an in-memory
|
||||
representation of the resources exposed by the compute service.
|
||||
"""
|
||||
|
||||
def __init__(self, config, osc=None):
|
||||
super(NovaClusterDataModelCollector, self).__init__(config, osc)
|
||||
self.wrapper = nova_helper.NovaHelper(osc=self.osc)
|
||||
|
||||
@property
|
||||
def notification_endpoints(self):
|
||||
@@ -62,49 +59,312 @@ class NovaClusterDataModelCollector(base.BaseClusterDataModelCollector):
|
||||
"""Build the compute cluster data model"""
|
||||
LOG.debug("Building latest Nova cluster data model")
|
||||
|
||||
model = model_root.ModelRoot()
|
||||
mem = element.Resource(element.ResourceType.memory)
|
||||
num_cores = element.Resource(element.ResourceType.cpu_cores)
|
||||
disk = element.Resource(element.ResourceType.disk)
|
||||
disk_capacity = element.Resource(element.ResourceType.disk_capacity)
|
||||
model.create_resource(mem)
|
||||
model.create_resource(num_cores)
|
||||
model.create_resource(disk)
|
||||
model.create_resource(disk_capacity)
|
||||
builder = ModelBuilder(self.osc)
|
||||
return builder.execute()
|
||||
|
||||
flavor_cache = {}
|
||||
nodes = self.wrapper.get_compute_node_list()
|
||||
for n in nodes:
|
||||
service = self.wrapper.nova.services.find(id=n.service['id'])
|
||||
# create node in cluster_model_collector
|
||||
node = element.ComputeNode(n.id)
|
||||
node.uuid = service.host
|
||||
node.hostname = n.hypervisor_hostname
|
||||
# set capacity
|
||||
mem.set_capacity(node, n.memory_mb)
|
||||
disk.set_capacity(node, n.free_disk_gb)
|
||||
disk_capacity.set_capacity(node, n.local_gb)
|
||||
num_cores.set_capacity(node, n.vcpus)
|
||||
node.state = n.state
|
||||
node.status = n.status
|
||||
model.add_node(node)
|
||||
instances = self.wrapper.get_instances_by_node(str(service.host))
|
||||
for v in instances:
|
||||
# create VM in cluster_model_collector
|
||||
instance = element.Instance()
|
||||
instance.uuid = v.id
|
||||
# nova/nova/compute/instance_states.py
|
||||
instance.state = getattr(v, 'OS-EXT-STS:vm_state')
|
||||
|
||||
# set capacity
|
||||
self.wrapper.get_flavor_instance(v, flavor_cache)
|
||||
mem.set_capacity(instance, v.flavor['ram'])
|
||||
# FIXME: update all strategies to use disk_capacity
|
||||
# for instances instead of disk
|
||||
disk.set_capacity(instance, v.flavor['disk'])
|
||||
disk_capacity.set_capacity(instance, v.flavor['disk'])
|
||||
num_cores.set_capacity(instance, v.flavor['vcpus'])
|
||||
class ModelBuilder(object):
|
||||
"""Build the graph-based model
|
||||
|
||||
model.map_instance(instance, node)
|
||||
This model builder adds the following data"
|
||||
|
||||
return model
|
||||
- Compute-related knowledge (Nova)
|
||||
- TODO(v-francoise): Storage-related knowledge (Cinder)
|
||||
- TODO(v-francoise): Network-related knowledge (Neutron)
|
||||
|
||||
NOTE(v-francoise): This model builder is meant to be extended in the future
|
||||
to also include both storage and network information respectively coming
|
||||
from Cinder and Neutron. Some prelimary work has been done in this
|
||||
direction in https://review.openstack.org/#/c/362730 but since we cannot
|
||||
guarantee a sufficient level of consistency for neither the storage nor the
|
||||
network part before the end of the Ocata cycle, this work has been
|
||||
re-scheduled for Pike. In the meantime, all the associated code has been
|
||||
commented out.
|
||||
"""
|
||||
def __init__(self, osc):
|
||||
self.osc = osc
|
||||
self.model = model_root.ModelRoot()
|
||||
self.nova = osc.nova()
|
||||
self.nova_helper = nova_helper.NovaHelper(osc=self.osc)
|
||||
# self.neutron = osc.neutron()
|
||||
# self.cinder = osc.cinder()
|
||||
|
||||
def _add_physical_layer(self):
|
||||
"""Add the physical layer of the graph.
|
||||
|
||||
This includes components which represent actual infrastructure
|
||||
hardware.
|
||||
"""
|
||||
for cnode in self.nova_helper.get_compute_node_list():
|
||||
self.add_compute_node(cnode)
|
||||
|
||||
def add_compute_node(self, node):
|
||||
# Build and add base node.
|
||||
compute_node = self.build_compute_node(node)
|
||||
self.model.add_node(compute_node)
|
||||
|
||||
# NOTE(v-francoise): we can encapsulate capabilities of the node
|
||||
# (special instruction sets of CPUs) in the attributes; as well as
|
||||
# sub-nodes can be added re-presenting e.g. GPUs/Accelerators etc.
|
||||
|
||||
# # Build & add disk, memory, network and cpu nodes.
|
||||
# disk_id, disk_node = self.build_disk_compute_node(base_id, node)
|
||||
# self.add_node(disk_id, disk_node)
|
||||
# mem_id, mem_node = self.build_memory_compute_node(base_id, node)
|
||||
# self.add_node(mem_id, mem_node)
|
||||
# net_id, net_node = self._build_network_compute_node(base_id)
|
||||
# self.add_node(net_id, net_node)
|
||||
# cpu_id, cpu_node = self.build_cpu_compute_node(base_id, node)
|
||||
# self.add_node(cpu_id, cpu_node)
|
||||
|
||||
# # Connect the base compute node to the dependant nodes.
|
||||
# self.add_edges_from([(base_id, disk_id), (base_id, mem_id),
|
||||
# (base_id, cpu_id), (base_id, net_id)],
|
||||
# label="contains")
|
||||
|
||||
def build_compute_node(self, node):
|
||||
"""Build a compute node from a Nova compute node
|
||||
|
||||
:param node: A node hypervisor instance
|
||||
:type node: :py:class:`~novaclient.v2.hypervisors.Hypervisor`
|
||||
"""
|
||||
# build up the compute node.
|
||||
compute_service = self.nova_helper.get_service(node.service["id"])
|
||||
node_attributes = {
|
||||
"id": node.id,
|
||||
"uuid": compute_service.host,
|
||||
"hostname": node.hypervisor_hostname,
|
||||
"memory": node.memory_mb,
|
||||
"disk": node.free_disk_gb,
|
||||
"disk_capacity": node.local_gb,
|
||||
"vcpus": node.vcpus,
|
||||
"state": node.state,
|
||||
"status": node.status}
|
||||
|
||||
compute_node = element.ComputeNode(**node_attributes)
|
||||
# compute_node = self._build_node("physical", "compute", "hypervisor",
|
||||
# node_attributes)
|
||||
return compute_node
|
||||
|
||||
# def _build_network_compute_node(self, base_node):
|
||||
# attributes = {}
|
||||
# net_node = self._build_node("physical", "network", "NIC", attributes)
|
||||
# net_id = "{}_network".format(base_node)
|
||||
# return net_id, net_node
|
||||
|
||||
# def build_disk_compute_node(self, base_node, compute):
|
||||
# # Build disk node attributes.
|
||||
# disk_attributes = {
|
||||
# "size_gb": compute.local_gb,
|
||||
# "used_gb": compute.local_gb_used,
|
||||
# "available_gb": compute.free_disk_gb}
|
||||
# disk_node = self._build_node("physical", "storage", "disk",
|
||||
# disk_attributes)
|
||||
# disk_id = "{}_disk".format(base_node)
|
||||
# return disk_id, disk_node
|
||||
|
||||
# def build_memory_compute_node(self, base_node, compute):
|
||||
# # Build memory node attributes.
|
||||
# memory_attrs = {"size_mb": compute.memory_mb,
|
||||
# "used_mb": compute.memory_mb_used,
|
||||
# "available_mb": compute.free_ram_mb}
|
||||
# memory_node = self._build_node("physical", "memory", "memory",
|
||||
# memory_attrs)
|
||||
# memory_id = "{}_memory".format(base_node)
|
||||
# return memory_id, memory_node
|
||||
|
||||
# def build_cpu_compute_node(self, base_node, compute):
|
||||
# # Build memory node attributes.
|
||||
# cpu_attributes = {"vcpus": compute.vcpus,
|
||||
# "vcpus_used": compute.vcpus_used,
|
||||
# "info": jsonutils.loads(compute.cpu_info)}
|
||||
# cpu_node = self._build_node("physical", "cpu", "cpu", cpu_attributes)
|
||||
# cpu_id = "{}_cpu".format(base_node)
|
||||
# return cpu_id, cpu_node
|
||||
|
||||
# @staticmethod
|
||||
# def _build_node(layer, category, node_type, attributes):
|
||||
# return {"layer": layer, "category": category, "type": node_type,
|
||||
# "attributes": attributes}
|
||||
|
||||
def _add_virtual_layer(self):
|
||||
"""Add the virtual layer to the graph.
|
||||
|
||||
This layer is the virtual components of the infrastructure,
|
||||
such as vms.
|
||||
"""
|
||||
self._add_virtual_servers()
|
||||
# self._add_virtual_network()
|
||||
# self._add_virtual_storage()
|
||||
|
||||
def _add_virtual_servers(self):
|
||||
all_instances = self.nova_helper.get_instance_list()
|
||||
for inst in all_instances:
|
||||
# Add Node
|
||||
instance = self._build_instance_node(inst)
|
||||
self.model.add_instance(instance)
|
||||
# Get the cnode_name uuid.
|
||||
cnode_uuid = getattr(inst, "OS-EXT-SRV-ATTR:host")
|
||||
if cnode_uuid is None:
|
||||
# The instance is not attached to any Compute node
|
||||
continue
|
||||
try:
|
||||
# Nova compute node
|
||||
# cnode = self.nova_helper.get_compute_node_by_hostname(
|
||||
# cnode_uuid)
|
||||
compute_node = self.model.get_node_by_uuid(
|
||||
cnode_uuid)
|
||||
# Connect the instance to its compute node
|
||||
self.model.add_edge(
|
||||
instance, compute_node, label='RUNS_ON')
|
||||
except exception.ComputeNodeNotFound:
|
||||
continue
|
||||
|
||||
def _build_instance_node(self, instance):
|
||||
"""Build an instance node
|
||||
|
||||
Create an instance node for the graph using nova and the
|
||||
`server` nova object.
|
||||
:param instance: Nova VM object.
|
||||
:return: A instance node for the graph.
|
||||
"""
|
||||
flavor = self.nova_helper.get_flavor(instance.flavor["id"])
|
||||
instance_attributes = {
|
||||
"uuid": instance.id,
|
||||
"human_id": instance.human_id,
|
||||
"memory": flavor.ram,
|
||||
"disk": flavor.disk,
|
||||
"disk_capacity": flavor.disk,
|
||||
"vcpus": flavor.vcpus,
|
||||
"state": getattr(instance, "OS-EXT-STS:vm_state")}
|
||||
|
||||
# node_attributes = dict()
|
||||
# node_attributes["layer"] = "virtual"
|
||||
# node_attributes["category"] = "compute"
|
||||
# node_attributes["type"] = "compute"
|
||||
# node_attributes["attributes"] = instance_attributes
|
||||
return element.Instance(**instance_attributes)
|
||||
|
||||
# def _add_virtual_storage(self):
|
||||
# try:
|
||||
# volumes = self.cinder.volumes.list()
|
||||
# except Exception:
|
||||
# return
|
||||
# for volume in volumes:
|
||||
# volume_id, volume_node = self._build_storage_node(volume)
|
||||
# self.add_node(volume_id, volume_node)
|
||||
# host = self._get_volume_host_id(volume_node)
|
||||
# self.add_edge(volume_id, host)
|
||||
# # Add connections to an instance.
|
||||
# if volume_node['attributes']['attachments']:
|
||||
# for attachment in volume_node['attributes']['attachments']:
|
||||
# self.add_edge(volume_id, attachment['server_id'],
|
||||
# label='ATTACHED_TO')
|
||||
# volume_node['attributes'].pop('attachments')
|
||||
|
||||
# def _add_virtual_network(self):
|
||||
# try:
|
||||
# routers = self.neutron.list_routers()
|
||||
# except Exception:
|
||||
# return
|
||||
|
||||
# for network in self.neutron.list_networks()['networks']:
|
||||
# self.add_node(*self._build_network(network))
|
||||
|
||||
# for router in routers['routers']:
|
||||
# self.add_node(*self._build_router(router))
|
||||
|
||||
# router_interfaces, _, compute_ports = self._group_ports()
|
||||
# for router_interface in router_interfaces:
|
||||
# interface = self._build_router_interface(router_interface)
|
||||
# router_interface_id = interface[0]
|
||||
# router_interface_node = interface[1]
|
||||
# router_id = interface[2]
|
||||
# self.add_node(router_interface_id, router_interface_node)
|
||||
# self.add_edge(router_id, router_interface_id)
|
||||
# network_id = router_interface_node['attributes']['network_id']
|
||||
# self.add_edge(router_interface_id, network_id)
|
||||
|
||||
# for compute_port in compute_ports:
|
||||
# cp_id, cp_node, instance_id = self._build_compute_port_node(
|
||||
# compute_port)
|
||||
# self.add_node(cp_id, cp_node)
|
||||
# self.add_edge(cp_id, vm_id)
|
||||
# net_id = cp_node['attributes']['network_id']
|
||||
# self.add_edge(net_id, cp_id)
|
||||
# # Connect port to physical node
|
||||
# phys_net_node = "{}_network".format(cp_node['attributes']
|
||||
# ['binding:host_id'])
|
||||
# self.add_edge(cp_id, phys_net_node)
|
||||
|
||||
# def _get_volume_host_id(self, volume_node):
|
||||
# host = volume_node['attributes']['os-vol-host-attr:host']
|
||||
# if host.find('@') != -1:
|
||||
# host = host.split('@')[0]
|
||||
# elif host.find('#') != -1:
|
||||
# host = host.split('#')[0]
|
||||
# return "{}_disk".format(host)
|
||||
|
||||
# def _build_storage_node(self, volume_obj):
|
||||
# volume = volume_obj.__dict__
|
||||
# volume["name"] = volume["id"]
|
||||
# volume.pop("id")
|
||||
# volume.pop("manager")
|
||||
# node = self._build_node("virtual", "storage", 'volume', volume)
|
||||
# return volume["name"], node
|
||||
|
||||
# def _build_compute_port_node(self, compute_port):
|
||||
# compute_port["name"] = compute_port["id"]
|
||||
# compute_port.pop("id")
|
||||
# nde_type = "{}_port".format(
|
||||
# compute_port["device_owner"].split(":")[0])
|
||||
# compute_port.pop("device_owner")
|
||||
# device_id = compute_port["device_id"]
|
||||
# compute_port.pop("device_id")
|
||||
# node = self._build_node("virtual", "network", nde_type, compute_port)
|
||||
# return compute_port["name"], node, device_id
|
||||
|
||||
# def _group_ports(self):
|
||||
# router_interfaces = []
|
||||
# floating_ips = []
|
||||
# compute_ports = []
|
||||
# interface_types = ["network:router_interface",
|
||||
# 'network:router_gateway']
|
||||
|
||||
# for port in self.neutron.list_ports()['ports']:
|
||||
# if port['device_owner'] in interface_types:
|
||||
# router_interfaces.append(port)
|
||||
# elif port['device_owner'].startswith('compute:'):
|
||||
# compute_ports.append(port)
|
||||
# elif port['device_owner'] == 'network:floatingip':
|
||||
# floating_ips.append(port)
|
||||
|
||||
# return router_interfaces, floating_ips, compute_ports
|
||||
|
||||
# def _build_router_interface(self, interface):
|
||||
# interface["name"] = interface["id"]
|
||||
# interface.pop("id")
|
||||
# node_type = interface["device_owner"].split(":")[1]
|
||||
# node = self._build_node("virtual", "network", node_type, interface)
|
||||
# return interface["name"], node, interface["device_id"]
|
||||
|
||||
# def _build_router(self, router):
|
||||
# router_attrs = {"uuid": router['id'],
|
||||
# "name": router['name'],
|
||||
# "state": router['status']}
|
||||
# node = self._build_node('virtual', 'network', 'router', router_attrs)
|
||||
# return str(router['id']), node
|
||||
|
||||
# def _build_network(self, network):
|
||||
# node = self._build_node('virtual', 'network', 'network', network)
|
||||
# return network['id'], node
|
||||
|
||||
def execute(self):
|
||||
"""Instantiates the graph with the openstack cluster data.
|
||||
|
||||
The graph is populated along 2 layers: virtual and physical. As each
|
||||
new layer is built connections are made back to previous layers.
|
||||
"""
|
||||
self._add_physical_layer()
|
||||
self._add_virtual_layer()
|
||||
return self.model
|
||||
|
||||
@@ -16,10 +16,8 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from watcher.decision_engine.model.element import disk_info
|
||||
from watcher.decision_engine.model.element import instance
|
||||
from watcher.decision_engine.model.element import node
|
||||
from watcher.decision_engine.model.element import resource
|
||||
|
||||
ServiceState = node.ServiceState
|
||||
ComputeNode = node.ComputeNode
|
||||
@@ -27,12 +25,4 @@ ComputeNode = node.ComputeNode
|
||||
InstanceState = instance.InstanceState
|
||||
Instance = instance.Instance
|
||||
|
||||
DiskInfo = disk_info.DiskInfo
|
||||
|
||||
ResourceType = resource.ResourceType
|
||||
Resource = resource.Resource
|
||||
|
||||
|
||||
__all__ = [
|
||||
'ServiceState', 'ComputeNode', 'InstanceState', 'Instance',
|
||||
'DiskInfo', 'ResourceType', 'Resource']
|
||||
__all__ = ['ServiceState', 'ComputeNode', 'InstanceState', 'Instance']
|
||||
|
||||
@@ -17,13 +17,51 @@
|
||||
# limitations under the License.
|
||||
|
||||
import abc
|
||||
import collections
|
||||
|
||||
from lxml import etree
|
||||
from oslo_log import log
|
||||
import six
|
||||
|
||||
from watcher.objects import base
|
||||
from watcher.objects import fields as wfields
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class Element(object):
|
||||
class Element(base.WatcherObject, base.WatcherObjectDictCompat):
|
||||
|
||||
# Initial version
|
||||
VERSION = '1.0'
|
||||
|
||||
fields = {}
|
||||
|
||||
def __init__(self, context=None, **kwargs):
|
||||
for name, field in self.fields.items():
|
||||
# The idea here is to force the initialization of unspecified
|
||||
# fields that have a default value
|
||||
if (name not in kwargs and not field.nullable and
|
||||
field.default != wfields.UnspecifiedDefault):
|
||||
kwargs[name] = field.default
|
||||
super(Element, self).__init__(context, **kwargs)
|
||||
|
||||
@abc.abstractmethod
|
||||
def accept(self, visitor):
|
||||
raise NotImplementedError()
|
||||
|
||||
def as_xml_element(self):
|
||||
sorted_fieldmap = []
|
||||
for field in self.fields:
|
||||
try:
|
||||
value = str(self[field])
|
||||
sorted_fieldmap.append((field, value))
|
||||
except Exception as exc:
|
||||
LOG.exception(exc)
|
||||
|
||||
attrib = collections.OrderedDict(sorted_fieldmap)
|
||||
|
||||
element_name = self.__class__.__name__
|
||||
instance_el = etree.Element(element_name, attrib=attrib)
|
||||
|
||||
return instance_el
|
||||
|
||||
@@ -19,39 +19,15 @@ import abc
|
||||
import six
|
||||
|
||||
from watcher.decision_engine.model.element import base
|
||||
from watcher.objects import fields as wfields
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class ComputeResource(base.Element):
|
||||
|
||||
def __init__(self):
|
||||
self._uuid = ""
|
||||
self._human_id = ""
|
||||
self._hostname = ""
|
||||
VERSION = '1.0'
|
||||
|
||||
@property
|
||||
def uuid(self):
|
||||
return self._uuid
|
||||
|
||||
@uuid.setter
|
||||
def uuid(self, u):
|
||||
self._uuid = u
|
||||
|
||||
@property
|
||||
def hostname(self):
|
||||
return self._hostname
|
||||
|
||||
@hostname.setter
|
||||
def hostname(self, h):
|
||||
self._hostname = h
|
||||
|
||||
@property
|
||||
def human_id(self):
|
||||
return self._human_id
|
||||
|
||||
@human_id.setter
|
||||
def human_id(self, h):
|
||||
self._human_id = h
|
||||
|
||||
def __str__(self):
|
||||
return "[{0}]".format(self.uuid)
|
||||
fields = {
|
||||
"uuid": wfields.StringField(),
|
||||
"human_id": wfields.StringField(default=""),
|
||||
}
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
#
|
||||
# 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 watcher.decision_engine.model.element import base
|
||||
|
||||
|
||||
class DiskInfo(base.Element):
|
||||
|
||||
def __init__(self):
|
||||
self.name = ""
|
||||
self.major = 0
|
||||
self.minor = 0
|
||||
self.size = 0
|
||||
self.scheduler = ""
|
||||
|
||||
def accept(self, visitor):
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_size(self, size):
|
||||
"""DiskInfo
|
||||
|
||||
:param size: Size in bytes
|
||||
"""
|
||||
self.size = size
|
||||
|
||||
def get_size(self):
|
||||
return self.size
|
||||
|
||||
def set_scheduler(self, scheduler):
|
||||
"""DiskInfo
|
||||
|
||||
I/O Scheduler noop cfq deadline
|
||||
:param scheduler:
|
||||
:return:
|
||||
"""
|
||||
self.scheduler = scheduler
|
||||
|
||||
def set_device_name(self, name):
|
||||
"""Device name
|
||||
|
||||
:param name:
|
||||
"""
|
||||
self.name = name
|
||||
|
||||
def get_device_name(self):
|
||||
return self.name
|
||||
@@ -17,6 +17,8 @@
|
||||
import enum
|
||||
|
||||
from watcher.decision_engine.model.element import compute_resource
|
||||
from watcher.objects import base
|
||||
from watcher.objects import fields as wfields
|
||||
|
||||
|
||||
class InstanceState(enum.Enum):
|
||||
@@ -36,19 +38,17 @@ class InstanceState(enum.Enum):
|
||||
ERROR = 'error'
|
||||
|
||||
|
||||
@base.WatcherObjectRegistry.register_if(False)
|
||||
class Instance(compute_resource.ComputeResource):
|
||||
|
||||
def __init__(self):
|
||||
super(Instance, self).__init__()
|
||||
self._state = InstanceState.ACTIVE.value
|
||||
fields = {
|
||||
"state": wfields.StringField(default=InstanceState.ACTIVE.value),
|
||||
|
||||
"memory": wfields.NonNegativeIntegerField(),
|
||||
"disk": wfields.IntegerField(),
|
||||
"disk_capacity": wfields.NonNegativeIntegerField(),
|
||||
"vcpus": wfields.NonNegativeIntegerField(),
|
||||
}
|
||||
|
||||
def accept(self, visitor):
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
return self._state
|
||||
|
||||
@state.setter
|
||||
def state(self, state):
|
||||
self._state = state
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
import enum
|
||||
|
||||
from watcher.decision_engine.model.element import compute_resource
|
||||
from watcher.objects import base
|
||||
from watcher.objects import fields as wfields
|
||||
|
||||
|
||||
class ServiceState(enum.Enum):
|
||||
@@ -26,29 +28,20 @@ class ServiceState(enum.Enum):
|
||||
DISABLED = 'disabled'
|
||||
|
||||
|
||||
@base.WatcherObjectRegistry.register_if(False)
|
||||
class ComputeNode(compute_resource.ComputeResource):
|
||||
|
||||
def __init__(self, id):
|
||||
super(ComputeNode, self).__init__()
|
||||
self.id = id
|
||||
self._state = ServiceState.ONLINE.value
|
||||
self._status = ServiceState.ENABLED.value
|
||||
fields = {
|
||||
"id": wfields.NonNegativeIntegerField(),
|
||||
"hostname": wfields.StringField(),
|
||||
"status": wfields.StringField(default=ServiceState.ENABLED.value),
|
||||
"state": wfields.StringField(default=ServiceState.ONLINE.value),
|
||||
|
||||
"memory": wfields.NonNegativeIntegerField(),
|
||||
"disk": wfields.IntegerField(),
|
||||
"disk_capacity": wfields.NonNegativeIntegerField(),
|
||||
"vcpus": wfields.NonNegativeIntegerField(),
|
||||
}
|
||||
|
||||
def accept(self, visitor):
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
return self._state
|
||||
|
||||
@state.setter
|
||||
def state(self, state):
|
||||
self._state = state
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
return self._status
|
||||
|
||||
@status.setter
|
||||
def status(self, s):
|
||||
self._status = s
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
#
|
||||
# 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
|
||||
|
||||
from watcher.common import exception
|
||||
|
||||
|
||||
class ResourceType(enum.Enum):
|
||||
cpu_cores = 'num_cores'
|
||||
memory = 'memory'
|
||||
disk = 'disk'
|
||||
disk_capacity = 'disk_capacity'
|
||||
|
||||
|
||||
class Resource(object):
|
||||
def __init__(self, name, capacity=None):
|
||||
"""Resource
|
||||
|
||||
:param name: ResourceType
|
||||
:param capacity: max
|
||||
:return:
|
||||
"""
|
||||
self._name = name
|
||||
self.capacity = capacity
|
||||
self.mapping = {}
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
@name.setter
|
||||
def name(self, n):
|
||||
self._name = n
|
||||
|
||||
def set_capacity(self, element, value):
|
||||
self.mapping[element.uuid] = value
|
||||
|
||||
def unset_capacity(self, element):
|
||||
del self.mapping[element.uuid]
|
||||
|
||||
def get_capacity_by_uuid(self, uuid):
|
||||
try:
|
||||
return self.mapping[str(uuid)]
|
||||
except KeyError:
|
||||
raise exception.CapacityNotDefined(
|
||||
capacity=self.name.value, resource=str(uuid))
|
||||
|
||||
def get_capacity(self, element):
|
||||
return self.get_capacity_by_uuid(element.uuid)
|
||||
@@ -1,101 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
#
|
||||
# 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_concurrency import lockutils
|
||||
from oslo_log import log
|
||||
|
||||
from watcher._i18n import _LW
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class Mapping(object):
|
||||
def __init__(self, model):
|
||||
self.model = model
|
||||
self.compute_node_mapping = {}
|
||||
self.instance_mapping = {}
|
||||
|
||||
def map(self, node, instance):
|
||||
"""Select the node where the instance is launched
|
||||
|
||||
:param node: the node
|
||||
:param instance: the virtual machine or instance
|
||||
"""
|
||||
with lockutils.lock(__name__):
|
||||
# init first
|
||||
if node.uuid not in self.compute_node_mapping.keys():
|
||||
self.compute_node_mapping[node.uuid] = set()
|
||||
|
||||
# map node => instances
|
||||
self.compute_node_mapping[node.uuid].add(instance.uuid)
|
||||
|
||||
# map instance => node
|
||||
self.instance_mapping[instance.uuid] = node.uuid
|
||||
|
||||
def unmap(self, node, instance):
|
||||
"""Remove the instance from the node
|
||||
|
||||
:param node: the node
|
||||
:param instance: the virtual machine or instance
|
||||
"""
|
||||
self.unmap_by_uuid(node.uuid, instance.uuid)
|
||||
|
||||
def unmap_by_uuid(self, node_uuid, instance_uuid):
|
||||
"""Remove the instance (by id) from the node (by id)
|
||||
|
||||
:rtype : object
|
||||
"""
|
||||
with lockutils.lock(__name__):
|
||||
if str(node_uuid) in self.compute_node_mapping:
|
||||
self.compute_node_mapping[str(node_uuid)].remove(
|
||||
str(instance_uuid))
|
||||
# remove instance
|
||||
self.instance_mapping.pop(instance_uuid)
|
||||
else:
|
||||
LOG.warning(
|
||||
_LW("Trying to delete the instance %(instance)s but it "
|
||||
"was not found on node %(node)s") %
|
||||
{'instance': instance_uuid, 'node': node_uuid})
|
||||
|
||||
def get_mapping(self):
|
||||
return self.compute_node_mapping
|
||||
|
||||
def get_node_from_instance(self, instance):
|
||||
return self.get_node_by_instance_uuid(instance.uuid)
|
||||
|
||||
def get_node_by_instance_uuid(self, instance_uuid):
|
||||
"""Getting host information from the guest instance
|
||||
|
||||
:param instance: the uuid of the instance
|
||||
:return: node
|
||||
"""
|
||||
return self.model.get_node_by_uuid(
|
||||
self.instance_mapping[str(instance_uuid)])
|
||||
|
||||
def get_node_instances(self, node):
|
||||
"""Get the list of instances running on the node
|
||||
|
||||
:param node:
|
||||
:return:
|
||||
"""
|
||||
return self.get_node_instances_by_uuid(node.uuid)
|
||||
|
||||
def get_node_instances_by_uuid(self, node_uuid):
|
||||
if str(node_uuid) in self.compute_node_mapping.keys():
|
||||
return self.compute_node_mapping[str(node_uuid)]
|
||||
else:
|
||||
# empty
|
||||
return set()
|
||||
@@ -1,39 +1,40 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
# Copyright (c) 2016 Intel Innovation and Research Ireland Ltd.
|
||||
#
|
||||
# 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
|
||||
# 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.
|
||||
# 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 collections
|
||||
"""
|
||||
Openstack implementation of the cluster graph.
|
||||
"""
|
||||
|
||||
from lxml import etree
|
||||
import networkx as nx
|
||||
from oslo_concurrency import lockutils
|
||||
from oslo_log import log
|
||||
import six
|
||||
|
||||
from watcher._i18n import _
|
||||
from watcher.common import exception
|
||||
from watcher.common import utils
|
||||
from watcher.decision_engine.model import base
|
||||
from watcher.decision_engine.model import element
|
||||
from watcher.decision_engine.model import mapping
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class ModelRoot(base.Model):
|
||||
class ModelRoot(nx.DiGraph, base.Model):
|
||||
"""Cluster graph for an Openstack cluster."""
|
||||
|
||||
def __init__(self, stale=False):
|
||||
self._nodes = utils.Struct()
|
||||
self._instances = utils.Struct()
|
||||
self.mapping = mapping.Mapping(self)
|
||||
self.resource = utils.Struct()
|
||||
super(ModelRoot, self).__init__()
|
||||
self.stale = stale
|
||||
|
||||
def __nonzero__(self):
|
||||
@@ -41,35 +42,47 @@ class ModelRoot(base.Model):
|
||||
|
||||
__bool__ = __nonzero__
|
||||
|
||||
def assert_node(self, obj):
|
||||
@staticmethod
|
||||
def assert_node(obj):
|
||||
if not isinstance(obj, element.ComputeNode):
|
||||
raise exception.IllegalArgumentException(
|
||||
message=_("'obj' argument type is not valid"))
|
||||
message=_("'obj' argument type is not valid: %s") % type(obj))
|
||||
|
||||
def assert_instance(self, obj):
|
||||
@staticmethod
|
||||
def assert_instance(obj):
|
||||
if not isinstance(obj, element.Instance):
|
||||
raise exception.IllegalArgumentException(
|
||||
message=_("'obj' argument type is not valid"))
|
||||
|
||||
@lockutils.synchronized("model_root")
|
||||
def add_node(self, node):
|
||||
self.assert_node(node)
|
||||
self._nodes[node.uuid] = node
|
||||
super(ModelRoot, self).add_node(node)
|
||||
|
||||
@lockutils.synchronized("model_root")
|
||||
def remove_node(self, node):
|
||||
self.assert_node(node)
|
||||
if str(node.uuid) not in self._nodes:
|
||||
try:
|
||||
super(ModelRoot, self).remove_node(node)
|
||||
except nx.NetworkXError as exc:
|
||||
LOG.exception(exc)
|
||||
raise exception.ComputeNodeNotFound(name=node.uuid)
|
||||
else:
|
||||
del self._nodes[node.uuid]
|
||||
|
||||
@lockutils.synchronized("model_root")
|
||||
def add_instance(self, instance):
|
||||
self.assert_instance(instance)
|
||||
self._instances[instance.uuid] = instance
|
||||
try:
|
||||
super(ModelRoot, self).add_node(instance)
|
||||
except nx.NetworkXError as exc:
|
||||
LOG.exception(exc)
|
||||
raise exception.InstanceNotFound(name=instance.uuid)
|
||||
|
||||
@lockutils.synchronized("model_root")
|
||||
def remove_instance(self, instance):
|
||||
self.assert_instance(instance)
|
||||
del self._instances[instance.uuid]
|
||||
super(ModelRoot, self).remove_node(instance)
|
||||
|
||||
@lockutils.synchronized("model_root")
|
||||
def map_instance(self, instance, node):
|
||||
"""Map a newly created instance to a node
|
||||
|
||||
@@ -82,38 +95,25 @@ class ModelRoot(base.Model):
|
||||
instance = self.get_instance_by_uuid(instance)
|
||||
if isinstance(node, six.string_types):
|
||||
node = self.get_node_by_uuid(node)
|
||||
self.assert_node(node)
|
||||
self.assert_instance(instance)
|
||||
|
||||
self.add_instance(instance)
|
||||
self.mapping.map(node, instance)
|
||||
self.add_edge(instance, node)
|
||||
|
||||
@lockutils.synchronized("model_root")
|
||||
def unmap_instance(self, instance, node):
|
||||
"""Unmap an instance from a node
|
||||
|
||||
:param instance: :py:class:`~.Instance` object or instance UUID
|
||||
:type instance: str or :py:class:`~.Instance`
|
||||
:param node: :py:class:`~.ComputeNode` object or node UUID
|
||||
:type node: str or :py:class:`~.Instance`
|
||||
"""
|
||||
if isinstance(instance, six.string_types):
|
||||
instance = self.get_instance_by_uuid(instance)
|
||||
if isinstance(node, six.string_types):
|
||||
node = self.get_node_by_uuid(node)
|
||||
|
||||
self.add_instance(instance)
|
||||
self.mapping.unmap(node, instance)
|
||||
self.remove_edge(instance, node)
|
||||
|
||||
def delete_instance(self, instance, node=None):
|
||||
if node is not None:
|
||||
self.mapping.unmap(node, instance)
|
||||
|
||||
self.assert_instance(instance)
|
||||
self.remove_instance(instance)
|
||||
|
||||
for resource in self.resource.values():
|
||||
try:
|
||||
resource.unset_capacity(instance)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@lockutils.synchronized("model_root")
|
||||
def migrate_instance(self, instance, source_node, destination_node):
|
||||
"""Migrate single instance from source_node to destination_node
|
||||
|
||||
@@ -122,96 +122,80 @@ class ModelRoot(base.Model):
|
||||
:param destination_node:
|
||||
:return:
|
||||
"""
|
||||
self.assert_instance(instance)
|
||||
self.assert_node(source_node)
|
||||
self.assert_node(destination_node)
|
||||
|
||||
if source_node == destination_node:
|
||||
return False
|
||||
|
||||
# unmap
|
||||
self.mapping.unmap(source_node, instance)
|
||||
self.remove_edge(instance, source_node)
|
||||
# map
|
||||
self.mapping.map(destination_node, instance)
|
||||
self.add_edge(instance, destination_node)
|
||||
return True
|
||||
|
||||
@lockutils.synchronized("model_root")
|
||||
def get_all_compute_nodes(self):
|
||||
return self._nodes
|
||||
return {cn.uuid: cn for cn in self.nodes()
|
||||
if isinstance(cn, element.ComputeNode)}
|
||||
|
||||
def get_node_by_uuid(self, node_uuid):
|
||||
if str(node_uuid) not in self._nodes:
|
||||
raise exception.ComputeNodeNotFound(name=node_uuid)
|
||||
return self._nodes[str(node_uuid)]
|
||||
@lockutils.synchronized("model_root")
|
||||
def get_node_by_uuid(self, uuid):
|
||||
for graph_node in self.nodes():
|
||||
if (isinstance(graph_node, element.ComputeNode) and
|
||||
graph_node.uuid == uuid):
|
||||
return graph_node
|
||||
raise exception.ComputeNodeNotFound(name=uuid)
|
||||
|
||||
@lockutils.synchronized("model_root")
|
||||
def get_instance_by_uuid(self, uuid):
|
||||
if str(uuid) not in self._instances:
|
||||
raise exception.InstanceNotFound(name=uuid)
|
||||
return self._instances[str(uuid)]
|
||||
return self._get_instance_by_uuid(uuid)
|
||||
|
||||
def _get_instance_by_uuid(self, uuid):
|
||||
for graph_node in self.nodes():
|
||||
if (isinstance(graph_node, element.Instance) and
|
||||
graph_node.uuid == str(uuid)):
|
||||
return graph_node
|
||||
raise exception.InstanceNotFound(name=uuid)
|
||||
|
||||
@lockutils.synchronized("model_root")
|
||||
def get_node_by_instance_uuid(self, instance_uuid):
|
||||
"""Getting host information from the guest instance
|
||||
|
||||
:param instance_uuid: the uuid of the instance
|
||||
:return: node
|
||||
"""
|
||||
if str(instance_uuid) not in self.mapping.instance_mapping:
|
||||
raise exception.InstanceNotFound(name=instance_uuid)
|
||||
return self.get_node_by_uuid(
|
||||
self.mapping.instance_mapping[str(instance_uuid)])
|
||||
instance = self._get_instance_by_uuid(instance_uuid)
|
||||
for node in self.neighbors(instance):
|
||||
if isinstance(node, element.ComputeNode):
|
||||
return node
|
||||
raise exception.ComputeNodeNotFound(name=instance_uuid)
|
||||
|
||||
@lockutils.synchronized("model_root")
|
||||
def get_all_instances(self):
|
||||
return self._instances
|
||||
|
||||
def get_mapping(self):
|
||||
return self.mapping
|
||||
|
||||
def create_resource(self, r):
|
||||
self.resource[str(r.name)] = r
|
||||
|
||||
def get_resource_by_uuid(self, resource_id):
|
||||
return self.resource[str(resource_id)]
|
||||
return {inst.uuid: inst for inst in self.nodes()
|
||||
if isinstance(inst, element.Instance)}
|
||||
|
||||
@lockutils.synchronized("model_root")
|
||||
def get_node_instances(self, node):
|
||||
return self.mapping.get_node_instances(node)
|
||||
self.assert_node(node)
|
||||
node_instances = []
|
||||
for neighbor in self.predecessors(node):
|
||||
if isinstance(neighbor, element.Instance):
|
||||
node_instances.append(neighbor)
|
||||
|
||||
def _build_compute_node_element(self, compute_node):
|
||||
attrib = collections.OrderedDict(
|
||||
id=six.text_type(compute_node.id), uuid=compute_node.uuid,
|
||||
human_id=compute_node.human_id, hostname=compute_node.hostname,
|
||||
state=compute_node.state, status=compute_node.status)
|
||||
|
||||
for resource_name, resource in sorted(
|
||||
self.resource.items(), key=lambda x: x[0]):
|
||||
res_value = resource.get_capacity(compute_node)
|
||||
if res_value is not None:
|
||||
attrib[resource_name] = six.text_type(res_value)
|
||||
|
||||
compute_node_el = etree.Element("ComputeNode", attrib=attrib)
|
||||
|
||||
return compute_node_el
|
||||
|
||||
def _build_instance_element(self, instance):
|
||||
attrib = collections.OrderedDict(
|
||||
uuid=instance.uuid, human_id=instance.human_id,
|
||||
hostname=instance.hostname, state=instance.state)
|
||||
|
||||
for resource_name, resource in sorted(
|
||||
self.resource.items(), key=lambda x: x[0]):
|
||||
res_value = resource.get_capacity(instance)
|
||||
if res_value is not None:
|
||||
attrib[resource_name] = six.text_type(res_value)
|
||||
|
||||
instance_el = etree.Element("Instance", attrib=attrib)
|
||||
|
||||
return instance_el
|
||||
return node_instances
|
||||
|
||||
def to_string(self):
|
||||
return self.to_xml()
|
||||
|
||||
def to_xml(self):
|
||||
root = etree.Element("ModelRoot")
|
||||
# Build compute node tree
|
||||
for cn in sorted(self.get_all_compute_nodes().values(),
|
||||
key=lambda cn: cn.uuid):
|
||||
compute_node_el = self._build_compute_node_element(cn)
|
||||
compute_node_el = cn.as_xml_element()
|
||||
|
||||
# Build mapped instance tree
|
||||
node_instance_uuids = self.get_node_instances(cn)
|
||||
for instance_uuid in sorted(node_instance_uuids):
|
||||
instance = self.get_instance_by_uuid(instance_uuid)
|
||||
instance_el = self._build_instance_element(instance)
|
||||
node_instances = self.get_node_instances(cn)
|
||||
for instance in sorted(node_instances, key=lambda x: x.uuid):
|
||||
instance_el = instance.as_xml_element()
|
||||
compute_node_el.append(instance_el)
|
||||
|
||||
root.append(compute_node_el)
|
||||
@@ -221,51 +205,23 @@ class ModelRoot(base.Model):
|
||||
key=lambda inst: inst.uuid):
|
||||
try:
|
||||
self.get_node_by_instance_uuid(instance.uuid)
|
||||
except exception.InstanceNotFound:
|
||||
root.append(self._build_instance_element(instance))
|
||||
except (exception.InstanceNotFound, exception.ComputeNodeNotFound):
|
||||
root.append(instance.as_xml_element())
|
||||
|
||||
return etree.tostring(root, pretty_print=True).decode('utf-8')
|
||||
|
||||
@classmethod
|
||||
def from_xml(cls, data):
|
||||
model = cls()
|
||||
|
||||
root = etree.fromstring(data)
|
||||
|
||||
mem = element.Resource(element.ResourceType.memory)
|
||||
num_cores = element.Resource(element.ResourceType.cpu_cores)
|
||||
disk = element.Resource(element.ResourceType.disk)
|
||||
disk_capacity = element.Resource(element.ResourceType.disk_capacity)
|
||||
model.create_resource(mem)
|
||||
model.create_resource(num_cores)
|
||||
model.create_resource(disk)
|
||||
model.create_resource(disk_capacity)
|
||||
|
||||
for cn in root.findall('.//ComputeNode'):
|
||||
node = element.ComputeNode(cn.get('id'))
|
||||
node.uuid = cn.get('uuid')
|
||||
node.hostname = cn.get('hostname')
|
||||
# set capacity
|
||||
mem.set_capacity(node, int(cn.get(str(mem.name))))
|
||||
disk.set_capacity(node, int(cn.get(str(disk.name))))
|
||||
disk_capacity.set_capacity(
|
||||
node, int(cn.get(str(disk_capacity.name))))
|
||||
num_cores.set_capacity(node, int(cn.get(str(num_cores.name))))
|
||||
node.state = cn.get('state')
|
||||
node.status = cn.get('status')
|
||||
|
||||
node = element.ComputeNode(**cn.attrib)
|
||||
model.add_node(node)
|
||||
|
||||
for inst in root.findall('.//Instance'):
|
||||
instance = element.Instance()
|
||||
instance.uuid = inst.get('uuid')
|
||||
instance.state = inst.get('state')
|
||||
|
||||
mem.set_capacity(instance, int(inst.get(str(mem.name))))
|
||||
disk.set_capacity(instance, int(inst.get(str(disk.name))))
|
||||
disk_capacity.set_capacity(
|
||||
instance, int(inst.get(str(disk_capacity.name))))
|
||||
num_cores.set_capacity(
|
||||
instance, int(inst.get(str(num_cores.name))))
|
||||
instance = element.Instance(**inst.attrib)
|
||||
model.add_instance(instance)
|
||||
|
||||
parent = inst.getparent()
|
||||
if parent.tag == 'ComputeNode':
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
from oslo_log import log
|
||||
|
||||
from watcher._i18n import _LI
|
||||
from watcher._i18n import _LI, _LW
|
||||
from watcher.common import exception
|
||||
from watcher.common import nova_helper
|
||||
from watcher.decision_engine.model import element
|
||||
@@ -40,14 +40,21 @@ class NovaNotification(base.NotificationEndpoint):
|
||||
self._nova = nova_helper.NovaHelper()
|
||||
return self._nova
|
||||
|
||||
def get_or_create_instance(self, uuid):
|
||||
def get_or_create_instance(self, instance_uuid, node_uuid=None):
|
||||
try:
|
||||
instance = self.cluster_data_model.get_instance_by_uuid(uuid)
|
||||
if node_uuid:
|
||||
self.get_or_create_node(node_uuid)
|
||||
except exception.ComputeNodeNotFound:
|
||||
LOG.warning(_LW("Could not find compute node %(node)s for "
|
||||
"instance %(instance)s"),
|
||||
dict(node=node_uuid, instance=instance_uuid))
|
||||
try:
|
||||
instance = self.cluster_data_model.get_instance_by_uuid(
|
||||
instance_uuid)
|
||||
except exception.InstanceNotFound:
|
||||
# The instance didn't exist yet so we create a new instance object
|
||||
LOG.debug("New instance created: %s", uuid)
|
||||
instance = element.Instance()
|
||||
instance.uuid = uuid
|
||||
LOG.debug("New instance created: %s", instance_uuid)
|
||||
instance = element.Instance(uuid=instance_uuid)
|
||||
|
||||
self.cluster_data_model.add_instance(instance)
|
||||
|
||||
@@ -57,21 +64,19 @@ class NovaNotification(base.NotificationEndpoint):
|
||||
instance_data = data['nova_object.data']
|
||||
instance_flavor_data = instance_data['flavor']['nova_object.data']
|
||||
|
||||
instance.state = instance_data['state']
|
||||
instance.hostname = instance_data['host_name']
|
||||
instance.human_id = instance_data['display_name']
|
||||
|
||||
memory_mb = instance_flavor_data['memory_mb']
|
||||
num_cores = instance_flavor_data['vcpus']
|
||||
disk_gb = instance_flavor_data['root_gb']
|
||||
|
||||
self.update_capacity(element.ResourceType.memory, instance, memory_mb)
|
||||
self.update_capacity(
|
||||
element.ResourceType.cpu_cores, instance, num_cores)
|
||||
self.update_capacity(
|
||||
element.ResourceType.disk, instance, disk_gb)
|
||||
self.update_capacity(
|
||||
element.ResourceType.disk_capacity, instance, disk_gb)
|
||||
instance.update({
|
||||
'state': instance_data['state'],
|
||||
'hostname': instance_data['host_name'],
|
||||
'human_id': instance_data['display_name'],
|
||||
'memory': memory_mb,
|
||||
'vcpus': num_cores,
|
||||
'disk': disk_gb,
|
||||
'disk_capacity': disk_gb,
|
||||
})
|
||||
|
||||
try:
|
||||
node = self.get_or_create_node(instance_data['host'])
|
||||
@@ -82,26 +87,20 @@ class NovaNotification(base.NotificationEndpoint):
|
||||
|
||||
self.update_instance_mapping(instance, node)
|
||||
|
||||
def update_capacity(self, resource_id, obj, value):
|
||||
resource = self.cluster_data_model.get_resource_by_uuid(resource_id)
|
||||
resource.set_capacity(obj, value)
|
||||
|
||||
def legacy_update_instance(self, instance, data):
|
||||
instance.state = data['state']
|
||||
instance.hostname = data['hostname']
|
||||
instance.human_id = data['display_name']
|
||||
|
||||
memory_mb = data['memory_mb']
|
||||
num_cores = data['vcpus']
|
||||
disk_gb = data['root_gb']
|
||||
|
||||
self.update_capacity(element.ResourceType.memory, instance, memory_mb)
|
||||
self.update_capacity(
|
||||
element.ResourceType.cpu_cores, instance, num_cores)
|
||||
self.update_capacity(
|
||||
element.ResourceType.disk, instance, disk_gb)
|
||||
self.update_capacity(
|
||||
element.ResourceType.disk_capacity, instance, disk_gb)
|
||||
instance.update({
|
||||
'state': data['state'],
|
||||
'hostname': data['hostname'],
|
||||
'human_id': data['display_name'],
|
||||
'memory': memory_mb,
|
||||
'vcpus': num_cores,
|
||||
'disk': disk_gb,
|
||||
'disk_capacity': disk_gb,
|
||||
})
|
||||
|
||||
try:
|
||||
node = self.get_or_create_node(data['host'])
|
||||
@@ -115,32 +114,34 @@ class NovaNotification(base.NotificationEndpoint):
|
||||
def update_compute_node(self, node, data):
|
||||
"""Update the compute node using the notification data."""
|
||||
node_data = data['nova_object.data']
|
||||
node.hostname = node_data['host']
|
||||
node.state = (
|
||||
node_state = (
|
||||
element.ServiceState.OFFLINE.value
|
||||
if node_data['forced_down'] else element.ServiceState.ONLINE.value)
|
||||
node.status = (
|
||||
node_status = (
|
||||
element.ServiceState.DISABLED.value
|
||||
if node_data['host'] else element.ServiceState.ENABLED.value)
|
||||
if node_data['disabled'] else element.ServiceState.ENABLED.value)
|
||||
|
||||
node.update({
|
||||
'hostname': node_data['host'],
|
||||
'state': node_state,
|
||||
'status': node_status,
|
||||
})
|
||||
|
||||
def create_compute_node(self, node_hostname):
|
||||
"""Update the compute node by querying the Nova API."""
|
||||
try:
|
||||
_node = self.nova.get_compute_node_by_hostname(node_hostname)
|
||||
node = element.ComputeNode(_node.id)
|
||||
node.uuid = node_hostname
|
||||
node.hostname = _node.hypervisor_hostname
|
||||
node.state = _node.state
|
||||
node.status = _node.status
|
||||
|
||||
self.update_capacity(
|
||||
element.ResourceType.memory, node, _node.memory_mb)
|
||||
self.update_capacity(
|
||||
element.ResourceType.cpu_cores, node, _node.vcpus)
|
||||
self.update_capacity(
|
||||
element.ResourceType.disk, node, _node.free_disk_gb)
|
||||
self.update_capacity(
|
||||
element.ResourceType.disk_capacity, node, _node.local_gb)
|
||||
node = element.ComputeNode(
|
||||
id=_node.id,
|
||||
uuid=node_hostname,
|
||||
hostname=_node.hypervisor_hostname,
|
||||
state=_node.state,
|
||||
status=_node.status,
|
||||
memory=_node.memory_mb,
|
||||
vcpus=_node.vcpus,
|
||||
disk=_node.free_disk_gb,
|
||||
disk_capacity=_node.local_gb,
|
||||
)
|
||||
return node
|
||||
except Exception as exc:
|
||||
LOG.exception(exc)
|
||||
@@ -160,6 +161,7 @@ class NovaNotification(base.NotificationEndpoint):
|
||||
node = self.create_compute_node(uuid)
|
||||
LOG.debug("New compute node created: %s", uuid)
|
||||
self.cluster_data_model.add_node(node)
|
||||
LOG.debug("New compute node mapped: %s", uuid)
|
||||
return node
|
||||
|
||||
def update_instance_mapping(self, instance, node):
|
||||
@@ -170,18 +172,20 @@ class NovaNotification(base.NotificationEndpoint):
|
||||
return
|
||||
try:
|
||||
try:
|
||||
old_node = self.get_or_create_node(node.uuid)
|
||||
current_node = (
|
||||
self.cluster_data_model.get_node_by_instance_uuid(
|
||||
instance.uuid) or self.get_or_create_node(node.uuid))
|
||||
except exception.ComputeNodeNotFound as exc:
|
||||
LOG.exception(exc)
|
||||
# If we can't create the node,
|
||||
# we consider the instance as unmapped
|
||||
old_node = None
|
||||
current_node = None
|
||||
|
||||
LOG.debug("Mapped node %s found", node.uuid)
|
||||
if node and node != old_node:
|
||||
if current_node and node != current_node:
|
||||
LOG.debug("Unmapping instance %s from %s",
|
||||
instance.uuid, node.uuid)
|
||||
self.cluster_data_model.unmap_instance(instance, old_node)
|
||||
self.cluster_data_model.unmap_instance(instance, current_node)
|
||||
except exception.InstanceNotFound:
|
||||
# The instance didn't exist yet so we map it for the first time
|
||||
LOG.debug("New instance: mapping it to %s", node.uuid)
|
||||
@@ -221,6 +225,7 @@ class ServiceUpdated(VersionnedNotificationEndpoint):
|
||||
dict(event=event_type,
|
||||
publisher=publisher_id,
|
||||
metadata=metadata))
|
||||
LOG.debug(payload)
|
||||
node_data = payload['nova_object.data']
|
||||
node_uuid = node_data['host']
|
||||
try:
|
||||
@@ -262,10 +267,12 @@ class InstanceCreated(VersionnedNotificationEndpoint):
|
||||
dict(event=event_type,
|
||||
publisher=publisher_id,
|
||||
metadata=metadata))
|
||||
LOG.debug(payload)
|
||||
instance_data = payload['nova_object.data']
|
||||
|
||||
instance_uuid = instance_data['uuid']
|
||||
instance = self.get_or_create_instance(instance_uuid)
|
||||
node_uuid = instance_data.get('host')
|
||||
instance = self.get_or_create_instance(instance_uuid, node_uuid)
|
||||
|
||||
self.update_instance(instance, payload)
|
||||
|
||||
@@ -294,9 +301,11 @@ class InstanceUpdated(VersionnedNotificationEndpoint):
|
||||
dict(event=event_type,
|
||||
publisher=publisher_id,
|
||||
metadata=metadata))
|
||||
LOG.debug(payload)
|
||||
instance_data = payload['nova_object.data']
|
||||
instance_uuid = instance_data['uuid']
|
||||
instance = self.get_or_create_instance(instance_uuid)
|
||||
node_uuid = instance_data.get('host')
|
||||
instance = self.get_or_create_instance(instance_uuid, node_uuid)
|
||||
|
||||
self.update_instance(instance, payload)
|
||||
|
||||
@@ -317,10 +326,12 @@ class InstanceDeletedEnd(VersionnedNotificationEndpoint):
|
||||
dict(event=event_type,
|
||||
publisher=publisher_id,
|
||||
metadata=metadata))
|
||||
LOG.debug(payload)
|
||||
|
||||
instance_data = payload['nova_object.data']
|
||||
instance_uuid = instance_data['uuid']
|
||||
instance = self.get_or_create_instance(instance_uuid)
|
||||
node_uuid = instance_data.get('host')
|
||||
instance = self.get_or_create_instance(instance_uuid, node_uuid)
|
||||
|
||||
try:
|
||||
node = self.get_or_create_node(instance_data['host'])
|
||||
@@ -348,9 +359,11 @@ class LegacyInstanceUpdated(UnversionnedNotificationEndpoint):
|
||||
dict(event=event_type,
|
||||
publisher=publisher_id,
|
||||
metadata=metadata))
|
||||
LOG.debug(payload)
|
||||
|
||||
instance_uuid = payload['instance_id']
|
||||
instance = self.get_or_create_instance(instance_uuid)
|
||||
node_uuid = payload.get('node')
|
||||
instance = self.get_or_create_instance(instance_uuid, node_uuid)
|
||||
|
||||
self.legacy_update_instance(instance, payload)
|
||||
|
||||
@@ -371,9 +384,11 @@ class LegacyInstanceCreatedEnd(UnversionnedNotificationEndpoint):
|
||||
dict(event=event_type,
|
||||
publisher=publisher_id,
|
||||
metadata=metadata))
|
||||
LOG.debug(payload)
|
||||
|
||||
instance_uuid = payload['instance_id']
|
||||
instance = self.get_or_create_instance(instance_uuid)
|
||||
node_uuid = payload.get('node')
|
||||
instance = self.get_or_create_instance(instance_uuid, node_uuid)
|
||||
|
||||
self.legacy_update_instance(instance, payload)
|
||||
|
||||
@@ -394,8 +409,10 @@ class LegacyInstanceDeletedEnd(UnversionnedNotificationEndpoint):
|
||||
dict(event=event_type,
|
||||
publisher=publisher_id,
|
||||
metadata=metadata))
|
||||
LOG.debug(payload)
|
||||
instance_uuid = payload['instance_id']
|
||||
instance = self.get_or_create_instance(instance_uuid)
|
||||
node_uuid = payload.get('node')
|
||||
instance = self.get_or_create_instance(instance_uuid, node_uuid)
|
||||
|
||||
try:
|
||||
node = self.get_or_create_node(payload['host'])
|
||||
@@ -423,8 +440,10 @@ class LegacyLiveMigratedEnd(UnversionnedNotificationEndpoint):
|
||||
dict(event=event_type,
|
||||
publisher=publisher_id,
|
||||
metadata=metadata))
|
||||
LOG.debug(payload)
|
||||
|
||||
instance_uuid = payload['instance_id']
|
||||
instance = self.get_or_create_instance(instance_uuid)
|
||||
node_uuid = payload.get('node')
|
||||
instance = self.get_or_create_instance(instance_uuid, node_uuid)
|
||||
|
||||
self.legacy_update_instance(instance, payload)
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
#
|
||||
# Authors: Jean-Emile DARTOIS <jean-emile.dartois@b-com.com>
|
||||
#
|
||||
# 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._i18n import _LW
|
||||
from watcher.common import utils
|
||||
from watcher.decision_engine.planner import base
|
||||
from watcher import objects
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class DefaultPlanner(base.BasePlanner):
|
||||
"""Default planner implementation
|
||||
|
||||
This implementation comes with basic rules with a set of action types that
|
||||
are weighted. An action having a lower weight will be scheduled before the
|
||||
other ones. The set of action types can be specified by 'weights' in the
|
||||
``watcher.conf``. You need to associate a different weight to all available
|
||||
actions into the configuration file, otherwise you will get an error when
|
||||
the new action will be referenced in the solution produced by a strategy.
|
||||
"""
|
||||
|
||||
weights_dict = {
|
||||
'nop': 0,
|
||||
'sleep': 1,
|
||||
'change_nova_service_state': 2,
|
||||
'migrate': 3,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_config_opts(cls):
|
||||
return [
|
||||
cfg.DictOpt(
|
||||
'weights',
|
||||
help="These weights are used to schedule the actions",
|
||||
default=cls.weights_dict),
|
||||
]
|
||||
|
||||
def create_action(self,
|
||||
action_plan_id,
|
||||
action_type,
|
||||
input_parameters=None):
|
||||
uuid = utils.generate_uuid()
|
||||
action = {
|
||||
'uuid': uuid,
|
||||
'action_plan_id': int(action_plan_id),
|
||||
'action_type': action_type,
|
||||
'input_parameters': input_parameters,
|
||||
'state': objects.action.State.PENDING,
|
||||
'next': None,
|
||||
}
|
||||
|
||||
return action
|
||||
|
||||
def schedule(self, context, audit_id, solution):
|
||||
LOG.debug('Creating an action plan for the audit uuid: %s', audit_id)
|
||||
priorities = self.config.weights
|
||||
action_plan = self._create_action_plan(context, audit_id, solution)
|
||||
|
||||
actions = list(solution.actions)
|
||||
to_schedule = []
|
||||
for action in actions:
|
||||
json_action = self.create_action(
|
||||
action_plan_id=action_plan.id,
|
||||
action_type=action.get('action_type'),
|
||||
input_parameters=action.get('input_parameters'))
|
||||
to_schedule.append((priorities[action.get('action_type')],
|
||||
json_action))
|
||||
|
||||
self._create_efficacy_indicators(
|
||||
context, action_plan.id, solution.efficacy_indicators)
|
||||
|
||||
# scheduling
|
||||
scheduled = sorted(to_schedule, key=lambda x: (x[0]))
|
||||
if len(scheduled) == 0:
|
||||
LOG.warning(_LW("The action plan is empty"))
|
||||
action_plan.first_action_id = None
|
||||
action_plan.state = objects.action_plan.State.SUCCEEDED
|
||||
action_plan.save()
|
||||
else:
|
||||
# create the first action
|
||||
parent_action = self._create_action(context,
|
||||
scheduled[0][1],
|
||||
None)
|
||||
# remove first
|
||||
scheduled.pop(0)
|
||||
|
||||
action_plan.first_action_id = parent_action.id
|
||||
action_plan.save()
|
||||
|
||||
for s_action in scheduled:
|
||||
current_action = self._create_action(context, s_action[1],
|
||||
parent_action)
|
||||
parent_action = current_action
|
||||
|
||||
return action_plan
|
||||
|
||||
def _create_action_plan(self, context, audit_id, solution):
|
||||
strategy = objects.Strategy.get_by_name(
|
||||
context, solution.strategy.name)
|
||||
|
||||
action_plan_dict = {
|
||||
'uuid': utils.generate_uuid(),
|
||||
'audit_id': audit_id,
|
||||
'strategy_id': strategy.id,
|
||||
'first_action_id': None,
|
||||
'state': objects.action_plan.State.RECOMMENDED,
|
||||
'global_efficacy': solution.global_efficacy,
|
||||
}
|
||||
|
||||
new_action_plan = objects.ActionPlan(context, **action_plan_dict)
|
||||
new_action_plan.create()
|
||||
|
||||
return new_action_plan
|
||||
|
||||
def _create_efficacy_indicators(self, context, action_plan_id, indicators):
|
||||
efficacy_indicators = []
|
||||
for indicator in indicators:
|
||||
efficacy_indicator_dict = {
|
||||
'uuid': utils.generate_uuid(),
|
||||
'name': indicator.name,
|
||||
'description': indicator.description,
|
||||
'unit': indicator.unit,
|
||||
'value': indicator.value,
|
||||
'action_plan_id': action_plan_id,
|
||||
}
|
||||
new_efficacy_indicator = objects.EfficacyIndicator(
|
||||
context, **efficacy_indicator_dict)
|
||||
new_efficacy_indicator.create()
|
||||
|
||||
efficacy_indicators.append(new_efficacy_indicator)
|
||||
return efficacy_indicators
|
||||
|
||||
def _create_action(self, context, _action, parent_action):
|
||||
try:
|
||||
LOG.debug("Creating the %s in the Watcher database",
|
||||
_action.get("action_type"))
|
||||
|
||||
new_action = objects.Action(context, **_action)
|
||||
new_action.create()
|
||||
new_action.save()
|
||||
|
||||
if parent_action:
|
||||
parent_action.next = new_action.id
|
||||
parent_action.save()
|
||||
|
||||
return new_action
|
||||
except Exception as exc:
|
||||
LOG.exception(exc)
|
||||
raise
|
||||
233
watcher/decision_engine/planner/weight.py
Normal file
233
watcher/decision_engine/planner/weight.py
Normal file
@@ -0,0 +1,233 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Authors: Vincent Francoise <Vincent.FRANCOISE@b-com.com>
|
||||
# Alexander Chadin <a.chadin@servionica.ru>
|
||||
# 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 collections
|
||||
|
||||
import networkx as nx
|
||||
from oslo_config import cfg
|
||||
from oslo_config import types
|
||||
from oslo_log import log
|
||||
|
||||
from watcher._i18n import _LW
|
||||
from watcher.common import utils
|
||||
from watcher.decision_engine.planner import base
|
||||
from watcher import objects
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class WeightPlanner(base.BasePlanner):
|
||||
"""Weight planner implementation
|
||||
|
||||
This implementation builds actions with parents in accordance with weights.
|
||||
Set of actions having a lower weight will be scheduled before
|
||||
the other ones. There are two config options to configure:
|
||||
action_weights and parallelization.
|
||||
|
||||
*Limitations*
|
||||
|
||||
- This planner requires to have action_weights and parallelization configs
|
||||
tuned well.
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
super(WeightPlanner, self).__init__(config)
|
||||
|
||||
action_weights = {
|
||||
'turn_host_to_acpi_s3_state': 10,
|
||||
'resize': 20,
|
||||
'migrate': 30,
|
||||
'sleep': 40,
|
||||
'change_nova_service_state': 50,
|
||||
'nop': 60,
|
||||
}
|
||||
|
||||
parallelization = {
|
||||
'turn_host_to_acpi_s3_state': 2,
|
||||
'resize': 2,
|
||||
'migrate': 2,
|
||||
'sleep': 1,
|
||||
'change_nova_service_state': 1,
|
||||
'nop': 1,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_config_opts(cls):
|
||||
return [
|
||||
cfg.Opt(
|
||||
'weights',
|
||||
type=types.Dict(value_type=types.Integer()),
|
||||
help="These weights are used to schedule the actions. "
|
||||
"Action Plan will be build in accordance with sets of "
|
||||
"actions ordered by descending weights."
|
||||
"Two action types cannot have the same weight. ",
|
||||
default=cls.action_weights),
|
||||
cfg.Opt(
|
||||
'parallelization',
|
||||
type=types.Dict(value_type=types.Integer()),
|
||||
help="Number of actions to be run in parallel on a per "
|
||||
"action type basis.",
|
||||
default=cls.parallelization),
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def format_action(action_plan_id, action_type,
|
||||
input_parameters=None, parents=()):
|
||||
return {
|
||||
'uuid': utils.generate_uuid(),
|
||||
'action_plan_id': int(action_plan_id),
|
||||
'action_type': action_type,
|
||||
'input_parameters': input_parameters,
|
||||
'state': objects.action.State.PENDING,
|
||||
'parents': parents or None,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def chunkify(lst, n):
|
||||
"""Yield successive n-sized chunks from lst."""
|
||||
if n < 1:
|
||||
# Just to make sure the number is valid
|
||||
n = 1
|
||||
|
||||
# Split a flat list in a list of chunks of size n.
|
||||
# e.g. chunkify([0, 1, 2, 3, 4], 2) -> [[0, 1], [2, 3], [4]]
|
||||
for i in range(0, len(lst), n):
|
||||
yield lst[i:i + n]
|
||||
|
||||
def compute_action_graph(self, sorted_weighted_actions):
|
||||
reverse_weights = {v: k for k, v in self.config.weights.items()}
|
||||
# leaf_groups contains a list of list of nodes called groups
|
||||
# each group is a set of nodes from which a future node will
|
||||
# branch off (parent nodes).
|
||||
|
||||
# START --> migrate-1 --> migrate-3
|
||||
# \ \--> resize-1 --> FINISH
|
||||
# \--> migrate-2 -------------/
|
||||
# In the above case migrate-1 will the only memeber of the leaf
|
||||
# group that migrate-3 will use as parent group, whereas
|
||||
# resize-1 will have both migrate-2 and migrate-3 in its
|
||||
# parent/leaf group
|
||||
leaf_groups = []
|
||||
action_graph = nx.DiGraph()
|
||||
# We iterate through each action type category (sorted by weight) to
|
||||
# insert them in a Directed Acyclic Graph
|
||||
for idx, (weight, actions) in enumerate(sorted_weighted_actions):
|
||||
action_chunks = self.chunkify(
|
||||
actions, self.config.parallelization[reverse_weights[weight]])
|
||||
|
||||
# We split the actions into chunks/layers that will have to be
|
||||
# spread across all the available branches of the graph
|
||||
for chunk_idx, actions_chunk in enumerate(action_chunks):
|
||||
for action in actions_chunk:
|
||||
action_graph.add_node(action)
|
||||
|
||||
# all other actions
|
||||
parent_nodes = []
|
||||
if not idx and not chunk_idx:
|
||||
parent_nodes = []
|
||||
elif leaf_groups:
|
||||
parent_nodes = leaf_groups
|
||||
|
||||
for parent_node in parent_nodes:
|
||||
action_graph.add_edge(parent_node, action)
|
||||
action.parents.append(parent_node.uuid)
|
||||
|
||||
if leaf_groups:
|
||||
leaf_groups = []
|
||||
leaf_groups.extend([a for a in actions_chunk])
|
||||
|
||||
return action_graph
|
||||
|
||||
def schedule(self, context, audit_id, solution):
|
||||
LOG.debug('Creating an action plan for the audit uuid: %s', audit_id)
|
||||
action_plan = self.create_action_plan(context, audit_id, solution)
|
||||
|
||||
sorted_weighted_actions = self.get_sorted_actions_by_weight(
|
||||
context, action_plan, solution)
|
||||
action_graph = self.compute_action_graph(sorted_weighted_actions)
|
||||
|
||||
self._create_efficacy_indicators(
|
||||
context, action_plan.id, solution.efficacy_indicators)
|
||||
|
||||
if len(action_graph.nodes()) == 0:
|
||||
LOG.warning(_LW("The action plan is empty"))
|
||||
action_plan.state = objects.action_plan.State.SUCCEEDED
|
||||
action_plan.save()
|
||||
|
||||
self.create_scheduled_actions(action_plan, action_graph)
|
||||
return action_plan
|
||||
|
||||
def get_sorted_actions_by_weight(self, context, action_plan, solution):
|
||||
# We need to make them immutable to add them to the graph
|
||||
action_objects = list([
|
||||
objects.Action(
|
||||
context, uuid=utils.generate_uuid(), parents=[],
|
||||
action_plan_id=action_plan.id, **a)
|
||||
for a in solution.actions])
|
||||
# This is a dict of list with each being a weight and the list being
|
||||
# all the actions associated to this weight
|
||||
weighted_actions = collections.defaultdict(list)
|
||||
for action in action_objects:
|
||||
action_weight = self.config.weights[action.action_type]
|
||||
weighted_actions[action_weight].append(action)
|
||||
|
||||
return reversed(sorted(weighted_actions.items(), key=lambda x: x[0]))
|
||||
|
||||
def create_scheduled_actions(self, action_plan, graph):
|
||||
for action in graph.nodes():
|
||||
LOG.debug("Creating the %s in the Watcher database",
|
||||
action.action_type)
|
||||
try:
|
||||
action.create()
|
||||
except Exception as exc:
|
||||
LOG.exception(exc)
|
||||
raise
|
||||
|
||||
def create_action_plan(self, context, audit_id, solution):
|
||||
strategy = objects.Strategy.get_by_name(
|
||||
context, solution.strategy.name)
|
||||
|
||||
action_plan_dict = {
|
||||
'uuid': utils.generate_uuid(),
|
||||
'audit_id': audit_id,
|
||||
'strategy_id': strategy.id,
|
||||
'state': objects.action_plan.State.RECOMMENDED,
|
||||
'global_efficacy': solution.global_efficacy,
|
||||
}
|
||||
|
||||
new_action_plan = objects.ActionPlan(context, **action_plan_dict)
|
||||
new_action_plan.create()
|
||||
|
||||
return new_action_plan
|
||||
|
||||
def _create_efficacy_indicators(self, context, action_plan_id, indicators):
|
||||
efficacy_indicators = []
|
||||
for indicator in indicators:
|
||||
efficacy_indicator_dict = {
|
||||
'uuid': utils.generate_uuid(),
|
||||
'name': indicator.name,
|
||||
'description': indicator.description,
|
||||
'unit': indicator.unit,
|
||||
'value': indicator.value,
|
||||
'action_plan_id': action_plan_id,
|
||||
}
|
||||
new_efficacy_indicator = objects.EfficacyIndicator(
|
||||
context, **efficacy_indicator_dict)
|
||||
new_efficacy_indicator.create()
|
||||
|
||||
efficacy_indicators.append(new_efficacy_indicator)
|
||||
return efficacy_indicators
|
||||
301
watcher/decision_engine/planner/workload_stabilization.py
Normal file
301
watcher/decision_engine/planner/workload_stabilization.py
Normal file
@@ -0,0 +1,301 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# 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 oslo_config import cfg
|
||||
from oslo_config import types
|
||||
from oslo_log import log
|
||||
|
||||
from watcher._i18n import _LW
|
||||
from watcher.common import clients
|
||||
from watcher.common import exception
|
||||
from watcher.common import nova_helper
|
||||
from watcher.common import utils
|
||||
from watcher.decision_engine.planner import base
|
||||
from watcher import objects
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class WorkloadStabilizationPlanner(base.BasePlanner):
|
||||
"""Workload Stabilization planner implementation
|
||||
|
||||
This implementation comes with basic rules with a set of action types that
|
||||
are weighted. An action having a lower weight will be scheduled before the
|
||||
other ones. The set of action types can be specified by 'weights' in the
|
||||
``watcher.conf``. You need to associate a different weight to all available
|
||||
actions into the configuration file, otherwise you will get an error when
|
||||
the new action will be referenced in the solution produced by a strategy.
|
||||
|
||||
*Limitations*
|
||||
|
||||
- This is a proof of concept that is not meant to be used in production
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
super(WorkloadStabilizationPlanner, self).__init__(config)
|
||||
self._osc = clients.OpenStackClients()
|
||||
|
||||
@property
|
||||
def osc(self):
|
||||
return self._osc
|
||||
|
||||
weights_dict = {
|
||||
'turn_host_to_acpi_s3_state': 0,
|
||||
'resize': 1,
|
||||
'migrate': 2,
|
||||
'sleep': 3,
|
||||
'change_nova_service_state': 4,
|
||||
'nop': 5,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_config_opts(cls):
|
||||
return [
|
||||
cfg.Opt(
|
||||
'weights',
|
||||
type=types.Dict(value_type=types.Integer()),
|
||||
help="These weights are used to schedule the actions",
|
||||
default=cls.weights_dict),
|
||||
]
|
||||
|
||||
def create_action(self,
|
||||
action_plan_id,
|
||||
action_type,
|
||||
input_parameters=None):
|
||||
uuid = utils.generate_uuid()
|
||||
action = {
|
||||
'uuid': uuid,
|
||||
'action_plan_id': int(action_plan_id),
|
||||
'action_type': action_type,
|
||||
'input_parameters': input_parameters,
|
||||
'state': objects.action.State.PENDING,
|
||||
'parents': None
|
||||
}
|
||||
|
||||
return action
|
||||
|
||||
def load_child_class(self, child_name):
|
||||
for c in BaseActionValidator.__subclasses__():
|
||||
if child_name == c.action_name:
|
||||
return c()
|
||||
return None
|
||||
|
||||
def schedule(self, context, audit_id, solution):
|
||||
LOG.debug('Creating an action plan for the audit uuid: %s', audit_id)
|
||||
weights = self.config.weights
|
||||
action_plan = self._create_action_plan(context, audit_id, solution)
|
||||
|
||||
actions = list(solution.actions)
|
||||
to_schedule = []
|
||||
for action in actions:
|
||||
json_action = self.create_action(
|
||||
action_plan_id=action_plan.id,
|
||||
action_type=action.get('action_type'),
|
||||
input_parameters=action.get('input_parameters'))
|
||||
to_schedule.append((weights[action.get('action_type')],
|
||||
json_action))
|
||||
|
||||
self._create_efficacy_indicators(
|
||||
context, action_plan.id, solution.efficacy_indicators)
|
||||
|
||||
# scheduling
|
||||
scheduled = sorted(to_schedule, key=lambda weight: (weight[0]),
|
||||
reverse=True)
|
||||
if len(scheduled) == 0:
|
||||
LOG.warning(_LW("The action plan is empty"))
|
||||
action_plan.state = objects.action_plan.State.SUCCEEDED
|
||||
action_plan.save()
|
||||
else:
|
||||
resource_action_map = {}
|
||||
scheduled_actions = [x[1] for x in scheduled]
|
||||
for action in scheduled_actions:
|
||||
a_type = action['action_type']
|
||||
if a_type != 'turn_host_to_acpi_s3_state':
|
||||
plugin_action = self.load_child_class(
|
||||
action.get("action_type"))
|
||||
if not plugin_action:
|
||||
raise exception.UnsupportedActionType(
|
||||
action_type=action.get("action_type"))
|
||||
db_action = self._create_action(context, action)
|
||||
parents = plugin_action.validate_parents(
|
||||
resource_action_map, action)
|
||||
if parents:
|
||||
db_action.parents = parents
|
||||
db_action.save()
|
||||
# if we have an action that will make host unreachable, we need
|
||||
# to complete all actions (resize and migration type)
|
||||
# related to the host.
|
||||
# Note(alexchadin): turn_host_to_acpi_s3_state doesn't
|
||||
# actually exist. Placed code shows relations between
|
||||
# action types.
|
||||
# TODO(alexchadin): add turn_host_to_acpi_s3_state action type.
|
||||
else:
|
||||
host_to_acpi_s3 = action['input_parameters']['resource_id']
|
||||
host_actions = resource_action_map.get(host_to_acpi_s3)
|
||||
action_parents = []
|
||||
if host_actions:
|
||||
resize_actions = [x[0] for x in host_actions
|
||||
if x[1] == 'resize']
|
||||
migrate_actions = [x[0] for x in host_actions
|
||||
if x[1] == 'migrate']
|
||||
resize_migration_parents = [
|
||||
x.parents for x in
|
||||
[objects.Action.get_by_uuid(context, resize_action)
|
||||
for resize_action in resize_actions]]
|
||||
# resize_migration_parents should be one level list
|
||||
resize_migration_parents = [
|
||||
parent for sublist in resize_migration_parents
|
||||
for parent in sublist]
|
||||
action_parents.extend([uuid for uuid in
|
||||
resize_actions])
|
||||
action_parents.extend([uuid for uuid in
|
||||
migrate_actions if uuid not in
|
||||
resize_migration_parents])
|
||||
db_action = self._create_action(context, action)
|
||||
db_action.parents = action_parents
|
||||
db_action.save()
|
||||
|
||||
return action_plan
|
||||
|
||||
def _create_action_plan(self, context, audit_id, solution):
|
||||
strategy = objects.Strategy.get_by_name(
|
||||
context, solution.strategy.name)
|
||||
|
||||
action_plan_dict = {
|
||||
'uuid': utils.generate_uuid(),
|
||||
'audit_id': audit_id,
|
||||
'strategy_id': strategy.id,
|
||||
'state': objects.action_plan.State.RECOMMENDED,
|
||||
'global_efficacy': solution.global_efficacy,
|
||||
}
|
||||
|
||||
new_action_plan = objects.ActionPlan(context, **action_plan_dict)
|
||||
new_action_plan.create()
|
||||
|
||||
return new_action_plan
|
||||
|
||||
def _create_efficacy_indicators(self, context, action_plan_id, indicators):
|
||||
efficacy_indicators = []
|
||||
for indicator in indicators:
|
||||
efficacy_indicator_dict = {
|
||||
'uuid': utils.generate_uuid(),
|
||||
'name': indicator.name,
|
||||
'description': indicator.description,
|
||||
'unit': indicator.unit,
|
||||
'value': indicator.value,
|
||||
'action_plan_id': action_plan_id,
|
||||
}
|
||||
new_efficacy_indicator = objects.EfficacyIndicator(
|
||||
context, **efficacy_indicator_dict)
|
||||
new_efficacy_indicator.create()
|
||||
|
||||
efficacy_indicators.append(new_efficacy_indicator)
|
||||
return efficacy_indicators
|
||||
|
||||
def _create_action(self, context, _action):
|
||||
try:
|
||||
LOG.debug("Creating the %s in the Watcher database",
|
||||
_action.get("action_type"))
|
||||
|
||||
new_action = objects.Action(context, **_action)
|
||||
new_action.create()
|
||||
|
||||
return new_action
|
||||
except Exception as exc:
|
||||
LOG.exception(exc)
|
||||
raise
|
||||
|
||||
|
||||
class BaseActionValidator(object):
|
||||
action_name = None
|
||||
|
||||
def __init__(self):
|
||||
super(BaseActionValidator, self).__init__()
|
||||
self._osc = None
|
||||
|
||||
@property
|
||||
def osc(self):
|
||||
if not self._osc:
|
||||
self._osc = clients.OpenStackClients()
|
||||
return self._osc
|
||||
|
||||
@abc.abstractmethod
|
||||
def validate_parents(self, resource_action_map, action):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _mapping(self, resource_action_map, resource_id, action_uuid,
|
||||
action_type):
|
||||
if resource_id not in resource_action_map:
|
||||
resource_action_map[resource_id] = [(action_uuid,
|
||||
action_type,)]
|
||||
else:
|
||||
resource_action_map[resource_id].append((action_uuid,
|
||||
action_type,))
|
||||
|
||||
|
||||
class MigrationActionValidator(BaseActionValidator):
|
||||
action_name = "migrate"
|
||||
|
||||
def validate_parents(self, resource_action_map, action):
|
||||
instance_uuid = action['input_parameters']['resource_id']
|
||||
host_name = action['input_parameters']['source_node']
|
||||
self._mapping(resource_action_map, instance_uuid, action['uuid'],
|
||||
'migrate')
|
||||
self._mapping(resource_action_map, host_name, action['uuid'],
|
||||
'migrate')
|
||||
|
||||
|
||||
class ResizeActionValidator(BaseActionValidator):
|
||||
action_name = "resize"
|
||||
|
||||
def validate_parents(self, resource_action_map, action):
|
||||
nova = nova_helper.NovaHelper(osc=self.osc)
|
||||
instance_uuid = action['input_parameters']['resource_id']
|
||||
parent_actions = resource_action_map.get(instance_uuid)
|
||||
host_of_instance = nova.get_hostname(
|
||||
nova.get_instance_by_uuid(instance_uuid)[0])
|
||||
self._mapping(resource_action_map, host_of_instance, action['uuid'],
|
||||
'resize')
|
||||
if parent_actions:
|
||||
return [x[0] for x in parent_actions]
|
||||
else:
|
||||
return []
|
||||
|
||||
|
||||
class ChangeNovaServiceStateActionValidator(BaseActionValidator):
|
||||
action_name = "change_nova_service_state"
|
||||
|
||||
def validate_parents(self, resource_action_map, action):
|
||||
host_name = action['input_parameters']['resource_id']
|
||||
self._mapping(resource_action_map, host_name, action.uuid,
|
||||
'change_nova_service_state')
|
||||
return []
|
||||
|
||||
|
||||
class SleepActionValidator(BaseActionValidator):
|
||||
action_name = "sleep"
|
||||
|
||||
def validate_parents(self, resource_action_map, action):
|
||||
return []
|
||||
|
||||
|
||||
class NOPActionValidator(BaseActionValidator):
|
||||
action_name = "nop"
|
||||
|
||||
def validate_parents(self, resource_action_map, action):
|
||||
return []
|
||||
@@ -13,9 +13,6 @@
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import copy
|
||||
|
||||
from oslo_log import log
|
||||
|
||||
@@ -101,9 +98,8 @@ class DefaultScope(base.BaseScope):
|
||||
self._osc = osc
|
||||
self.wrapper = nova_helper.NovaHelper(osc=self._osc)
|
||||
|
||||
def _remove_instance(self, cluster_model, instance_uuid, node_name):
|
||||
def remove_instance(self, cluster_model, instance, node_name):
|
||||
node = cluster_model.get_node_by_uuid(node_name)
|
||||
instance = cluster_model.get_instance_by_uuid(instance_uuid)
|
||||
cluster_model.delete_instance(instance, node)
|
||||
|
||||
def _check_wildcard(self, aggregate_list):
|
||||
@@ -147,7 +143,7 @@ class DefaultScope(base.BaseScope):
|
||||
if zone.zoneName in zone_names or include_all_nodes:
|
||||
allowed_nodes.extend(zone.hosts.keys())
|
||||
|
||||
def _exclude_resources(self, resources, **kwargs):
|
||||
def exclude_resources(self, resources, **kwargs):
|
||||
instances_to_exclude = kwargs.get('instances')
|
||||
nodes_to_exclude = kwargs.get('nodes')
|
||||
for resource in resources:
|
||||
@@ -160,37 +156,38 @@ class DefaultScope(base.BaseScope):
|
||||
[host['name'] for host
|
||||
in resource['compute_nodes']])
|
||||
|
||||
def _remove_node_from_model(self, nodes_to_remove, cluster_model):
|
||||
for node_name in nodes_to_remove:
|
||||
instances = copy.copy(
|
||||
cluster_model.get_mapping().get_node_instances_by_uuid(
|
||||
node_name))
|
||||
for instance_uuid in instances:
|
||||
self._remove_instance(cluster_model, instance_uuid, node_name)
|
||||
node = cluster_model.get_node_by_uuid(node_name)
|
||||
def remove_nodes_from_model(self, nodes_to_remove, cluster_model):
|
||||
for node_uuid in nodes_to_remove:
|
||||
node = cluster_model.get_node_by_uuid(node_uuid)
|
||||
instances = cluster_model.get_node_instances(node)
|
||||
for instance in instances:
|
||||
self.remove_instance(cluster_model, instance, node_uuid)
|
||||
cluster_model.remove_node(node)
|
||||
|
||||
def _remove_instances_from_model(self, instances_to_remove, cluster_model):
|
||||
def remove_instances_from_model(self, instances_to_remove, cluster_model):
|
||||
for instance_uuid in instances_to_remove:
|
||||
try:
|
||||
node_name = (cluster_model.get_mapping()
|
||||
.get_node_by_instance_uuid(instance_uuid).uuid)
|
||||
except KeyError:
|
||||
node_name = cluster_model.get_node_by_instance_uuid(
|
||||
instance_uuid).uuid
|
||||
except exception.InstanceNotFound:
|
||||
LOG.warning(_LW("The following instance %s cannot be found. "
|
||||
"It might be deleted from CDM along with node"
|
||||
" instance was hosted on."),
|
||||
instance_uuid)
|
||||
continue
|
||||
self._remove_instance(cluster_model, instance_uuid, node_name)
|
||||
self.remove_instance(
|
||||
cluster_model,
|
||||
cluster_model.get_instance_by_uuid(instance_uuid),
|
||||
node_name)
|
||||
|
||||
def get_scoped_model(self, cluster_model):
|
||||
"""Leave only nodes and instances proposed in the audit scope"""
|
||||
|
||||
if not cluster_model:
|
||||
return None
|
||||
|
||||
allowed_nodes = []
|
||||
nodes_to_exclude = []
|
||||
nodes_to_remove = set()
|
||||
instances_to_exclude = []
|
||||
model_hosts = list(cluster_model.get_all_compute_nodes().keys())
|
||||
|
||||
@@ -205,15 +202,16 @@ class DefaultScope(base.BaseScope):
|
||||
self._collect_zones(rule['availability_zones'],
|
||||
allowed_nodes)
|
||||
elif 'exclude' in rule:
|
||||
self._exclude_resources(
|
||||
self.exclude_resources(
|
||||
rule['exclude'], instances=instances_to_exclude,
|
||||
nodes=nodes_to_exclude)
|
||||
|
||||
instances_to_remove = set(instances_to_exclude)
|
||||
nodes_to_remove = set(model_hosts) - set(allowed_nodes)
|
||||
if allowed_nodes:
|
||||
nodes_to_remove = set(model_hosts) - set(allowed_nodes)
|
||||
nodes_to_remove.update(nodes_to_exclude)
|
||||
|
||||
self._remove_node_from_model(nodes_to_remove, cluster_model)
|
||||
self._remove_instances_from_model(instances_to_remove, cluster_model)
|
||||
self.remove_nodes_from_model(nodes_to_remove, cluster_model)
|
||||
self.remove_instances_from_model(instances_to_remove, cluster_model)
|
||||
|
||||
return cluster_model
|
||||
|
||||
@@ -41,6 +41,9 @@ class DefaultSolution(base.BaseSolution):
|
||||
if baction.BaseAction.RESOURCE_ID in input_parameters.keys():
|
||||
raise exception.ReservedWord(name=baction.BaseAction.
|
||||
RESOURCE_ID)
|
||||
else:
|
||||
input_parameters = {}
|
||||
|
||||
if resource_id is not None:
|
||||
input_parameters[baction.BaseAction.RESOURCE_ID] = resource_id
|
||||
action = {
|
||||
|
||||
@@ -35,11 +35,13 @@ migration is possible on your OpenStack cluster.
|
||||
|
||||
"""
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
|
||||
from watcher._i18n import _, _LE, _LI, _LW
|
||||
from watcher.common import exception
|
||||
from watcher.decision_engine.cluster.history import ceilometer as cch
|
||||
from watcher.datasource import ceilometer as ceil
|
||||
from watcher.datasource import monasca as mon
|
||||
from watcher.decision_engine.model import element
|
||||
from watcher.decision_engine.strategy.strategies import base
|
||||
|
||||
@@ -52,6 +54,15 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
HOST_CPU_USAGE_METRIC_NAME = 'compute.node.cpu.percent'
|
||||
INSTANCE_CPU_USAGE_METRIC_NAME = 'cpu_util'
|
||||
|
||||
METRIC_NAMES = dict(
|
||||
ceilometer=dict(
|
||||
host_cpu_usage='compute.node.cpu.percent',
|
||||
instance_cpu_usage='cpu_util'),
|
||||
monasca=dict(
|
||||
host_cpu_usage='cpu.percent',
|
||||
instance_cpu_usage='vm.cpu.utilization_perc'),
|
||||
)
|
||||
|
||||
MIGRATION = "migrate"
|
||||
CHANGE_NOVA_SERVICE_STATE = "change_nova_service_state"
|
||||
|
||||
@@ -64,6 +75,8 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
"""
|
||||
super(BasicConsolidation, self).__init__(config, osc)
|
||||
|
||||
# set default value for the number of enabled compute nodes
|
||||
self.number_of_enabled_nodes = 0
|
||||
# set default value for the number of released nodes
|
||||
self.number_of_released_nodes = 0
|
||||
# set default value for the number of migrations
|
||||
@@ -73,6 +86,7 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
self.efficacy = 100
|
||||
|
||||
self._ceilometer = None
|
||||
self._monasca = None
|
||||
|
||||
# TODO(jed): improve threshold overbooking?
|
||||
self.threshold_mem = 1
|
||||
@@ -87,6 +101,10 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
def migration_attempts(self):
|
||||
return self.input_parameters.get('migration_attempts', 0)
|
||||
|
||||
@property
|
||||
def period(self):
|
||||
return self.input_parameters.get('period', 7200)
|
||||
|
||||
@classmethod
|
||||
def get_display_name(cls):
|
||||
return _("Basic offline consolidation")
|
||||
@@ -108,19 +126,45 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
"type": "number",
|
||||
"default": 0
|
||||
},
|
||||
"period": {
|
||||
"description": "The time interval in seconds for "
|
||||
"getting statistic aggregation",
|
||||
"type": "number",
|
||||
"default": 7200
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_config_opts(cls):
|
||||
return [
|
||||
cfg.StrOpt(
|
||||
"datasource",
|
||||
help="Data source to use in order to query the needed metrics",
|
||||
default="ceilometer",
|
||||
choices=["ceilometer", "monasca"]),
|
||||
]
|
||||
|
||||
@property
|
||||
def ceilometer(self):
|
||||
if self._ceilometer is None:
|
||||
self._ceilometer = cch.CeilometerClusterHistory(osc=self.osc)
|
||||
self._ceilometer = ceil.CeilometerHelper(osc=self.osc)
|
||||
return self._ceilometer
|
||||
|
||||
@ceilometer.setter
|
||||
def ceilometer(self, ceilometer):
|
||||
self._ceilometer = ceilometer
|
||||
|
||||
@property
|
||||
def monasca(self):
|
||||
if self._monasca is None:
|
||||
self._monasca = mon.MonascaHelper(osc=self.osc)
|
||||
return self._monasca
|
||||
|
||||
@monasca.setter
|
||||
def monasca(self, monasca):
|
||||
self._monasca = monasca
|
||||
|
||||
def check_migration(self, source_node, destination_node,
|
||||
instance_to_migrate):
|
||||
"""Check if the migration is possible
|
||||
@@ -139,24 +183,16 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
total_cores = 0
|
||||
total_disk = 0
|
||||
total_mem = 0
|
||||
cpu_capacity = self.compute_model.get_resource_by_uuid(
|
||||
element.ResourceType.cpu_cores)
|
||||
disk_capacity = self.compute_model.get_resource_by_uuid(
|
||||
element.ResourceType.disk)
|
||||
memory_capacity = self.compute_model.get_resource_by_uuid(
|
||||
element.ResourceType.memory)
|
||||
|
||||
for instance_id in self.compute_model.mapping.get_node_instances(
|
||||
for instance in self.compute_model.get_node_instances(
|
||||
destination_node):
|
||||
instance = self.compute_model.get_instance_by_uuid(instance_id)
|
||||
total_cores += cpu_capacity.get_capacity(instance)
|
||||
total_disk += disk_capacity.get_capacity(instance)
|
||||
total_mem += memory_capacity.get_capacity(instance)
|
||||
total_cores += instance.vcpus
|
||||
total_disk += instance.disk
|
||||
total_mem += instance.memory
|
||||
|
||||
# capacity requested by the compute node
|
||||
total_cores += cpu_capacity.get_capacity(instance_to_migrate)
|
||||
total_disk += disk_capacity.get_capacity(instance_to_migrate)
|
||||
total_mem += memory_capacity.get_capacity(instance_to_migrate)
|
||||
total_cores += instance_to_migrate.vcpus
|
||||
total_disk += instance_to_migrate.disk
|
||||
total_mem += instance_to_migrate.memory
|
||||
|
||||
return self.check_threshold(destination_node, total_cores, total_disk,
|
||||
total_mem)
|
||||
@@ -175,12 +211,9 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
:param total_mem: total memory used by the virtual machine
|
||||
:return: True if the threshold is not exceed
|
||||
"""
|
||||
cpu_capacity = self.compute_model.get_resource_by_uuid(
|
||||
element.ResourceType.cpu_cores).get_capacity(destination_node)
|
||||
disk_capacity = self.compute_model.get_resource_by_uuid(
|
||||
element.ResourceType.disk).get_capacity(destination_node)
|
||||
memory_capacity = self.compute_model.get_resource_by_uuid(
|
||||
element.ResourceType.memory).get_capacity(destination_node)
|
||||
cpu_capacity = destination_node.vcpus
|
||||
disk_capacity = destination_node.disk
|
||||
memory_capacity = destination_node.memory
|
||||
|
||||
return (cpu_capacity >= total_cores * self.threshold_cores and
|
||||
disk_capacity >= total_disk * self.threshold_disk and
|
||||
@@ -196,14 +229,9 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
:param total_memory_used:
|
||||
:return:
|
||||
"""
|
||||
cpu_capacity = self.compute_model.get_resource_by_uuid(
|
||||
element.ResourceType.cpu_cores).get_capacity(compute_resource)
|
||||
|
||||
disk_capacity = self.compute_model.get_resource_by_uuid(
|
||||
element.ResourceType.disk).get_capacity(compute_resource)
|
||||
|
||||
memory_capacity = self.compute_model.get_resource_by_uuid(
|
||||
element.ResourceType.memory).get_capacity(compute_resource)
|
||||
cpu_capacity = compute_resource.vcpus
|
||||
disk_capacity = compute_resource.disk
|
||||
memory_capacity = compute_resource.memory
|
||||
|
||||
score_cores = (1 - (float(cpu_capacity) - float(total_cores_used)) /
|
||||
float(cpu_capacity))
|
||||
@@ -221,6 +249,64 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
# TODO(jed): take in account weight
|
||||
return (score_cores + score_disk + score_memory) / 3
|
||||
|
||||
def get_node_cpu_usage(self, node):
|
||||
metric_name = self.METRIC_NAMES[
|
||||
self.config.datasource]['host_cpu_usage']
|
||||
if self.config.datasource == "ceilometer":
|
||||
resource_id = "%s_%s" % (node.uuid, node.hostname)
|
||||
return self.ceilometer.statistic_aggregation(
|
||||
resource_id=resource_id,
|
||||
meter_name=metric_name,
|
||||
period=self.period,
|
||||
aggregate='avg',
|
||||
)
|
||||
elif self.config.datasource == "monasca":
|
||||
statistics = self.monasca.statistic_aggregation(
|
||||
meter_name=metric_name,
|
||||
dimensions=dict(hostname=node.uuid),
|
||||
period=self.period,
|
||||
aggregate='avg'
|
||||
)
|
||||
cpu_usage = None
|
||||
for stat in statistics:
|
||||
avg_col_idx = stat['columns'].index('avg')
|
||||
values = [r[avg_col_idx] for r in stat['statistics']]
|
||||
value = float(sum(values)) / len(values)
|
||||
cpu_usage = value
|
||||
|
||||
return cpu_usage
|
||||
|
||||
raise exception.UnsupportedDataSource(
|
||||
strategy=self.name, datasource=self.config.datasource)
|
||||
|
||||
def get_instance_cpu_usage(self, instance):
|
||||
metric_name = self.METRIC_NAMES[
|
||||
self.config.datasource]['instance_cpu_usage']
|
||||
if self.config.datasource == "ceilometer":
|
||||
return self.ceilometer.statistic_aggregation(
|
||||
resource_id=instance.uuid,
|
||||
meter_name=metric_name,
|
||||
period=self.period,
|
||||
aggregate='avg'
|
||||
)
|
||||
elif self.config.datasource == "monasca":
|
||||
statistics = self.monasca.statistic_aggregation(
|
||||
meter_name=metric_name,
|
||||
dimensions=dict(resource_id=instance.uuid),
|
||||
period=self.period,
|
||||
aggregate='avg'
|
||||
)
|
||||
cpu_usage = None
|
||||
for stat in statistics:
|
||||
avg_col_idx = stat['columns'].index('avg')
|
||||
values = [r[avg_col_idx] for r in stat['statistics']]
|
||||
value = float(sum(values)) / len(values)
|
||||
cpu_usage = value
|
||||
return cpu_usage
|
||||
|
||||
raise exception.UnsupportedDataSource(
|
||||
strategy=self.name, datasource=self.config.datasource)
|
||||
|
||||
def calculate_score_node(self, node):
|
||||
"""Calculate the score that represent the utilization level
|
||||
|
||||
@@ -228,65 +314,39 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
:return: Score for the given compute node
|
||||
:rtype: float
|
||||
"""
|
||||
resource_id = "%s_%s" % (node.uuid, node.hostname)
|
||||
host_avg_cpu_util = self.ceilometer.statistic_aggregation(
|
||||
resource_id=resource_id,
|
||||
meter_name=self.HOST_CPU_USAGE_METRIC_NAME,
|
||||
period="7200",
|
||||
aggregate='avg')
|
||||
host_avg_cpu_util = self.get_node_cpu_usage(node)
|
||||
|
||||
if host_avg_cpu_util is None:
|
||||
resource_id = "%s_%s" % (node.uuid, node.hostname)
|
||||
LOG.error(
|
||||
_LE("No values returned by %(resource_id)s "
|
||||
"for %(metric_name)s") % dict(
|
||||
resource_id=resource_id,
|
||||
metric_name=self.HOST_CPU_USAGE_METRIC_NAME))
|
||||
metric_name=self.METRIC_NAMES[
|
||||
self.config.datasource]['host_cpu_usage']))
|
||||
host_avg_cpu_util = 100
|
||||
|
||||
cpu_capacity = self.compute_model.get_resource_by_uuid(
|
||||
element.ResourceType.cpu_cores).get_capacity(node)
|
||||
|
||||
total_cores_used = cpu_capacity * (host_avg_cpu_util / 100.0)
|
||||
total_cores_used = node.vcpus * (host_avg_cpu_util / 100.0)
|
||||
|
||||
return self.calculate_weight(node, total_cores_used, 0, 0)
|
||||
|
||||
def calculate_migration_efficacy(self):
|
||||
"""Calculate migration efficacy
|
||||
|
||||
:return: The efficacy tells us that every VM migration resulted
|
||||
in releasing on node
|
||||
"""
|
||||
if self.number_of_migrations > 0:
|
||||
return (float(self.number_of_released_nodes) / float(
|
||||
self.number_of_migrations)) * 100
|
||||
else:
|
||||
return 0
|
||||
|
||||
def calculate_score_instance(self, instance):
|
||||
"""Calculate Score of virtual machine
|
||||
|
||||
:param instance: the virtual machine
|
||||
:return: score
|
||||
"""
|
||||
instance_cpu_utilization = self.ceilometer. \
|
||||
statistic_aggregation(
|
||||
resource_id=instance.uuid,
|
||||
meter_name=self.INSTANCE_CPU_USAGE_METRIC_NAME,
|
||||
period="7200",
|
||||
aggregate='avg'
|
||||
)
|
||||
instance_cpu_utilization = self.get_instance_cpu_usage(instance)
|
||||
if instance_cpu_utilization is None:
|
||||
LOG.error(
|
||||
_LE("No values returned by %(resource_id)s "
|
||||
"for %(metric_name)s") % dict(
|
||||
resource_id=instance.uuid,
|
||||
metric_name=self.INSTANCE_CPU_USAGE_METRIC_NAME))
|
||||
metric_name=self.METRIC_NAMES[
|
||||
self.config.datasource]['instance_cpu_usage']))
|
||||
instance_cpu_utilization = 100
|
||||
|
||||
cpu_capacity = self.compute_model.get_resource_by_uuid(
|
||||
element.ResourceType.cpu_cores).get_capacity(instance)
|
||||
|
||||
total_cores_used = cpu_capacity * (instance_cpu_utilization / 100.0)
|
||||
total_cores_used = instance.vcpus * (instance_cpu_utilization / 100.0)
|
||||
|
||||
return self.calculate_weight(instance, total_cores_used, 0, 0)
|
||||
|
||||
@@ -312,28 +372,27 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
"""Calculate score of nodes based on load by VMs"""
|
||||
score = []
|
||||
for node in self.compute_model.get_all_compute_nodes().values():
|
||||
count = self.compute_model.mapping.get_node_instances(node)
|
||||
if len(count) > 0:
|
||||
if node.status == element.ServiceState.ENABLED.value:
|
||||
self.number_of_enabled_nodes += 1
|
||||
|
||||
instances = self.compute_model.get_node_instances(node)
|
||||
if len(instances) > 0:
|
||||
result = self.calculate_score_node(node)
|
||||
else:
|
||||
# The node has not VMs
|
||||
result = 0
|
||||
if len(count) > 0:
|
||||
score.append((node.uuid, result))
|
||||
|
||||
return score
|
||||
|
||||
def node_and_instance_score(self, sorted_scores):
|
||||
"""Get List of VMs from node"""
|
||||
node_to_release = sorted_scores[len(sorted_scores) - 1][0]
|
||||
instances_to_migrate = self.compute_model.mapping.get_node_instances(
|
||||
instances_to_migrate = self.compute_model.get_node_instances(
|
||||
self.compute_model.get_node_by_uuid(node_to_release))
|
||||
|
||||
instance_score = []
|
||||
for instance_id in instances_to_migrate:
|
||||
instance = self.compute_model.get_instance_by_uuid(instance_id)
|
||||
for instance in instances_to_migrate:
|
||||
if instance.state == element.InstanceState.ACTIVE.value:
|
||||
instance_score.append(
|
||||
(instance_id, self.calculate_score_instance(instance)))
|
||||
(instance, self.calculate_score_instance(instance)))
|
||||
|
||||
return node_to_release, instance_score
|
||||
|
||||
@@ -346,8 +405,7 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
mig_source_node.uuid,
|
||||
mig_destination_node.uuid)
|
||||
|
||||
if len(self.compute_model.mapping.get_node_instances(
|
||||
mig_source_node)) == 0:
|
||||
if len(self.compute_model.get_node_instances(mig_source_node)) == 0:
|
||||
self.add_change_service_state(mig_source_node.
|
||||
uuid,
|
||||
element.ServiceState.DISABLED.value)
|
||||
@@ -356,10 +414,8 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
def calculate_num_migrations(self, sorted_instances, node_to_release,
|
||||
sorted_score):
|
||||
number_migrations = 0
|
||||
for instance in sorted_instances:
|
||||
for mig_instance, __ in sorted_instances:
|
||||
for j in range(0, len(sorted_score)):
|
||||
mig_instance = self.compute_model.get_instance_by_uuid(
|
||||
instance[0])
|
||||
mig_source_node = self.compute_model.get_node_by_uuid(
|
||||
node_to_release)
|
||||
mig_destination_node = self.compute_model.get_node_by_uuid(
|
||||
@@ -399,15 +455,6 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
def do_execute(self):
|
||||
unsuccessful_migration = 0
|
||||
|
||||
for node_uuid, node in self.compute_model.get_all_compute_nodes(
|
||||
).items():
|
||||
node_instances = self.compute_model.mapping.get_node_instances(
|
||||
node)
|
||||
if node_instances:
|
||||
if node.state == element.ServiceState.ENABLED:
|
||||
self.add_change_service_state(
|
||||
node_uuid, element.ServiceState.DISABLED.value)
|
||||
|
||||
scores = self.compute_score_of_nodes()
|
||||
# Sort compute nodes by Score decreasing
|
||||
sorted_scores = sorted(scores, reverse=True, key=lambda x: (x[1]))
|
||||
@@ -444,6 +491,7 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
sorted_scores.pop()
|
||||
|
||||
infos = {
|
||||
"compute_nodes_count": self.number_of_enabled_nodes,
|
||||
"released_compute_nodes_count": self.number_of_released_nodes,
|
||||
"instance_migrations_count": self.number_of_migrations,
|
||||
"efficacy": self.efficacy
|
||||
@@ -452,6 +500,7 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
|
||||
def post_execute(self):
|
||||
self.solution.set_efficacy_indicators(
|
||||
compute_nodes_count=self.number_of_enabled_nodes,
|
||||
released_compute_nodes_count=self.number_of_released_nodes,
|
||||
instance_migrations_count=self.number_of_migrations,
|
||||
)
|
||||
|
||||
@@ -60,12 +60,12 @@ class DummyStrategy(base.DummyBaseStrategy):
|
||||
self.solution.add_action(action_type=self.NOP,
|
||||
input_parameters=parameters)
|
||||
|
||||
parameters = {'message': 'Welcome'}
|
||||
parameters = {'message': para2}
|
||||
self.solution.add_action(action_type=self.NOP,
|
||||
input_parameters=parameters)
|
||||
|
||||
self.solution.add_action(action_type=self.SLEEP,
|
||||
input_parameters={'duration': 5.0})
|
||||
input_parameters={'duration': para1})
|
||||
|
||||
def post_execute(self):
|
||||
pass
|
||||
|
||||
121
watcher/decision_engine/strategy/strategies/dummy_with_resize.py
Normal file
121
watcher/decision_engine/strategy/strategies/dummy_with_resize.py
Normal file
@@ -0,0 +1,121 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# 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._i18n import _
|
||||
from watcher.decision_engine.strategy.strategies import base
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class DummyWithResize(base.DummyBaseStrategy):
|
||||
"""Dummy strategy used for integration testing via Tempest
|
||||
|
||||
*Description*
|
||||
|
||||
This strategy does not provide any useful optimization. Its only purpose
|
||||
is to be used by Tempest tests.
|
||||
|
||||
*Requirements*
|
||||
|
||||
<None>
|
||||
|
||||
*Limitations*
|
||||
|
||||
Do not use in production.
|
||||
|
||||
*Spec URL*
|
||||
|
||||
<None>
|
||||
"""
|
||||
|
||||
NOP = "nop"
|
||||
SLEEP = "sleep"
|
||||
|
||||
def pre_execute(self):
|
||||
pass
|
||||
|
||||
def do_execute(self):
|
||||
para1 = self.input_parameters.para1
|
||||
para2 = self.input_parameters.para2
|
||||
LOG.debug("Executing Dummy strategy with para1=%(p1)f, para2=%(p2)s",
|
||||
{'p1': para1, 'p2': para2})
|
||||
parameters = {'message': 'hello World'}
|
||||
self.solution.add_action(action_type=self.NOP,
|
||||
input_parameters=parameters)
|
||||
|
||||
parameters = {'message': 'Welcome'}
|
||||
self.solution.add_action(action_type=self.NOP,
|
||||
input_parameters=parameters)
|
||||
|
||||
self.solution.add_action(action_type=self.SLEEP,
|
||||
input_parameters={'duration': 5.0})
|
||||
self.solution.add_action(
|
||||
action_type='migrate',
|
||||
resource_id='b199db0c-1408-4d52-b5a5-5ca14de0ff36',
|
||||
input_parameters={
|
||||
'source_node': 'compute2',
|
||||
'destination_node': 'compute3',
|
||||
'migration_type': 'live'})
|
||||
|
||||
self.solution.add_action(
|
||||
action_type='migrate',
|
||||
resource_id='8db1b3c1-7938-4c34-8c03-6de14b874f8f',
|
||||
input_parameters={
|
||||
'source_node': 'compute2',
|
||||
'destination_node': 'compute3',
|
||||
'migration_type': 'live'}
|
||||
)
|
||||
self.solution.add_action(
|
||||
action_type='resize',
|
||||
resource_id='8db1b3c1-7938-4c34-8c03-6de14b874f8f',
|
||||
input_parameters={'flavor': 'x2'}
|
||||
)
|
||||
|
||||
def post_execute(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def get_name(cls):
|
||||
return "dummy_with_resize"
|
||||
|
||||
@classmethod
|
||||
def get_display_name(cls):
|
||||
return _("Dummy strategy with resize")
|
||||
|
||||
@classmethod
|
||||
def get_translatable_display_name(cls):
|
||||
return "Dummy strategy with resize"
|
||||
|
||||
@classmethod
|
||||
def get_schema(cls):
|
||||
# Mandatory default setting for each element
|
||||
return {
|
||||
"properties": {
|
||||
"para1": {
|
||||
"description": "number parameter example",
|
||||
"type": "number",
|
||||
"default": 3.2,
|
||||
"minimum": 1.0,
|
||||
"maximum": 10.2,
|
||||
},
|
||||
"para2": {
|
||||
"description": "string parameter example",
|
||||
"type": "string",
|
||||
"default": "hello"
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -32,7 +32,7 @@ from oslo_log import log
|
||||
|
||||
from watcher._i18n import _, _LW, _LI
|
||||
from watcher.common import exception as wexc
|
||||
from watcher.decision_engine.cluster.history import ceilometer as ceil
|
||||
from watcher.datasource import ceilometer as ceil
|
||||
from watcher.decision_engine.model import element
|
||||
from watcher.decision_engine.strategy.strategies import base
|
||||
|
||||
@@ -114,26 +114,23 @@ class OutletTempControl(base.ThermalOptimizationBaseStrategy):
|
||||
@property
|
||||
def ceilometer(self):
|
||||
if self._ceilometer is None:
|
||||
self._ceilometer = ceil.CeilometerClusterHistory(osc=self.osc)
|
||||
self._ceilometer = ceil.CeilometerHelper(osc=self.osc)
|
||||
return self._ceilometer
|
||||
|
||||
@ceilometer.setter
|
||||
def ceilometer(self, c):
|
||||
self._ceilometer = c
|
||||
|
||||
def calc_used_res(self, node, cpu_capacity,
|
||||
memory_capacity, disk_capacity):
|
||||
def calc_used_resource(self, node):
|
||||
"""Calculate the used vcpus, memory and disk based on VM flavors"""
|
||||
instances = self.compute_model.mapping.get_node_instances(node)
|
||||
instances = self.compute_model.get_node_instances(node)
|
||||
vcpus_used = 0
|
||||
memory_mb_used = 0
|
||||
disk_gb_used = 0
|
||||
if len(instances) > 0:
|
||||
for instance_id in instances:
|
||||
instance = self.compute_model.get_instance_by_uuid(instance_id)
|
||||
vcpus_used += cpu_capacity.get_capacity(instance)
|
||||
memory_mb_used += memory_capacity.get_capacity(instance)
|
||||
disk_gb_used += disk_capacity.get_capacity(instance)
|
||||
for instance in instances:
|
||||
vcpus_used += instance.vcpus
|
||||
memory_mb_used += instance.memory
|
||||
disk_gb_used += instance.disk
|
||||
|
||||
return vcpus_used, memory_mb_used, disk_gb_used
|
||||
|
||||
@@ -146,9 +143,7 @@ class OutletTempControl(base.ThermalOptimizationBaseStrategy):
|
||||
|
||||
hosts_need_release = []
|
||||
hosts_target = []
|
||||
for node_id in nodes:
|
||||
node = self.compute_model.get_node_by_uuid(
|
||||
node_id)
|
||||
for node in nodes.values():
|
||||
resource_id = node.uuid
|
||||
|
||||
outlet_temp = self.ceilometer.statistic_aggregation(
|
||||
@@ -174,49 +169,38 @@ class OutletTempControl(base.ThermalOptimizationBaseStrategy):
|
||||
"""Pick up an active instance to migrate from provided hosts"""
|
||||
for instance_data in hosts:
|
||||
mig_source_node = instance_data['node']
|
||||
instances_of_src = self.compute_model.mapping.get_node_instances(
|
||||
instances_of_src = self.compute_model.get_node_instances(
|
||||
mig_source_node)
|
||||
if len(instances_of_src) > 0:
|
||||
for instance_id in instances_of_src:
|
||||
try:
|
||||
# select the first active instance to migrate
|
||||
instance = self.compute_model.get_instance_by_uuid(
|
||||
instance_id)
|
||||
if (instance.state !=
|
||||
element.InstanceState.ACTIVE.value):
|
||||
LOG.info(_LI("Instance not active, skipped: %s"),
|
||||
instance.uuid)
|
||||
continue
|
||||
return mig_source_node, instance
|
||||
except wexc.InstanceNotFound as e:
|
||||
LOG.exception(e)
|
||||
LOG.info(_LI("Instance not found"))
|
||||
for instance in instances_of_src:
|
||||
try:
|
||||
# select the first active instance to migrate
|
||||
if (instance.state !=
|
||||
element.InstanceState.ACTIVE.value):
|
||||
LOG.info(_LI("Instance not active, skipped: %s"),
|
||||
instance.uuid)
|
||||
continue
|
||||
return mig_source_node, instance
|
||||
except wexc.InstanceNotFound as e:
|
||||
LOG.exception(e)
|
||||
LOG.info(_LI("Instance not found"))
|
||||
|
||||
return None
|
||||
|
||||
def filter_dest_servers(self, hosts, instance_to_migrate):
|
||||
"""Only return hosts with sufficient available resources"""
|
||||
cpu_capacity = self.compute_model.get_resource_by_uuid(
|
||||
element.ResourceType.cpu_cores)
|
||||
disk_capacity = self.compute_model.get_resource_by_uuid(
|
||||
element.ResourceType.disk)
|
||||
memory_capacity = self.compute_model.get_resource_by_uuid(
|
||||
element.ResourceType.memory)
|
||||
|
||||
required_cores = cpu_capacity.get_capacity(instance_to_migrate)
|
||||
required_disk = disk_capacity.get_capacity(instance_to_migrate)
|
||||
required_memory = memory_capacity.get_capacity(instance_to_migrate)
|
||||
required_cores = instance_to_migrate.vcpus
|
||||
required_disk = instance_to_migrate.disk
|
||||
required_memory = instance_to_migrate.memory
|
||||
|
||||
# filter nodes without enough resource
|
||||
dest_servers = []
|
||||
for instance_data in hosts:
|
||||
host = instance_data['node']
|
||||
# available
|
||||
cores_used, mem_used, disk_used = self.calc_used_res(
|
||||
host, cpu_capacity, memory_capacity, disk_capacity)
|
||||
cores_available = cpu_capacity.get_capacity(host) - cores_used
|
||||
disk_available = disk_capacity.get_capacity(host) - disk_used
|
||||
mem_available = memory_capacity.get_capacity(host) - mem_used
|
||||
cores_used, mem_used, disk_used = self.calc_used_resource(host)
|
||||
cores_available = host.vcpus - cores_used
|
||||
disk_available = host.disk - disk_used
|
||||
mem_available = host.memory - mem_used
|
||||
if cores_available >= required_cores \
|
||||
and disk_available >= required_disk \
|
||||
and mem_available >= required_memory:
|
||||
|
||||
@@ -45,9 +45,9 @@ airflow is higher than the specified threshold.
|
||||
|
||||
from oslo_log import log
|
||||
|
||||
from watcher._i18n import _, _LE, _LI, _LW
|
||||
from watcher._i18n import _, _LI, _LW
|
||||
from watcher.common import exception as wexc
|
||||
from watcher.decision_engine.cluster.history import ceilometer as ceil
|
||||
from watcher.datasource import ceilometer as ceil
|
||||
from watcher.decision_engine.model import element
|
||||
from watcher.decision_engine.strategy.strategies import base
|
||||
|
||||
@@ -110,7 +110,7 @@ class UniformAirflow(base.BaseStrategy):
|
||||
@property
|
||||
def ceilometer(self):
|
||||
if self._ceilometer is None:
|
||||
self._ceilometer = ceil.CeilometerClusterHistory(osc=self.osc)
|
||||
self._ceilometer = ceil.CeilometerHelper(osc=self.osc)
|
||||
return self._ceilometer
|
||||
|
||||
@ceilometer.setter
|
||||
@@ -164,18 +164,16 @@ class UniformAirflow(base.BaseStrategy):
|
||||
},
|
||||
}
|
||||
|
||||
def calculate_used_resource(self, node, cap_cores, cap_mem, cap_disk):
|
||||
def calculate_used_resource(self, node):
|
||||
"""Compute the used vcpus, memory and disk based on instance flavors"""
|
||||
instances = self.compute_model.mapping.get_node_instances(node)
|
||||
instances = self.compute_model.get_node_instances(node)
|
||||
vcpus_used = 0
|
||||
memory_mb_used = 0
|
||||
disk_gb_used = 0
|
||||
for instance_id in instances:
|
||||
instance = self.compute_model.get_instance_by_uuid(
|
||||
instance_id)
|
||||
vcpus_used += cap_cores.get_capacity(instance)
|
||||
memory_mb_used += cap_mem.get_capacity(instance)
|
||||
disk_gb_used += cap_disk.get_capacity(instance)
|
||||
for instance in instances:
|
||||
vcpus_used += instance.vcpus
|
||||
memory_mb_used += instance.memory
|
||||
disk_gb_used += instance.disk
|
||||
|
||||
return vcpus_used, memory_mb_used, disk_gb_used
|
||||
|
||||
@@ -187,7 +185,7 @@ class UniformAirflow(base.BaseStrategy):
|
||||
instances_tobe_migrate = []
|
||||
for nodemap in hosts:
|
||||
source_node = nodemap['node']
|
||||
source_instances = self.compute_model.mapping.get_node_instances(
|
||||
source_instances = self.compute_model.get_node_instances(
|
||||
source_node)
|
||||
if source_instances:
|
||||
inlet_t = self.ceilometer.statistic_aggregation(
|
||||
@@ -203,55 +201,36 @@ class UniformAirflow(base.BaseStrategy):
|
||||
if (power < self.threshold_power and
|
||||
inlet_t < self.threshold_inlet_t):
|
||||
# hardware issue, migrate all instances from this node
|
||||
for instance_id in source_instances:
|
||||
try:
|
||||
instance = (self.compute_model.
|
||||
get_instance_by_uuid(instance_id))
|
||||
instances_tobe_migrate.append(instance)
|
||||
except wexc.InstanceNotFound:
|
||||
LOG.error(_LE("Instance not found; error: %s"),
|
||||
instance_id)
|
||||
for instance in source_instances:
|
||||
instances_tobe_migrate.append(instance)
|
||||
return source_node, instances_tobe_migrate
|
||||
else:
|
||||
# migrate the first active instance
|
||||
for instance_id in source_instances:
|
||||
try:
|
||||
instance = (self.compute_model.
|
||||
get_instance_by_uuid(instance_id))
|
||||
if (instance.state !=
|
||||
element.InstanceState.ACTIVE.value):
|
||||
LOG.info(
|
||||
_LI("Instance not active, skipped: %s"),
|
||||
instance.uuid)
|
||||
continue
|
||||
instances_tobe_migrate.append(instance)
|
||||
return source_node, instances_tobe_migrate
|
||||
except wexc.InstanceNotFound:
|
||||
LOG.error(_LE("Instance not found; error: %s"),
|
||||
instance_id)
|
||||
for instance in source_instances:
|
||||
if (instance.state !=
|
||||
element.InstanceState.ACTIVE.value):
|
||||
LOG.info(
|
||||
_LI("Instance not active, skipped: %s"),
|
||||
instance.uuid)
|
||||
continue
|
||||
instances_tobe_migrate.append(instance)
|
||||
return source_node, instances_tobe_migrate
|
||||
else:
|
||||
LOG.info(_LI("Instance not found on node: %s"),
|
||||
source_node.uuid)
|
||||
|
||||
def filter_destination_hosts(self, hosts, instances_to_migrate):
|
||||
"""Find instance and host with sufficient available resources"""
|
||||
|
||||
cap_cores = self.compute_model.get_resource_by_uuid(
|
||||
element.ResourceType.cpu_cores)
|
||||
cap_disk = self.compute_model.get_resource_by_uuid(
|
||||
element.ResourceType.disk)
|
||||
cap_mem = self.compute_model.get_resource_by_uuid(
|
||||
element.ResourceType.memory)
|
||||
# large instance go first
|
||||
# large instances go first
|
||||
instances_to_migrate = sorted(
|
||||
instances_to_migrate, reverse=True,
|
||||
key=lambda x: (cap_cores.get_capacity(x)))
|
||||
key=lambda x: (x.vcpus))
|
||||
# find hosts for instances
|
||||
destination_hosts = []
|
||||
for instance_to_migrate in instances_to_migrate:
|
||||
required_cores = cap_cores.get_capacity(instance_to_migrate)
|
||||
required_disk = cap_disk.get_capacity(instance_to_migrate)
|
||||
required_mem = cap_mem.get_capacity(instance_to_migrate)
|
||||
required_cores = instance_to_migrate.vcpus
|
||||
required_disk = instance_to_migrate.disk
|
||||
required_mem = instance_to_migrate.memory
|
||||
dest_migrate_info = {}
|
||||
for nodemap in hosts:
|
||||
host = nodemap['node']
|
||||
@@ -259,13 +238,13 @@ class UniformAirflow(base.BaseStrategy):
|
||||
# calculate the available resources
|
||||
nodemap['cores_used'], nodemap['mem_used'],\
|
||||
nodemap['disk_used'] = self.calculate_used_resource(
|
||||
host, cap_cores, cap_mem, cap_disk)
|
||||
cores_available = (cap_cores.get_capacity(host) -
|
||||
host)
|
||||
cores_available = (host.vcpus -
|
||||
nodemap['cores_used'])
|
||||
disk_available = (cap_disk.get_capacity(host) -
|
||||
disk_available = (host.disk -
|
||||
nodemap['disk_used'])
|
||||
mem_available = (
|
||||
cap_mem.get_capacity(host) - nodemap['mem_used'])
|
||||
host.memory - nodemap['mem_used'])
|
||||
if (cores_available >= required_cores and
|
||||
disk_available >= required_disk and
|
||||
mem_available >= required_mem):
|
||||
|
||||
@@ -58,8 +58,7 @@ import six
|
||||
|
||||
from watcher._i18n import _, _LE, _LI
|
||||
from watcher.common import exception
|
||||
from watcher.decision_engine.cluster.history import ceilometer \
|
||||
as ceilometer_cluster_history
|
||||
from watcher.datasource import ceilometer as ceil
|
||||
from watcher.decision_engine.model import element
|
||||
from watcher.decision_engine.strategy.strategies import base
|
||||
|
||||
@@ -91,8 +90,7 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
@property
|
||||
def ceilometer(self):
|
||||
if self._ceilometer is None:
|
||||
self._ceilometer = (ceilometer_cluster_history.
|
||||
CeilometerClusterHistory(osc=self.osc))
|
||||
self._ceilometer = ceil.CeilometerHelper(osc=self.osc)
|
||||
return self._ceilometer
|
||||
|
||||
@ceilometer.setter
|
||||
@@ -110,9 +108,9 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
return state.value
|
||||
else:
|
||||
LOG.error(_LE('Unexpexted resource state type, '
|
||||
'state=%(state)s, state_type=%(st)s.'),
|
||||
state=state,
|
||||
st=type(state))
|
||||
'state=%(state)s, state_type=%(st)s.') % dict(
|
||||
state=state,
|
||||
st=type(state)))
|
||||
raise exception.WatcherException
|
||||
|
||||
def add_action_enable_compute_node(self, node):
|
||||
@@ -141,18 +139,14 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
input_parameters=params)
|
||||
self.number_of_released_nodes += 1
|
||||
|
||||
def add_migration(self, instance_uuid, source_node,
|
||||
destination_node, model):
|
||||
def add_migration(self, instance, source_node, destination_node):
|
||||
"""Add an action for VM migration into the solution.
|
||||
|
||||
:param instance_uuid: instance uuid
|
||||
:param instance: instance object
|
||||
:param source_node: node object
|
||||
:param destination_node: node object
|
||||
:param model: model_root object
|
||||
:return: None
|
||||
"""
|
||||
instance = model.get_instance_by_uuid(instance_uuid)
|
||||
|
||||
instance_state_str = self.get_state_str(instance.state)
|
||||
if instance_state_str != element.InstanceState.ACTIVE.value:
|
||||
# Watcher curently only supports live VM migration and block live
|
||||
@@ -161,9 +155,9 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
# migration mechanism to move non active VMs.
|
||||
LOG.error(
|
||||
_LE('Cannot live migrate: instance_uuid=%(instance_uuid)s, '
|
||||
'state=%(instance_state)s.'),
|
||||
instance_uuid=instance_uuid,
|
||||
instance_state=instance_state_str)
|
||||
'state=%(instance_state)s.') % dict(
|
||||
instance_uuid=instance.uuid,
|
||||
instance_state=instance_state_str))
|
||||
return
|
||||
|
||||
migration_type = 'live'
|
||||
@@ -171,41 +165,39 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
destination_node_state_str = self.get_state_str(destination_node.state)
|
||||
if destination_node_state_str == element.ServiceState.DISABLED.value:
|
||||
self.add_action_enable_compute_node(destination_node)
|
||||
model.mapping.unmap(source_node, instance)
|
||||
model.mapping.map(destination_node, instance)
|
||||
|
||||
params = {'migration_type': migration_type,
|
||||
'source_node': source_node.uuid,
|
||||
'destination_node': destination_node.uuid}
|
||||
self.solution.add_action(action_type='migrate',
|
||||
resource_id=instance.uuid,
|
||||
input_parameters=params)
|
||||
self.number_of_migrations += 1
|
||||
if self.compute_model.migrate_instance(
|
||||
instance, source_node, destination_node):
|
||||
params = {'migration_type': migration_type,
|
||||
'source_node': source_node.uuid,
|
||||
'destination_node': destination_node.uuid}
|
||||
self.solution.add_action(action_type='migrate',
|
||||
resource_id=instance.uuid,
|
||||
input_parameters=params)
|
||||
self.number_of_migrations += 1
|
||||
|
||||
def disable_unused_nodes(self, model):
|
||||
def disable_unused_nodes(self):
|
||||
"""Generate actions for disablity of unused nodes.
|
||||
|
||||
:param model: model_root object
|
||||
:return: None
|
||||
"""
|
||||
for node in model.get_all_compute_nodes().values():
|
||||
if (len(model.mapping.get_node_instances(node)) == 0 and
|
||||
for node in self.compute_model.get_all_compute_nodes().values():
|
||||
if (len(self.compute_model.get_node_instances(node)) == 0 and
|
||||
node.status !=
|
||||
element.ServiceState.DISABLED.value):
|
||||
self.add_action_disable_node(node)
|
||||
|
||||
def get_instance_utilization(self, instance_uuid, model,
|
||||
def get_instance_utilization(self, instance,
|
||||
period=3600, aggr='avg'):
|
||||
"""Collect cpu, ram and disk utilization statistics of a VM.
|
||||
|
||||
:param instance_uuid: instance object
|
||||
:param model: model_root object
|
||||
:param instance: instance object
|
||||
:param period: seconds
|
||||
:param aggr: string
|
||||
:return: dict(cpu(number of vcpus used), ram(MB used), disk(B used))
|
||||
"""
|
||||
if instance_uuid in self.ceilometer_instance_data_cache.keys():
|
||||
return self.ceilometer_instance_data_cache.get(instance_uuid)
|
||||
if instance.uuid in self.ceilometer_instance_data_cache.keys():
|
||||
return self.ceilometer_instance_data_cache.get(instance.uuid)
|
||||
|
||||
cpu_util_metric = 'cpu_util'
|
||||
ram_util_metric = 'memory.usage'
|
||||
@@ -213,61 +205,54 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
ram_alloc_metric = 'memory'
|
||||
disk_alloc_metric = 'disk.root.size'
|
||||
instance_cpu_util = self.ceilometer.statistic_aggregation(
|
||||
resource_id=instance_uuid, meter_name=cpu_util_metric,
|
||||
resource_id=instance.uuid, meter_name=cpu_util_metric,
|
||||
period=period, aggregate=aggr)
|
||||
instance_cpu_cores = model.get_resource_by_uuid(
|
||||
element.ResourceType.cpu_cores).get_capacity(
|
||||
model.get_instance_by_uuid(instance_uuid))
|
||||
|
||||
if instance_cpu_util:
|
||||
total_cpu_utilization = (
|
||||
instance_cpu_cores * (instance_cpu_util / 100.0))
|
||||
instance.vcpus * (instance_cpu_util / 100.0))
|
||||
else:
|
||||
total_cpu_utilization = instance_cpu_cores
|
||||
total_cpu_utilization = instance.vcpus
|
||||
|
||||
instance_ram_util = self.ceilometer.statistic_aggregation(
|
||||
resource_id=instance_uuid, meter_name=ram_util_metric,
|
||||
resource_id=instance.uuid, meter_name=ram_util_metric,
|
||||
period=period, aggregate=aggr)
|
||||
|
||||
if not instance_ram_util:
|
||||
instance_ram_util = self.ceilometer.statistic_aggregation(
|
||||
resource_id=instance_uuid, meter_name=ram_alloc_metric,
|
||||
resource_id=instance.uuid, meter_name=ram_alloc_metric,
|
||||
period=period, aggregate=aggr)
|
||||
|
||||
instance_disk_util = self.ceilometer.statistic_aggregation(
|
||||
resource_id=instance_uuid, meter_name=disk_alloc_metric,
|
||||
resource_id=instance.uuid, meter_name=disk_alloc_metric,
|
||||
period=period, aggregate=aggr)
|
||||
|
||||
if not instance_ram_util or not instance_disk_util:
|
||||
LOG.error(
|
||||
_LE('No values returned by %(resource_id)s '
|
||||
'for memory.usage or disk.root.size'),
|
||||
resource_id=instance_uuid
|
||||
)
|
||||
_LE('No values returned by %s for memory.usage '
|
||||
'or disk.root.size'), instance.uuid)
|
||||
raise exception.NoDataFound
|
||||
|
||||
self.ceilometer_instance_data_cache[instance_uuid] = dict(
|
||||
self.ceilometer_instance_data_cache[instance.uuid] = dict(
|
||||
cpu=total_cpu_utilization, ram=instance_ram_util,
|
||||
disk=instance_disk_util)
|
||||
return self.ceilometer_instance_data_cache.get(instance_uuid)
|
||||
return self.ceilometer_instance_data_cache.get(instance.uuid)
|
||||
|
||||
def get_node_utilization(self, node, model, period=3600, aggr='avg'):
|
||||
def get_node_utilization(self, node, period=3600, aggr='avg'):
|
||||
"""Collect cpu, ram and disk utilization statistics of a node.
|
||||
|
||||
:param node: node object
|
||||
:param model: model_root object
|
||||
:param period: seconds
|
||||
:param aggr: string
|
||||
:return: dict(cpu(number of cores used), ram(MB used), disk(B used))
|
||||
"""
|
||||
node_instances = model.mapping.get_node_instances_by_uuid(
|
||||
node.uuid)
|
||||
node_instances = self.compute_model.get_node_instances(node)
|
||||
node_ram_util = 0
|
||||
node_disk_util = 0
|
||||
node_cpu_util = 0
|
||||
for instance_uuid in node_instances:
|
||||
for instance in node_instances:
|
||||
instance_util = self.get_instance_utilization(
|
||||
instance_uuid, model, period, aggr)
|
||||
instance, period, aggr)
|
||||
node_cpu_util += instance_util['cpu']
|
||||
node_ram_util += instance_util['ram']
|
||||
node_disk_util += instance_util['disk']
|
||||
@@ -275,53 +260,40 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
return dict(cpu=node_cpu_util, ram=node_ram_util,
|
||||
disk=node_disk_util)
|
||||
|
||||
def get_node_capacity(self, node, model):
|
||||
def get_node_capacity(self, node):
|
||||
"""Collect cpu, ram and disk capacity of a node.
|
||||
|
||||
:param node: node object
|
||||
:param model: model_root object
|
||||
:return: dict(cpu(cores), ram(MB), disk(B))
|
||||
"""
|
||||
node_cpu_capacity = model.get_resource_by_uuid(
|
||||
element.ResourceType.cpu_cores).get_capacity(node)
|
||||
return dict(cpu=node.vcpus, ram=node.memory, disk=node.disk_capacity)
|
||||
|
||||
node_disk_capacity = model.get_resource_by_uuid(
|
||||
element.ResourceType.disk_capacity).get_capacity(node)
|
||||
|
||||
node_ram_capacity = model.get_resource_by_uuid(
|
||||
element.ResourceType.memory).get_capacity(node)
|
||||
return dict(cpu=node_cpu_capacity, ram=node_ram_capacity,
|
||||
disk=node_disk_capacity)
|
||||
|
||||
def get_relative_node_utilization(self, node, model):
|
||||
"""Return relative node utilization (rhu).
|
||||
def get_relative_node_utilization(self, node):
|
||||
"""Return relative node utilization.
|
||||
|
||||
:param node: node object
|
||||
:param model: model_root object
|
||||
:return: {'cpu': <0,1>, 'ram': <0,1>, 'disk': <0,1>}
|
||||
"""
|
||||
rhu = {}
|
||||
util = self.get_node_utilization(node, model)
|
||||
cap = self.get_node_capacity(node, model)
|
||||
relative_node_utilization = {}
|
||||
util = self.get_node_utilization(node)
|
||||
cap = self.get_node_capacity(node)
|
||||
for k in util.keys():
|
||||
rhu[k] = float(util[k]) / float(cap[k])
|
||||
return rhu
|
||||
relative_node_utilization[k] = float(util[k]) / float(cap[k])
|
||||
return relative_node_utilization
|
||||
|
||||
def get_relative_cluster_utilization(self, model):
|
||||
def get_relative_cluster_utilization(self):
|
||||
"""Calculate relative cluster utilization (rcu).
|
||||
|
||||
RCU is an average of relative utilizations (rhu) of active nodes.
|
||||
:param model: model_root object
|
||||
:return: {'cpu': <0,1>, 'ram': <0,1>, 'disk': <0,1>}
|
||||
"""
|
||||
nodes = model.get_all_compute_nodes().values()
|
||||
nodes = self.compute_model.get_all_compute_nodes().values()
|
||||
rcu = {}
|
||||
counters = {}
|
||||
for node in nodes:
|
||||
node_state_str = self.get_state_str(node.state)
|
||||
if node_state_str == element.ServiceState.ENABLED.value:
|
||||
rhu = self.get_relative_node_utilization(
|
||||
node, model)
|
||||
rhu = self.get_relative_node_utilization(node)
|
||||
for k in rhu.keys():
|
||||
if k not in rcu:
|
||||
rcu[k] = 0
|
||||
@@ -333,39 +305,35 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
rcu[k] /= counters[k]
|
||||
return rcu
|
||||
|
||||
def is_overloaded(self, node, model, cc):
|
||||
def is_overloaded(self, node, cc):
|
||||
"""Indicate whether a node is overloaded.
|
||||
|
||||
This considers provided resource capacity coefficients (cc).
|
||||
:param node: node object
|
||||
:param model: model_root object
|
||||
:param cc: dictionary containing resource capacity coefficients
|
||||
:return: [True, False]
|
||||
"""
|
||||
node_capacity = self.get_node_capacity(node, model)
|
||||
node_capacity = self.get_node_capacity(node)
|
||||
node_utilization = self.get_node_utilization(
|
||||
node, model)
|
||||
node)
|
||||
metrics = ['cpu']
|
||||
for m in metrics:
|
||||
if node_utilization[m] > node_capacity[m] * cc[m]:
|
||||
return True
|
||||
return False
|
||||
|
||||
def instance_fits(self, instance_uuid, node, model, cc):
|
||||
def instance_fits(self, instance, node, cc):
|
||||
"""Indicate whether is a node able to accommodate a VM.
|
||||
|
||||
This considers provided resource capacity coefficients (cc).
|
||||
:param instance_uuid: string
|
||||
:param instance: :py:class:`~.element.Instance`
|
||||
:param node: node object
|
||||
:param model: model_root object
|
||||
:param cc: dictionary containing resource capacity coefficients
|
||||
:return: [True, False]
|
||||
"""
|
||||
node_capacity = self.get_node_capacity(node, model)
|
||||
node_utilization = self.get_node_utilization(
|
||||
node, model)
|
||||
instance_utilization = self.get_instance_utilization(
|
||||
instance_uuid, model)
|
||||
node_capacity = self.get_node_capacity(node)
|
||||
node_utilization = self.get_node_utilization(node)
|
||||
instance_utilization = self.get_instance_utilization(instance)
|
||||
metrics = ['cpu', 'ram', 'disk']
|
||||
for m in metrics:
|
||||
if (instance_utilization[m] + node_utilization[m] >
|
||||
@@ -373,7 +341,7 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
return False
|
||||
return True
|
||||
|
||||
def optimize_solution(self, model):
|
||||
def optimize_solution(self):
|
||||
"""Optimize solution.
|
||||
|
||||
This is done by eliminating unnecessary or circular set of migrations
|
||||
@@ -386,8 +354,6 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
one migration instead of two.
|
||||
* A->B, B->A => remove A->B and B->A as they do not result
|
||||
in a new VM placement.
|
||||
|
||||
:param model: model_root object
|
||||
"""
|
||||
migrate_actions = (
|
||||
a for a in self.solution.actions if a[
|
||||
@@ -401,15 +367,20 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
'input_parameters'][
|
||||
'resource_id'] == instance_uuid)
|
||||
if len(actions) > 1:
|
||||
src = actions[0]['input_parameters']['source_node']
|
||||
dst = actions[-1]['input_parameters']['destination_node']
|
||||
src_uuid = actions[0]['input_parameters']['source_node']
|
||||
dst_uuid = actions[-1]['input_parameters']['destination_node']
|
||||
for a in actions:
|
||||
self.solution.actions.remove(a)
|
||||
self.number_of_migrations -= 1
|
||||
if src != dst:
|
||||
self.add_migration(instance_uuid, src, dst, model)
|
||||
src_node = self.compute_model.get_node_by_uuid(src_uuid)
|
||||
dst_node = self.compute_model.get_node_by_uuid(dst_uuid)
|
||||
instance = self.compute_model.get_instance_by_uuid(
|
||||
instance_uuid)
|
||||
if self.compute_model.migrate_instance(
|
||||
instance, dst_node, src_node):
|
||||
self.add_migration(instance, src_node, dst_node)
|
||||
|
||||
def offload_phase(self, model, cc):
|
||||
def offload_phase(self, cc):
|
||||
"""Perform offloading phase.
|
||||
|
||||
This considers provided resource capacity coefficients.
|
||||
@@ -425,29 +396,28 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
the node enabler in this phase doesn't necessarily results
|
||||
in more enabled nodes in the final solution.
|
||||
|
||||
:param model: model_root object
|
||||
:param cc: dictionary containing resource capacity coefficients
|
||||
"""
|
||||
sorted_nodes = sorted(
|
||||
model.get_all_compute_nodes().values(),
|
||||
key=lambda x: self.get_node_utilization(x, model)['cpu'])
|
||||
self.compute_model.get_all_compute_nodes().values(),
|
||||
key=lambda x: self.get_node_utilization(x)['cpu'])
|
||||
for node in reversed(sorted_nodes):
|
||||
if self.is_overloaded(node, model, cc):
|
||||
if self.is_overloaded(node, cc):
|
||||
for instance in sorted(
|
||||
model.mapping.get_node_instances(node),
|
||||
self.compute_model.get_node_instances(node),
|
||||
key=lambda x: self.get_instance_utilization(
|
||||
x, model)['cpu']
|
||||
x)['cpu']
|
||||
):
|
||||
for destination_node in reversed(sorted_nodes):
|
||||
if self.instance_fits(
|
||||
instance, destination_node, model, cc):
|
||||
instance, destination_node, cc):
|
||||
self.add_migration(instance, node,
|
||||
destination_node, model)
|
||||
destination_node)
|
||||
break
|
||||
if not self.is_overloaded(node, model, cc):
|
||||
if not self.is_overloaded(node, cc):
|
||||
break
|
||||
|
||||
def consolidation_phase(self, model, cc):
|
||||
def consolidation_phase(self, cc):
|
||||
"""Perform consolidation phase.
|
||||
|
||||
This considers provided resource capacity coefficients.
|
||||
@@ -459,26 +429,25 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
in the system than less cpu utilizied VMs which can be later used
|
||||
to fill smaller CPU capacity gaps.
|
||||
|
||||
:param model: model_root object
|
||||
:param cc: dictionary containing resource capacity coefficients
|
||||
"""
|
||||
sorted_nodes = sorted(
|
||||
model.get_all_compute_nodes().values(),
|
||||
key=lambda x: self.get_node_utilization(x, model)['cpu'])
|
||||
self.compute_model.get_all_compute_nodes().values(),
|
||||
key=lambda x: self.get_node_utilization(x)['cpu'])
|
||||
asc = 0
|
||||
for node in sorted_nodes:
|
||||
instances = sorted(
|
||||
model.mapping.get_node_instances(node),
|
||||
key=lambda x: self.get_instance_utilization(x, model)['cpu'])
|
||||
self.compute_model.get_node_instances(node),
|
||||
key=lambda x: self.get_instance_utilization(x)['cpu'])
|
||||
for instance in reversed(instances):
|
||||
dsc = len(sorted_nodes) - 1
|
||||
for destination_node in reversed(sorted_nodes):
|
||||
if asc >= dsc:
|
||||
break
|
||||
if self.instance_fits(
|
||||
instance, destination_node, model, cc):
|
||||
instance, destination_node, cc):
|
||||
self.add_migration(instance, node,
|
||||
destination_node, model)
|
||||
destination_node)
|
||||
break
|
||||
dsc -= 1
|
||||
asc += 1
|
||||
@@ -507,26 +476,26 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
:param original_model: root_model object
|
||||
"""
|
||||
LOG.info(_LI('Executing Smart Strategy'))
|
||||
model = self.compute_model
|
||||
rcu = self.get_relative_cluster_utilization(model)
|
||||
self.ceilometer_vm_data_cache = dict()
|
||||
rcu = self.get_relative_cluster_utilization()
|
||||
|
||||
cc = {'cpu': 1.0, 'ram': 1.0, 'disk': 1.0}
|
||||
|
||||
# Offloading phase
|
||||
self.offload_phase(model, cc)
|
||||
self.offload_phase(cc)
|
||||
|
||||
# Consolidation phase
|
||||
self.consolidation_phase(model, cc)
|
||||
self.consolidation_phase(cc)
|
||||
|
||||
# Optimize solution
|
||||
self.optimize_solution(model)
|
||||
self.optimize_solution()
|
||||
|
||||
# disable unused nodes
|
||||
self.disable_unused_nodes(model)
|
||||
self.disable_unused_nodes()
|
||||
|
||||
rcu_after = self.get_relative_cluster_utilization(model)
|
||||
rcu_after = self.get_relative_cluster_utilization()
|
||||
info = {
|
||||
"compute_nodes_count": len(
|
||||
self.compute_model.get_all_compute_nodes()),
|
||||
'number_of_migrations': self.number_of_migrations,
|
||||
'number_of_released_nodes':
|
||||
self.number_of_released_nodes,
|
||||
@@ -538,6 +507,8 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy):
|
||||
|
||||
def post_execute(self):
|
||||
self.solution.set_efficacy_indicators(
|
||||
compute_nodes_count=len(
|
||||
self.compute_model.get_all_compute_nodes()),
|
||||
released_compute_nodes_count=self.number_of_released_nodes,
|
||||
instance_migrations_count=self.number_of_migrations,
|
||||
)
|
||||
|
||||
@@ -51,7 +51,7 @@ from oslo_log import log
|
||||
|
||||
from watcher._i18n import _, _LE, _LI, _LW
|
||||
from watcher.common import exception as wexc
|
||||
from watcher.decision_engine.cluster.history import ceilometer as ceil
|
||||
from watcher.datasource import ceilometer as ceil
|
||||
from watcher.decision_engine.model import element
|
||||
from watcher.decision_engine.strategy.strategies import base
|
||||
|
||||
@@ -108,7 +108,7 @@ class WorkloadBalance(base.WorkloadStabilizationBaseStrategy):
|
||||
@property
|
||||
def ceilometer(self):
|
||||
if self._ceilometer is None:
|
||||
self._ceilometer = ceil.CeilometerClusterHistory(osc=self.osc)
|
||||
self._ceilometer = ceil.CeilometerHelper(osc=self.osc)
|
||||
return self._ceilometer
|
||||
|
||||
@ceilometer.setter
|
||||
@@ -145,18 +145,16 @@ class WorkloadBalance(base.WorkloadStabilizationBaseStrategy):
|
||||
},
|
||||
}
|
||||
|
||||
def calculate_used_resource(self, node, cap_cores, cap_mem,
|
||||
cap_disk):
|
||||
def calculate_used_resource(self, node):
|
||||
"""Calculate the used vcpus, memory and disk based on VM flavors"""
|
||||
instances = self.compute_model.mapping.get_node_instances(node)
|
||||
instances = self.compute_model.get_node_instances(node)
|
||||
vcpus_used = 0
|
||||
memory_mb_used = 0
|
||||
disk_gb_used = 0
|
||||
for instance_id in instances:
|
||||
instance = self.compute_model.get_instance_by_uuid(instance_id)
|
||||
vcpus_used += cap_cores.get_capacity(instance)
|
||||
memory_mb_used += cap_mem.get_capacity(instance)
|
||||
disk_gb_used += cap_disk.get_capacity(instance)
|
||||
for instance in instances:
|
||||
vcpus_used += instance.vcpus
|
||||
memory_mb_used += instance.memory
|
||||
disk_gb_used += instance.disk
|
||||
|
||||
return vcpus_used, memory_mb_used, disk_gb_used
|
||||
|
||||
@@ -169,27 +167,25 @@ class WorkloadBalance(base.WorkloadStabilizationBaseStrategy):
|
||||
"""
|
||||
for instance_data in hosts:
|
||||
source_node = instance_data['node']
|
||||
source_instances = self.compute_model.mapping.get_node_instances(
|
||||
source_instances = self.compute_model.get_node_instances(
|
||||
source_node)
|
||||
if source_instances:
|
||||
delta_workload = instance_data['workload'] - avg_workload
|
||||
min_delta = 1000000
|
||||
instance_id = None
|
||||
for inst_id in source_instances:
|
||||
for instance in source_instances:
|
||||
try:
|
||||
# select the first active VM to migrate
|
||||
instance = self.compute_model.get_instance_by_uuid(
|
||||
inst_id)
|
||||
if (instance.state !=
|
||||
element.InstanceState.ACTIVE.value):
|
||||
LOG.debug("Instance not active, skipped: %s",
|
||||
instance.uuid)
|
||||
continue
|
||||
current_delta = (
|
||||
delta_workload - workload_cache[inst_id])
|
||||
delta_workload - workload_cache[instance.uuid])
|
||||
if 0 <= current_delta < min_delta:
|
||||
min_delta = current_delta
|
||||
instance_id = inst_id
|
||||
instance_id = instance.uuid
|
||||
except wexc.InstanceNotFound:
|
||||
LOG.error(_LE("Instance not found; error: %s"),
|
||||
instance_id)
|
||||
@@ -203,18 +199,10 @@ class WorkloadBalance(base.WorkloadStabilizationBaseStrategy):
|
||||
|
||||
def filter_destination_hosts(self, hosts, instance_to_migrate,
|
||||
avg_workload, workload_cache):
|
||||
'''Only return hosts with sufficient available resources'''
|
||||
|
||||
cap_cores = self.compute_model.get_resource_by_uuid(
|
||||
element.ResourceType.cpu_cores)
|
||||
cap_disk = self.compute_model.get_resource_by_uuid(
|
||||
element.ResourceType.disk)
|
||||
cap_mem = self.compute_model.get_resource_by_uuid(
|
||||
element.ResourceType.memory)
|
||||
|
||||
required_cores = cap_cores.get_capacity(instance_to_migrate)
|
||||
required_disk = cap_disk.get_capacity(instance_to_migrate)
|
||||
required_mem = cap_mem.get_capacity(instance_to_migrate)
|
||||
"""Only return hosts with sufficient available resources"""
|
||||
required_cores = instance_to_migrate.vcpus
|
||||
required_disk = instance_to_migrate.disk
|
||||
required_mem = instance_to_migrate.memory
|
||||
|
||||
# filter nodes without enough resource
|
||||
destination_hosts = []
|
||||
@@ -224,16 +212,16 @@ class WorkloadBalance(base.WorkloadStabilizationBaseStrategy):
|
||||
workload = instance_data['workload']
|
||||
# calculate the available resources
|
||||
cores_used, mem_used, disk_used = self.calculate_used_resource(
|
||||
host, cap_cores, cap_mem, cap_disk)
|
||||
cores_available = cap_cores.get_capacity(host) - cores_used
|
||||
disk_available = cap_disk.get_capacity(host) - disk_used
|
||||
mem_available = cap_mem.get_capacity(host) - mem_used
|
||||
host)
|
||||
cores_available = host.vcpus - cores_used
|
||||
disk_available = host.disk - disk_used
|
||||
mem_available = host.memory - mem_used
|
||||
if (
|
||||
cores_available >= required_cores and
|
||||
disk_available >= required_disk and
|
||||
mem_available >= required_mem and
|
||||
(src_instance_workload + workload) < self.threshold / 100 *
|
||||
cap_cores.get_capacity(host)
|
||||
((src_instance_workload + workload) <
|
||||
self.threshold / 100 * host.vcpus)
|
||||
):
|
||||
destination_hosts.append(instance_data)
|
||||
|
||||
@@ -252,26 +240,20 @@ class WorkloadBalance(base.WorkloadStabilizationBaseStrategy):
|
||||
cluster_size = len(nodes)
|
||||
if not nodes:
|
||||
raise wexc.ClusterEmpty()
|
||||
# get cpu cores capacity of nodes and instances
|
||||
cap_cores = self.compute_model.get_resource_by_uuid(
|
||||
element.ResourceType.cpu_cores)
|
||||
overload_hosts = []
|
||||
nonoverload_hosts = []
|
||||
# total workload of cluster
|
||||
# it's the total core numbers being utilized in a cluster.
|
||||
cluster_workload = 0.0
|
||||
# use workload_cache to store the workload of VMs for reuse purpose
|
||||
workload_cache = {}
|
||||
for node_id in nodes:
|
||||
node = self.compute_model.get_node_by_uuid(
|
||||
node_id)
|
||||
instances = self.compute_model.mapping.get_node_instances(node)
|
||||
node = self.compute_model.get_node_by_uuid(node_id)
|
||||
instances = self.compute_model.get_node_instances(node)
|
||||
node_workload = 0.0
|
||||
for instance_id in instances:
|
||||
instance = self.compute_model.get_instance_by_uuid(instance_id)
|
||||
for instance in instances:
|
||||
try:
|
||||
cpu_util = self.ceilometer.statistic_aggregation(
|
||||
resource_id=instance_id,
|
||||
resource_id=instance.uuid,
|
||||
meter_name=self._meter,
|
||||
period=self._period,
|
||||
aggregate='avg')
|
||||
@@ -280,21 +262,19 @@ class WorkloadBalance(base.WorkloadStabilizationBaseStrategy):
|
||||
LOG.error(_LE("Can not get cpu_util from Ceilometer"))
|
||||
continue
|
||||
if cpu_util is None:
|
||||
LOG.debug("Instance (%s): cpu_util is None", instance_id)
|
||||
LOG.debug("Instance (%s): cpu_util is None", instance.uuid)
|
||||
continue
|
||||
instance_cores = cap_cores.get_capacity(instance)
|
||||
workload_cache[instance_id] = cpu_util * instance_cores / 100
|
||||
node_workload += workload_cache[instance_id]
|
||||
LOG.debug("VM (%s): cpu_util %f", instance_id, cpu_util)
|
||||
node_cores = cap_cores.get_capacity(node)
|
||||
hy_cpu_util = node_workload / node_cores * 100
|
||||
workload_cache[instance.uuid] = cpu_util * instance.vcpus / 100
|
||||
node_workload += workload_cache[instance.uuid]
|
||||
LOG.debug("VM (%s): cpu_util %f", instance.uuid, cpu_util)
|
||||
node_cpu_util = node_workload / node.vcpus * 100
|
||||
|
||||
cluster_workload += node_workload
|
||||
|
||||
instance_data = {
|
||||
'node': node, "cpu_util": hy_cpu_util,
|
||||
'node': node, "cpu_util": node_cpu_util,
|
||||
'workload': node_workload}
|
||||
if hy_cpu_util >= self.threshold:
|
||||
if node_cpu_util >= self.threshold:
|
||||
# mark the node to release resources
|
||||
overload_hosts.append(instance_data)
|
||||
else:
|
||||
|
||||
@@ -40,8 +40,7 @@ import oslo_utils
|
||||
|
||||
from watcher._i18n import _LI, _LW, _
|
||||
from watcher.common import exception
|
||||
from watcher.decision_engine.cluster.history import ceilometer as \
|
||||
ceilometer_cluster_history
|
||||
from watcher.datasource import ceilometer as ceil
|
||||
from watcher.decision_engine.model import element
|
||||
from watcher.decision_engine.strategy.strategies import base
|
||||
|
||||
@@ -157,8 +156,7 @@ class WorkloadStabilization(base.WorkloadStabilizationBaseStrategy):
|
||||
@property
|
||||
def ceilometer(self):
|
||||
if self._ceilometer is None:
|
||||
self._ceilometer = (ceilometer_cluster_history.
|
||||
CeilometerClusterHistory(osc=self.osc))
|
||||
self._ceilometer = ceil.CeilometerHelper(osc=self.osc)
|
||||
return self._ceilometer
|
||||
|
||||
@property
|
||||
@@ -187,20 +185,17 @@ class WorkloadStabilization(base.WorkloadStabilizationBaseStrategy):
|
||||
(instance_load['vcpus'] / float(host_vcpus)))
|
||||
|
||||
@MEMOIZE
|
||||
def get_instance_load(self, instance_uuid):
|
||||
def get_instance_load(self, instance):
|
||||
"""Gathering instance load through ceilometer statistic.
|
||||
|
||||
:param instance_uuid: instance for which statistic is gathered.
|
||||
:param instance: instance for which statistic is gathered.
|
||||
:return: dict
|
||||
"""
|
||||
LOG.debug('get_instance_load started')
|
||||
instance_vcpus = self.compute_model.get_resource_by_uuid(
|
||||
element.ResourceType.cpu_cores).get_capacity(
|
||||
self.compute_model.get_instance_by_uuid(instance_uuid))
|
||||
instance_load = {'uuid': instance_uuid, 'vcpus': instance_vcpus}
|
||||
instance_load = {'uuid': instance.uuid, 'vcpus': instance.vcpus}
|
||||
for meter in self.metrics:
|
||||
avg_meter = self.ceilometer.statistic_aggregation(
|
||||
resource_id=instance_uuid,
|
||||
resource_id=instance.uuid,
|
||||
meter_name=meter,
|
||||
period=self.periods['instance'],
|
||||
aggregate='min'
|
||||
@@ -209,8 +204,8 @@ class WorkloadStabilization(base.WorkloadStabilizationBaseStrategy):
|
||||
LOG.warning(
|
||||
_LW("No values returned by %(resource_id)s "
|
||||
"for %(metric_name)s") % dict(
|
||||
resource_id=instance_uuid,
|
||||
metric_name=meter))
|
||||
resource_id=instance.uuid,
|
||||
metric_name=meter))
|
||||
avg_meter = 0
|
||||
if meter == 'cpu_util':
|
||||
avg_meter /= float(100)
|
||||
@@ -221,10 +216,8 @@ class WorkloadStabilization(base.WorkloadStabilizationBaseStrategy):
|
||||
normalized_hosts = copy.deepcopy(hosts)
|
||||
for host in normalized_hosts:
|
||||
if 'memory.resident' in normalized_hosts[host]:
|
||||
h_memory = self.compute_model.get_resource_by_uuid(
|
||||
element.ResourceType.memory).get_capacity(
|
||||
self.compute_model.get_node_by_uuid(host))
|
||||
normalized_hosts[host]['memory.resident'] /= float(h_memory)
|
||||
node = self.compute_model.get_node_by_uuid(host)
|
||||
normalized_hosts[host]['memory.resident'] /= float(node.memory)
|
||||
|
||||
return normalized_hosts
|
||||
|
||||
@@ -239,13 +232,9 @@ class WorkloadStabilization(base.WorkloadStabilizationBaseStrategy):
|
||||
hosts_load = {}
|
||||
for node_id, node in self.get_available_nodes().items():
|
||||
hosts_load[node_id] = {}
|
||||
host_vcpus = self.compute_model.get_resource_by_uuid(
|
||||
element.ResourceType.cpu_cores).get_capacity(
|
||||
self.compute_model.get_node_by_uuid(node_id))
|
||||
hosts_load[node_id]['vcpus'] = host_vcpus
|
||||
hosts_load[node_id]['vcpus'] = node.vcpus
|
||||
|
||||
for metric in self.metrics:
|
||||
|
||||
resource_id = ''
|
||||
meter_name = self.instance_metrics[metric]
|
||||
if re.match('^compute.node', meter_name) is not None:
|
||||
@@ -296,34 +285,31 @@ class WorkloadStabilization(base.WorkloadStabilizationBaseStrategy):
|
||||
" for %s in weight dict.") % metric)
|
||||
return weighted_sd
|
||||
|
||||
def calculate_migration_case(self, hosts, instance_id,
|
||||
src_node_id, dst_node_id):
|
||||
def calculate_migration_case(self, hosts, instance, src_node, dst_node):
|
||||
"""Calculate migration case
|
||||
|
||||
Return list of standard deviation values, that appearing in case of
|
||||
migration of instance from source host to destination host
|
||||
:param hosts: hosts with their workload
|
||||
:param instance_id: the virtual machine
|
||||
:param src_node_id: the source node id
|
||||
:param dst_node_id: the destination node id
|
||||
:param instance: the virtual machine
|
||||
:param src_node: the source node
|
||||
:param dst_node: the destination node
|
||||
:return: list of standard deviation values
|
||||
"""
|
||||
migration_case = []
|
||||
new_hosts = copy.deepcopy(hosts)
|
||||
instance_load = self.get_instance_load(instance_id)
|
||||
d_host_vcpus = new_hosts[dst_node_id]['vcpus']
|
||||
s_host_vcpus = new_hosts[src_node_id]['vcpus']
|
||||
instance_load = self.get_instance_load(instance)
|
||||
s_host_vcpus = new_hosts[src_node.uuid]['vcpus']
|
||||
d_host_vcpus = new_hosts[dst_node.uuid]['vcpus']
|
||||
for metric in self.metrics:
|
||||
if metric is 'cpu_util':
|
||||
new_hosts[src_node_id][metric] -= self.transform_instance_cpu(
|
||||
instance_load,
|
||||
s_host_vcpus)
|
||||
new_hosts[dst_node_id][metric] += self.transform_instance_cpu(
|
||||
instance_load,
|
||||
d_host_vcpus)
|
||||
new_hosts[src_node.uuid][metric] -= (
|
||||
self.transform_instance_cpu(instance_load, s_host_vcpus))
|
||||
new_hosts[dst_node.uuid][metric] += (
|
||||
self.transform_instance_cpu(instance_load, d_host_vcpus))
|
||||
else:
|
||||
new_hosts[src_node_id][metric] -= instance_load[metric]
|
||||
new_hosts[dst_node_id][metric] += instance_load[metric]
|
||||
new_hosts[src_node.uuid][metric] -= instance_load[metric]
|
||||
new_hosts[dst_node.uuid][metric] += instance_load[metric]
|
||||
normalized_hosts = self.normalize_hosts_load(new_hosts)
|
||||
for metric in self.metrics:
|
||||
migration_case.append(self.get_sd(normalized_hosts, metric))
|
||||
@@ -345,29 +331,27 @@ class WorkloadStabilization(base.WorkloadStabilizationBaseStrategy):
|
||||
|
||||
instance_host_map = []
|
||||
nodes = list(self.get_available_nodes())
|
||||
for source_hp_id in nodes:
|
||||
for src_host in nodes:
|
||||
src_node = self.compute_model.get_node_by_uuid(src_host)
|
||||
c_nodes = copy.copy(nodes)
|
||||
c_nodes.remove(source_hp_id)
|
||||
c_nodes.remove(src_host)
|
||||
node_list = yield_nodes(c_nodes)
|
||||
instances_id = self.compute_model.get_mapping(). \
|
||||
get_node_instances_by_uuid(source_hp_id)
|
||||
for instance_id in instances_id:
|
||||
for instance in self.compute_model.get_node_instances(src_node):
|
||||
min_sd_case = {'value': len(self.metrics)}
|
||||
instance = self.compute_model.get_instance_by_uuid(instance_id)
|
||||
if instance.state not in [element.InstanceState.ACTIVE.value,
|
||||
element.InstanceState.PAUSED.value]:
|
||||
continue
|
||||
for dst_node_id in next(node_list):
|
||||
sd_case = self.calculate_migration_case(hosts, instance_id,
|
||||
source_hp_id,
|
||||
dst_node_id)
|
||||
for dst_host in next(node_list):
|
||||
dst_node = self.compute_model.get_node_by_uuid(dst_host)
|
||||
sd_case = self.calculate_migration_case(
|
||||
hosts, instance, src_node, dst_node)
|
||||
|
||||
weighted_sd = self.calculate_weighted_sd(sd_case[:-1])
|
||||
|
||||
if weighted_sd < min_sd_case['value']:
|
||||
min_sd_case = {
|
||||
'host': dst_node_id, 'value': weighted_sd,
|
||||
's_host': source_hp_id, 'instance': instance_id}
|
||||
'host': dst_node.uuid, 'value': weighted_sd,
|
||||
's_host': src_node.uuid, 'instance': instance.uuid}
|
||||
instance_host_map.append(min_sd_case)
|
||||
return sorted(instance_host_map, key=lambda x: x['value'])
|
||||
|
||||
@@ -438,19 +422,16 @@ class WorkloadStabilization(base.WorkloadStabilizationBaseStrategy):
|
||||
min_sd = 1
|
||||
balanced = False
|
||||
for instance_host in migration:
|
||||
dst_hp_disk = self.compute_model.get_resource_by_uuid(
|
||||
element.ResourceType.disk).get_capacity(
|
||||
self.compute_model.get_node_by_uuid(
|
||||
instance_host['host']))
|
||||
instance_disk = self.compute_model.get_resource_by_uuid(
|
||||
element.ResourceType.disk).get_capacity(
|
||||
self.compute_model.get_instance_by_uuid(
|
||||
instance_host['instance']))
|
||||
if instance_disk > dst_hp_disk:
|
||||
instance = self.compute_model.get_instance_by_uuid(
|
||||
instance_host['instance'])
|
||||
src_node = self.compute_model.get_node_by_uuid(
|
||||
instance_host['s_host'])
|
||||
dst_node = self.compute_model.get_node_by_uuid(
|
||||
instance_host['host'])
|
||||
if instance.disk > dst_node.disk:
|
||||
continue
|
||||
instance_load = self.calculate_migration_case(
|
||||
hosts_load, instance_host['instance'],
|
||||
instance_host['s_host'], instance_host['host'])
|
||||
hosts_load, instance, src_node, dst_node)
|
||||
weighted_sd = self.calculate_weighted_sd(instance_load[:-1])
|
||||
if weighted_sd < min_sd:
|
||||
min_sd = weighted_sd
|
||||
|
||||
@@ -37,7 +37,8 @@ class Action(base.WatcherPersistentObject, base.WatcherObject,
|
||||
|
||||
# Version 1.0: Initial version
|
||||
# Version 1.1: Added 'action_plan' object field
|
||||
VERSION = '1.1'
|
||||
# Version 2.0: Removed 'next' object field, Added 'parents' object field
|
||||
VERSION = '2.0'
|
||||
|
||||
dbapi = db_api.get_instance()
|
||||
|
||||
@@ -48,7 +49,7 @@ class Action(base.WatcherPersistentObject, base.WatcherObject,
|
||||
'action_type': wfields.StringField(nullable=True),
|
||||
'input_parameters': wfields.DictField(nullable=True),
|
||||
'state': wfields.StringField(nullable=True),
|
||||
'next': wfields.IntegerField(nullable=True),
|
||||
'parents': wfields.ListOfStringsField(nullable=True),
|
||||
|
||||
'action_plan': wfields.ObjectField('ActionPlan', nullable=True),
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ class State(object):
|
||||
SUCCEEDED = 'SUCCEEDED'
|
||||
DELETED = 'DELETED'
|
||||
CANCELLED = 'CANCELLED'
|
||||
SUPERSEDED = 'SUPERSEDED'
|
||||
|
||||
|
||||
@base.WatcherObjectRegistry.register
|
||||
@@ -94,7 +95,8 @@ class ActionPlan(base.WatcherPersistentObject, base.WatcherObject,
|
||||
# Version 1.0: Initial version
|
||||
# Version 1.1: Added 'audit' and 'strategy' object field
|
||||
# Version 1.2: audit_id is not nullable anymore
|
||||
VERSION = '1.2'
|
||||
# Version 2.0: Removed 'first_action_id' object field
|
||||
VERSION = '2.0'
|
||||
|
||||
dbapi = db_api.get_instance()
|
||||
|
||||
@@ -103,7 +105,6 @@ class ActionPlan(base.WatcherPersistentObject, base.WatcherObject,
|
||||
'uuid': wfields.UUIDField(),
|
||||
'audit_id': wfields.IntegerField(),
|
||||
'strategy_id': wfields.IntegerField(),
|
||||
'first_action_id': wfields.IntegerField(nullable=True),
|
||||
'state': wfields.StringField(nullable=True),
|
||||
'global_efficacy': wfields.FlexibleDictField(nullable=True),
|
||||
|
||||
|
||||
@@ -79,7 +79,8 @@ class Audit(base.WatcherPersistentObject, base.WatcherObject,
|
||||
|
||||
# Version 1.0: Initial version
|
||||
# Version 1.1: Added 'goal' and 'strategy' object field
|
||||
VERSION = '1.1'
|
||||
# Version 1.2 Added 'auto_trigger' boolean field
|
||||
VERSION = '1.2'
|
||||
|
||||
dbapi = db_api.get_instance()
|
||||
|
||||
@@ -93,6 +94,7 @@ class Audit(base.WatcherPersistentObject, base.WatcherObject,
|
||||
'scope': wfields.FlexibleListOfDictField(nullable=True),
|
||||
'goal_id': wfields.IntegerField(),
|
||||
'strategy_id': wfields.IntegerField(nullable=True),
|
||||
'auto_trigger': wfields.BooleanField(),
|
||||
|
||||
'goal': wfields.ObjectField('Goal', nullable=True),
|
||||
'strategy': wfields.ObjectField('Strategy', nullable=True),
|
||||
|
||||
@@ -104,7 +104,7 @@ class WatcherPersistentObject(object):
|
||||
}
|
||||
|
||||
# Mapping between the object field name and a 2-tuple pair composed of
|
||||
# its object type (e.g. objects.RelatedObject) and the the name of the
|
||||
# its object type (e.g. objects.RelatedObject) and the name of the
|
||||
# model field related ID (or UUID) foreign key field.
|
||||
# e.g.:
|
||||
#
|
||||
|
||||
@@ -17,21 +17,21 @@
|
||||
import ast
|
||||
import six
|
||||
|
||||
from oslo_log import log
|
||||
from oslo_versionedobjects import fields
|
||||
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
BaseEnumField = fields.BaseEnumField
|
||||
BooleanField = fields.BooleanField
|
||||
DateTimeField = fields.DateTimeField
|
||||
Enum = fields.Enum
|
||||
FloatField = fields.FloatField
|
||||
IntegerField = fields.IntegerField
|
||||
ListOfStringsField = fields.ListOfStringsField
|
||||
NonNegativeFloatField = fields.NonNegativeFloatField
|
||||
NonNegativeIntegerField = fields.NonNegativeIntegerField
|
||||
ObjectField = fields.ObjectField
|
||||
StringField = fields.StringField
|
||||
UnspecifiedDefault = fields.UnspecifiedDefault
|
||||
UUIDField = fields.UUIDField
|
||||
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ def post_get_test_action(**kw):
|
||||
del action['action_plan_id']
|
||||
action['action_plan_uuid'] = kw.get('action_plan_uuid',
|
||||
action_plan['uuid'])
|
||||
action['next'] = None
|
||||
action['parents'] = None
|
||||
return action
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ class TestActionObject(base.TestCase):
|
||||
|
||||
def test_action_init(self):
|
||||
action_dict = api_utils.action_post_data(action_plan_id=None,
|
||||
next=None)
|
||||
parents=None)
|
||||
del action_dict['state']
|
||||
action = api_action.Action(**action_dict)
|
||||
self.assertEqual(wtypes.Unset, action.state)
|
||||
@@ -67,13 +67,13 @@ class TestListAction(api_base.FunctionalTest):
|
||||
self.assertIn(field, action)
|
||||
|
||||
def test_one(self):
|
||||
action = obj_utils.create_test_action(self.context, next=None)
|
||||
action = obj_utils.create_test_action(self.context, parents=None)
|
||||
response = self.get_json('/actions')
|
||||
self.assertEqual(action.uuid, response['actions'][0]["uuid"])
|
||||
self._assert_action_fields(response['actions'][0])
|
||||
|
||||
def test_one_soft_deleted(self):
|
||||
action = obj_utils.create_test_action(self.context, next=None)
|
||||
action = obj_utils.create_test_action(self.context, parents=None)
|
||||
action.soft_delete()
|
||||
response = self.get_json('/actions',
|
||||
headers={'X-Show-Deleted': 'True'})
|
||||
@@ -84,7 +84,7 @@ class TestListAction(api_base.FunctionalTest):
|
||||
self.assertEqual([], response['actions'])
|
||||
|
||||
def test_get_one(self):
|
||||
action = obj_utils.create_test_action(self.context, next=None)
|
||||
action = obj_utils.create_test_action(self.context, parents=None)
|
||||
response = self.get_json('/actions/%s' % action['uuid'])
|
||||
self.assertEqual(action.uuid, response['uuid'])
|
||||
self.assertEqual(action.action_type, response['action_type'])
|
||||
@@ -92,7 +92,7 @@ class TestListAction(api_base.FunctionalTest):
|
||||
self._assert_action_fields(response)
|
||||
|
||||
def test_get_one_soft_deleted(self):
|
||||
action = obj_utils.create_test_action(self.context, next=None)
|
||||
action = obj_utils.create_test_action(self.context, parents=None)
|
||||
action.soft_delete()
|
||||
response = self.get_json('/actions/%s' % action['uuid'],
|
||||
headers={'X-Show-Deleted': 'True'})
|
||||
@@ -104,13 +104,13 @@ class TestListAction(api_base.FunctionalTest):
|
||||
self.assertEqual(404, response.status_int)
|
||||
|
||||
def test_detail(self):
|
||||
action = obj_utils.create_test_action(self.context, next=None)
|
||||
action = obj_utils.create_test_action(self.context, parents=None)
|
||||
response = self.get_json('/actions/detail')
|
||||
self.assertEqual(action.uuid, response['actions'][0]["uuid"])
|
||||
self._assert_action_fields(response['actions'][0])
|
||||
|
||||
def test_detail_soft_deleted(self):
|
||||
action = obj_utils.create_test_action(self.context, next=None)
|
||||
action = obj_utils.create_test_action(self.context, parents=None)
|
||||
action.soft_delete()
|
||||
response = self.get_json('/actions/detail',
|
||||
headers={'X-Show-Deleted': 'True'})
|
||||
@@ -121,7 +121,7 @@ class TestListAction(api_base.FunctionalTest):
|
||||
self.assertEqual([], response['actions'])
|
||||
|
||||
def test_detail_against_single(self):
|
||||
action = obj_utils.create_test_action(self.context, next=None)
|
||||
action = obj_utils.create_test_action(self.context, parents=None)
|
||||
response = self.get_json('/actions/%s/detail' % action['uuid'],
|
||||
expect_errors=True)
|
||||
self.assertEqual(404, response.status_int)
|
||||
@@ -312,18 +312,23 @@ class TestListAction(api_base.FunctionalTest):
|
||||
set([act['uuid'] for act in response['actions']
|
||||
if act['action_plan_uuid'] == action_plan2.uuid]))
|
||||
|
||||
def test_many_with_next_uuid(self):
|
||||
def test_many_with_parents(self):
|
||||
action_list = []
|
||||
for id_ in range(5):
|
||||
action = obj_utils.create_test_action(self.context, id=id_,
|
||||
uuid=utils.generate_uuid(),
|
||||
next=id_ + 1)
|
||||
if id_ > 0:
|
||||
action = obj_utils.create_test_action(
|
||||
self.context, id=id_, uuid=utils.generate_uuid(),
|
||||
parents=[action_list[id_ - 1]])
|
||||
else:
|
||||
action = obj_utils.create_test_action(
|
||||
self.context, id=id_, uuid=utils.generate_uuid(),
|
||||
parents=[])
|
||||
action_list.append(action.uuid)
|
||||
response = self.get_json('/actions')
|
||||
response_actions = response['actions']
|
||||
for id_ in range(4):
|
||||
self.assertEqual(response_actions[id_]['next_uuid'],
|
||||
response_actions[id_ + 1]['uuid'])
|
||||
self.assertEqual(response_actions[id_]['uuid'],
|
||||
response_actions[id_ + 1]['parents'][0])
|
||||
|
||||
def test_many_without_soft_deleted(self):
|
||||
action_list = []
|
||||
@@ -357,30 +362,6 @@ class TestListAction(api_base.FunctionalTest):
|
||||
uuids = [s['uuid'] for s in response['actions']]
|
||||
self.assertEqual(sorted(action_list), sorted(uuids))
|
||||
|
||||
def test_many_with_sort_key_next_uuid(self):
|
||||
for id_ in range(5):
|
||||
obj_utils.create_test_action(self.context, id=id_,
|
||||
uuid=utils.generate_uuid(),
|
||||
next=id_ + 1)
|
||||
response = self.get_json('/actions/')
|
||||
reference_uuids = [
|
||||
s.get('next_uuid', '') for s in response['actions']
|
||||
]
|
||||
|
||||
response = self.get_json('/actions/?sort_key=next_uuid')
|
||||
|
||||
self.assertEqual(5, len(response['actions']))
|
||||
uuids = [(s['next_uuid'] if 'next_uuid' in s else '')
|
||||
for s in response['actions']]
|
||||
self.assertEqual(sorted(reference_uuids), uuids)
|
||||
|
||||
response = self.get_json('/actions/?sort_key=next_uuid&sort_dir=desc')
|
||||
|
||||
self.assertEqual(5, len(response['actions']))
|
||||
uuids = [(s['next_uuid'] if 'next_uuid' in s else '')
|
||||
for s in response['actions']]
|
||||
self.assertEqual(sorted(reference_uuids, reverse=True), uuids)
|
||||
|
||||
def test_links(self):
|
||||
uuid = utils.generate_uuid()
|
||||
obj_utils.create_test_action(self.context, id=1, uuid=uuid)
|
||||
@@ -393,18 +374,15 @@ class TestListAction(api_base.FunctionalTest):
|
||||
self.assertTrue(self.validate_link(l['href'], bookmark=bookmark))
|
||||
|
||||
def test_collection_links(self):
|
||||
next = -1
|
||||
parents = None
|
||||
for id_ in range(5):
|
||||
action = obj_utils.create_test_action(self.context, id=id_,
|
||||
uuid=utils.generate_uuid(),
|
||||
next=next)
|
||||
next = action.id
|
||||
parents=parents)
|
||||
parents = [action.id]
|
||||
response = self.get_json('/actions/?limit=3')
|
||||
self.assertEqual(3, len(response['actions']))
|
||||
|
||||
next_marker = response['actions'][-1]['uuid']
|
||||
self.assertIn(next_marker, response['next'])
|
||||
|
||||
def test_collection_links_default_limit(self):
|
||||
cfg.CONF.set_override('max_limit', 3, 'api',
|
||||
enforce_type=True)
|
||||
@@ -414,9 +392,6 @@ class TestListAction(api_base.FunctionalTest):
|
||||
response = self.get_json('/actions')
|
||||
self.assertEqual(3, len(response['actions']))
|
||||
|
||||
next_marker = response['actions'][-1]['uuid']
|
||||
self.assertIn(next_marker, response['next'])
|
||||
|
||||
|
||||
class TestPatch(api_base.FunctionalTest):
|
||||
|
||||
@@ -426,7 +401,7 @@ class TestPatch(api_base.FunctionalTest):
|
||||
obj_utils.create_test_strategy(self.context)
|
||||
obj_utils.create_test_audit(self.context)
|
||||
obj_utils.create_test_action_plan(self.context)
|
||||
self.action = obj_utils.create_test_action(self.context, next=None)
|
||||
self.action = obj_utils.create_test_action(self.context, parents=None)
|
||||
p = mock.patch.object(db_api.BaseConnection, 'update_action')
|
||||
self.mock_action_update = p.start()
|
||||
self.mock_action_update.side_effect = self._simulate_rpc_action_update
|
||||
@@ -461,7 +436,7 @@ class TestDelete(api_base.FunctionalTest):
|
||||
self.strategy = obj_utils.create_test_strategy(self.context)
|
||||
self.audit = obj_utils.create_test_audit(self.context)
|
||||
self.action_plan = obj_utils.create_test_action_plan(self.context)
|
||||
self.action = obj_utils.create_test_action(self.context, next=None)
|
||||
self.action = obj_utils.create_test_action(self.context, parents=None)
|
||||
p = mock.patch.object(db_api.BaseConnection, 'update_action')
|
||||
self.mock_action_update = p.start()
|
||||
self.mock_action_update.side_effect = self._simulate_rpc_action_update
|
||||
|
||||
@@ -77,14 +77,6 @@ class TestListActionPlan(api_base.FunctionalTest):
|
||||
'unit': '%'}],
|
||||
response['efficacy_indicators'])
|
||||
|
||||
def test_get_one_with_first_action(self):
|
||||
action_plan = obj_utils.create_test_action_plan(self.context)
|
||||
action = obj_utils.create_test_action(self.context, id=1)
|
||||
response = self.get_json('/action_plans/%s' % action_plan['uuid'])
|
||||
self.assertEqual(action_plan.uuid, response['uuid'])
|
||||
self.assertEqual(action.uuid, response['first_action_uuid'])
|
||||
self._assert_action_plans_fields(response)
|
||||
|
||||
def test_get_one_soft_deleted(self):
|
||||
action_plan = obj_utils.create_test_action_plan(self.context)
|
||||
action_plan.soft_delete()
|
||||
@@ -322,7 +314,7 @@ class TestDelete(api_base.FunctionalTest):
|
||||
|
||||
def test_delete_action_plan_with_action(self):
|
||||
action = obj_utils.create_test_action(
|
||||
self.context, id=self.action_plan.first_action_id)
|
||||
self.context, id=1)
|
||||
|
||||
self.delete('/action_plans/%s' % self.action_plan.uuid)
|
||||
ap_response = self.get_json('/action_plans/%s' % self.action_plan.uuid,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
# Copyright 2013 Red Hat, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
#
|
||||
# Authors: Jean-Emile DARTOIS <jean-emile.dartois@b-com.com>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2016 b<>com
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2016 b<>com
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2016 b<>com
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
||||
102
watcher/tests/applier/actions/test_resize.py
Normal file
102
watcher/tests/applier/actions/test_resize.py
Normal file
@@ -0,0 +1,102 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# 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 __future__ import unicode_literals
|
||||
|
||||
import mock
|
||||
import voluptuous
|
||||
|
||||
from watcher.applier.actions import base as baction
|
||||
from watcher.applier.actions import resize
|
||||
from watcher.common import clients
|
||||
from watcher.common import nova_helper
|
||||
from watcher.tests import base
|
||||
|
||||
|
||||
class TestResize(base.TestCase):
|
||||
|
||||
INSTANCE_UUID = "94ae2f92-b7fd-4da7-9e97-f13504ae98c4"
|
||||
|
||||
def setUp(self):
|
||||
super(TestResize, self).setUp()
|
||||
|
||||
self.r_osc_cls = mock.Mock()
|
||||
self.r_helper_cls = mock.Mock()
|
||||
self.r_helper = mock.Mock(spec=nova_helper.NovaHelper)
|
||||
self.r_helper_cls.return_value = self.r_helper
|
||||
self.r_osc = mock.Mock(spec=clients.OpenStackClients)
|
||||
self.r_osc_cls.return_value = self.r_osc
|
||||
|
||||
r_openstack_clients = mock.patch.object(
|
||||
clients, "OpenStackClients", self.r_osc_cls)
|
||||
r_nova_helper = mock.patch.object(
|
||||
nova_helper, "NovaHelper", self.r_helper_cls)
|
||||
|
||||
r_openstack_clients.start()
|
||||
r_nova_helper.start()
|
||||
|
||||
self.addCleanup(r_openstack_clients.stop)
|
||||
self.addCleanup(r_nova_helper.stop)
|
||||
|
||||
self.input_parameters = {
|
||||
"flavor": "x1",
|
||||
baction.BaseAction.RESOURCE_ID: self.INSTANCE_UUID,
|
||||
}
|
||||
self.action = resize.Resize(mock.Mock())
|
||||
self.action.input_parameters = self.input_parameters
|
||||
|
||||
def test_parameters(self):
|
||||
params = {baction.BaseAction.RESOURCE_ID:
|
||||
self.INSTANCE_UUID,
|
||||
self.action.FLAVOR: 'x1'}
|
||||
self.action.input_parameters = params
|
||||
self.assertTrue(self.action.validate_parameters())
|
||||
|
||||
def test_parameters_exception_empty_fields(self):
|
||||
parameters = {baction.BaseAction.RESOURCE_ID:
|
||||
self.INSTANCE_UUID,
|
||||
self.action.FLAVOR: None}
|
||||
self.action.input_parameters = parameters
|
||||
exc = self.assertRaises(
|
||||
voluptuous.MultipleInvalid, self.action.validate_parameters)
|
||||
self.assertEqual([(['flavor'], voluptuous.TypeInvalid)],
|
||||
[(e.path, type(e)) for e in exc.errors])
|
||||
|
||||
def test_parameters_exception_flavor(self):
|
||||
parameters = {baction.BaseAction.RESOURCE_ID:
|
||||
self.INSTANCE_UUID,
|
||||
self.action.FLAVOR: None}
|
||||
self.action.input_parameters = parameters
|
||||
exc = self.assertRaises(
|
||||
voluptuous.MultipleInvalid, self.action.validate_parameters)
|
||||
self.assertEqual(
|
||||
[(['flavor'], voluptuous.TypeInvalid)],
|
||||
[(e.path, type(e)) for e in exc.errors])
|
||||
|
||||
def test_parameters_exception_resource_id(self):
|
||||
parameters = {baction.BaseAction.RESOURCE_ID: "EFEF",
|
||||
self.action.FLAVOR: 'x1'}
|
||||
self.action.input_parameters = parameters
|
||||
exc = self.assertRaises(
|
||||
voluptuous.MultipleInvalid, self.action.validate_parameters)
|
||||
self.assertEqual(
|
||||
[(['resource_id'], voluptuous.Invalid)],
|
||||
[(e.path, type(e)) for e in exc.errors])
|
||||
|
||||
def test_execute_resize(self):
|
||||
self.r_helper.find_instance.return_value = self.INSTANCE_UUID
|
||||
self.action.execute()
|
||||
self.r_helper.resize_instance.assert_called_once_with(
|
||||
instance_id=self.INSTANCE_UUID, flavor='x1')
|
||||
@@ -1,5 +1,3 @@
|
||||
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2016 b<>com
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2015 b<>com
|
||||
#
|
||||
# Authors: Jean-Emile DARTOIS <jean-emile.dartois@b-com.com>
|
||||
|
||||
@@ -59,6 +59,7 @@ class TestDefaultWorkFlowEngine(base.DbTestCase):
|
||||
config=mock.Mock(),
|
||||
context=self.context,
|
||||
applier_manager=mock.MagicMock())
|
||||
self.engine.config.max_workers = 2
|
||||
|
||||
@mock.patch('taskflow.engines.load')
|
||||
@mock.patch('taskflow.patterns.graph_flow.Flow.link')
|
||||
@@ -70,14 +71,15 @@ class TestDefaultWorkFlowEngine(base.DbTestCase):
|
||||
except Exception as exc:
|
||||
self.fail(exc)
|
||||
|
||||
def create_action(self, action_type, parameters, next):
|
||||
def create_action(self, action_type, parameters, parents, uuid=None):
|
||||
action = {
|
||||
'uuid': utils.generate_uuid(),
|
||||
'uuid': uuid or utils.generate_uuid(),
|
||||
'action_plan_id': 0,
|
||||
'action_type': action_type,
|
||||
'input_parameters': parameters,
|
||||
'state': objects.action.State.PENDING,
|
||||
'next': next,
|
||||
'parents': parents,
|
||||
|
||||
}
|
||||
new_action = objects.Action(self.context, **action)
|
||||
new_action.create()
|
||||
@@ -113,10 +115,89 @@ class TestDefaultWorkFlowEngine(base.DbTestCase):
|
||||
except Exception as exc:
|
||||
self.fail(exc)
|
||||
|
||||
def test_execute_nop_sleep(self):
|
||||
actions = []
|
||||
first_nop = self.create_action("nop", {'message': 'test'}, [])
|
||||
second_nop = self.create_action("nop", {'message': 'second test'}, [])
|
||||
sleep = self.create_action("sleep", {'duration': 0.0},
|
||||
[first_nop.uuid, second_nop.uuid])
|
||||
actions.extend([first_nop, second_nop, sleep])
|
||||
|
||||
try:
|
||||
self.engine.execute(actions)
|
||||
self.check_actions_state(actions, objects.action.State.SUCCEEDED)
|
||||
|
||||
except Exception as exc:
|
||||
self.fail(exc)
|
||||
|
||||
def test_execute_with_parents(self):
|
||||
actions = []
|
||||
first_nop = self.create_action(
|
||||
"nop", {'message': 'test'}, [],
|
||||
uuid='bc7eee5c-4fbe-4def-9744-b539be55aa19')
|
||||
second_nop = self.create_action(
|
||||
"nop", {'message': 'second test'}, [],
|
||||
uuid='0565bd5c-aa00-46e5-8d81-2cb5cc1ffa23')
|
||||
first_sleep = self.create_action(
|
||||
"sleep", {'duration': 0.0}, [first_nop.uuid, second_nop.uuid],
|
||||
uuid='be436531-0da3-4dad-a9c0-ea1d2aff6496')
|
||||
second_sleep = self.create_action(
|
||||
"sleep", {'duration': 0.0}, [first_sleep.uuid],
|
||||
uuid='9eb51e14-936d-4d12-a500-6ba0f5e0bb1c')
|
||||
actions.extend([first_nop, second_nop, first_sleep, second_sleep])
|
||||
|
||||
expected_nodes = [
|
||||
{'uuid': 'bc7eee5c-4fbe-4def-9744-b539be55aa19',
|
||||
'input_parameters': {u'message': u'test'},
|
||||
'action_plan_id': 0, 'state': u'PENDING', 'parents': [],
|
||||
'action_type': u'nop', 'id': 1},
|
||||
{'uuid': '0565bd5c-aa00-46e5-8d81-2cb5cc1ffa23',
|
||||
'input_parameters': {u'message': u'second test'},
|
||||
'action_plan_id': 0, 'state': u'PENDING', 'parents': [],
|
||||
'action_type': u'nop', 'id': 2},
|
||||
{'uuid': 'be436531-0da3-4dad-a9c0-ea1d2aff6496',
|
||||
'input_parameters': {u'duration': 0.0},
|
||||
'action_plan_id': 0, 'state': u'PENDING',
|
||||
'parents': [u'bc7eee5c-4fbe-4def-9744-b539be55aa19',
|
||||
u'0565bd5c-aa00-46e5-8d81-2cb5cc1ffa23'],
|
||||
'action_type': u'sleep', 'id': 3},
|
||||
{'uuid': '9eb51e14-936d-4d12-a500-6ba0f5e0bb1c',
|
||||
'input_parameters': {u'duration': 0.0},
|
||||
'action_plan_id': 0, 'state': u'PENDING',
|
||||
'parents': [u'be436531-0da3-4dad-a9c0-ea1d2aff6496'],
|
||||
'action_type': u'sleep', 'id': 4}]
|
||||
|
||||
expected_edges = [
|
||||
('action_type:nop uuid:0565bd5c-aa00-46e5-8d81-2cb5cc1ffa23',
|
||||
'action_type:sleep uuid:be436531-0da3-4dad-a9c0-ea1d2aff6496'),
|
||||
('action_type:nop uuid:bc7eee5c-4fbe-4def-9744-b539be55aa19',
|
||||
'action_type:sleep uuid:be436531-0da3-4dad-a9c0-ea1d2aff6496'),
|
||||
('action_type:sleep uuid:be436531-0da3-4dad-a9c0-ea1d2aff6496',
|
||||
'action_type:sleep uuid:9eb51e14-936d-4d12-a500-6ba0f5e0bb1c')]
|
||||
|
||||
try:
|
||||
flow = self.engine.execute(actions)
|
||||
actual_nodes = sorted([x[0]._db_action.as_dict()
|
||||
for x in flow.iter_nodes()],
|
||||
key=lambda x: x['id'])
|
||||
for expected, actual in zip(expected_nodes, actual_nodes):
|
||||
for key in expected.keys():
|
||||
self.assertIn(expected[key], actual.values())
|
||||
actual_edges = [(u.name, v.name)
|
||||
for (u, v, _) in flow.iter_links()]
|
||||
|
||||
for edge in expected_edges:
|
||||
self.assertIn(edge, actual_edges)
|
||||
|
||||
self.check_actions_state(actions, objects.action.State.SUCCEEDED)
|
||||
|
||||
except Exception as exc:
|
||||
self.fail(exc)
|
||||
|
||||
def test_execute_with_two_actions(self):
|
||||
actions = []
|
||||
second = self.create_action("sleep", {'duration': 0.0}, None)
|
||||
first = self.create_action("nop", {'message': 'test'}, second.id)
|
||||
first = self.create_action("nop", {'message': 'test'}, None)
|
||||
|
||||
actions.append(first)
|
||||
actions.append(second)
|
||||
@@ -132,8 +213,8 @@ class TestDefaultWorkFlowEngine(base.DbTestCase):
|
||||
actions = []
|
||||
|
||||
third = self.create_action("nop", {'message': 'next'}, None)
|
||||
second = self.create_action("sleep", {'duration': 0.0}, third.id)
|
||||
first = self.create_action("nop", {'message': 'hello'}, second.id)
|
||||
second = self.create_action("sleep", {'duration': 0.0}, None)
|
||||
first = self.create_action("nop", {'message': 'hello'}, None)
|
||||
|
||||
self.check_action_state(first, objects.action.State.PENDING)
|
||||
self.check_action_state(second, objects.action.State.PENDING)
|
||||
@@ -154,8 +235,8 @@ class TestDefaultWorkFlowEngine(base.DbTestCase):
|
||||
actions = []
|
||||
|
||||
third = self.create_action("no_exist", {'message': 'next'}, None)
|
||||
second = self.create_action("sleep", {'duration': 0.0}, third.id)
|
||||
first = self.create_action("nop", {'message': 'hello'}, second.id)
|
||||
second = self.create_action("sleep", {'duration': 0.0}, None)
|
||||
first = self.create_action("nop", {'message': 'hello'}, None)
|
||||
|
||||
self.check_action_state(first, objects.action.State.PENDING)
|
||||
self.check_action_state(second, objects.action.State.PENDING)
|
||||
|
||||
@@ -17,38 +17,31 @@ from cinderclient.v1 import client as ciclient_v1
|
||||
from glanceclient import client as glclient
|
||||
from keystoneauth1 import loading as ka_loading
|
||||
import mock
|
||||
from monascaclient import client as monclient
|
||||
from monascaclient.v2_0 import client as monclient_v2
|
||||
from neutronclient.neutron import client as netclient
|
||||
from neutronclient.v2_0 import client as netclient_v2
|
||||
from novaclient import client as nvclient
|
||||
from oslo_config import cfg
|
||||
|
||||
from watcher.common import clients
|
||||
from watcher import conf
|
||||
from watcher.tests import base
|
||||
|
||||
CONF = conf.CONF
|
||||
|
||||
|
||||
class TestClients(base.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestClients, self).setUp()
|
||||
|
||||
cfg.CONF.import_opt('api_version', 'watcher.common.clients',
|
||||
group='nova_client')
|
||||
cfg.CONF.import_opt('api_version', 'watcher.common.clients',
|
||||
group='glance_client')
|
||||
cfg.CONF.import_opt('api_version', 'watcher.common.clients',
|
||||
group='cinder_client')
|
||||
cfg.CONF.import_opt('api_version', 'watcher.common.clients',
|
||||
group='ceilometer_client')
|
||||
cfg.CONF.import_opt('api_version', 'watcher.common.clients',
|
||||
group='neutron_client')
|
||||
|
||||
def test_get_keystone_session(self):
|
||||
def _register_watcher_clients_auth_opts(self):
|
||||
_AUTH_CONF_GROUP = 'watcher_clients_auth'
|
||||
ka_loading.register_auth_conf_options(cfg.CONF, _AUTH_CONF_GROUP)
|
||||
ka_loading.register_session_conf_options(cfg.CONF, _AUTH_CONF_GROUP)
|
||||
ka_loading.register_auth_conf_options(CONF, _AUTH_CONF_GROUP)
|
||||
ka_loading.register_session_conf_options(CONF, _AUTH_CONF_GROUP)
|
||||
CONF.set_override('auth_type', 'password', group=_AUTH_CONF_GROUP)
|
||||
|
||||
cfg.CONF.set_override('auth_type', 'password',
|
||||
group=_AUTH_CONF_GROUP)
|
||||
# ka_loading.load_auth_from_conf_options(CONF, _AUTH_CONF_GROUP)
|
||||
# ka_loading.load_session_from_conf_options(CONF, _AUTH_CONF_GROUP)
|
||||
# CONF.set_override(
|
||||
# 'auth-url', 'http://server.ip:35357', group=_AUTH_CONF_GROUP)
|
||||
|
||||
# If we don't clean up the _AUTH_CONF_GROUP conf options, then other
|
||||
# tests that run after this one will fail, complaining about required
|
||||
@@ -56,27 +49,29 @@ class TestClients(base.TestCase):
|
||||
def cleanup_conf_from_loading():
|
||||
# oslo_config doesn't seem to allow unregistering groups through a
|
||||
# single method, so we do this instead
|
||||
cfg.CONF.reset()
|
||||
del cfg.CONF._groups[_AUTH_CONF_GROUP]
|
||||
CONF.reset()
|
||||
del CONF._groups[_AUTH_CONF_GROUP]
|
||||
|
||||
self.addCleanup(cleanup_conf_from_loading)
|
||||
|
||||
osc = clients.OpenStackClients()
|
||||
|
||||
expected = {'username': 'foousername',
|
||||
'password': 'foopassword',
|
||||
'auth_url': 'http://server.ip:35357',
|
||||
'user_domain_id': 'foouserdomainid',
|
||||
'project_domain_id': 'fooprojdomainid'}
|
||||
|
||||
def reset_register_opts_mock(conf_obj, original_method):
|
||||
conf_obj.register_opts = original_method
|
||||
|
||||
original_register_opts = cfg.CONF.register_opts
|
||||
original_register_opts = CONF.register_opts
|
||||
self.addCleanup(reset_register_opts_mock,
|
||||
cfg.CONF,
|
||||
CONF,
|
||||
original_register_opts)
|
||||
|
||||
expected = {'username': 'foousername',
|
||||
'password': 'foopassword',
|
||||
'auth_url': 'http://server.ip:35357',
|
||||
'cafile': None,
|
||||
'certfile': None,
|
||||
'keyfile': None,
|
||||
'insecure': False,
|
||||
'user_domain_id': 'foouserdomainid',
|
||||
'project_domain_id': 'fooprojdomainid'}
|
||||
|
||||
# Because some of the conf options for auth plugins are not registered
|
||||
# until right before they are loaded, and because the method that does
|
||||
# the actual loading of the conf option values is an anonymous method
|
||||
@@ -88,10 +83,21 @@ class TestClients(base.TestCase):
|
||||
ret = original_register_opts(*args, **kwargs)
|
||||
if 'group' in kwargs and kwargs['group'] == _AUTH_CONF_GROUP:
|
||||
for key, value in expected.items():
|
||||
cfg.CONF.set_override(key, value, group=_AUTH_CONF_GROUP)
|
||||
CONF.set_override(key, value, group=_AUTH_CONF_GROUP)
|
||||
return ret
|
||||
|
||||
cfg.CONF.register_opts = mock_register_opts
|
||||
CONF.register_opts = mock_register_opts
|
||||
|
||||
def test_get_keystone_session(self):
|
||||
self._register_watcher_clients_auth_opts()
|
||||
|
||||
osc = clients.OpenStackClients()
|
||||
|
||||
expected = {'username': 'foousername',
|
||||
'password': 'foopassword',
|
||||
'auth_url': 'http://server.ip:35357',
|
||||
'user_domain_id': 'foouserdomainid',
|
||||
'project_domain_id': 'fooprojdomainid'}
|
||||
|
||||
sess = osc.session
|
||||
self.assertEqual(expected['auth_url'], sess.auth.auth_url)
|
||||
@@ -107,13 +113,12 @@ class TestClients(base.TestCase):
|
||||
osc = clients.OpenStackClients()
|
||||
osc._nova = None
|
||||
osc.nova()
|
||||
mock_call.assert_called_once_with(cfg.CONF.nova_client.api_version,
|
||||
mock_call.assert_called_once_with(CONF.nova_client.api_version,
|
||||
session=mock_session)
|
||||
|
||||
@mock.patch.object(clients.OpenStackClients, 'session')
|
||||
def test_clients_nova_diff_vers(self, mock_session):
|
||||
cfg.CONF.set_override('api_version', '2.3',
|
||||
group='nova_client')
|
||||
CONF.set_override('api_version', '2.3', group='nova_client')
|
||||
osc = clients.OpenStackClients()
|
||||
osc._nova = None
|
||||
osc.nova()
|
||||
@@ -133,13 +138,12 @@ class TestClients(base.TestCase):
|
||||
osc = clients.OpenStackClients()
|
||||
osc._glance = None
|
||||
osc.glance()
|
||||
mock_call.assert_called_once_with(cfg.CONF.glance_client.api_version,
|
||||
mock_call.assert_called_once_with(CONF.glance_client.api_version,
|
||||
session=mock_session)
|
||||
|
||||
@mock.patch.object(clients.OpenStackClients, 'session')
|
||||
def test_clients_glance_diff_vers(self, mock_session):
|
||||
cfg.CONF.set_override('api_version', '1',
|
||||
group='glance_client')
|
||||
CONF.set_override('api_version', '1', group='glance_client')
|
||||
osc = clients.OpenStackClients()
|
||||
osc._glance = None
|
||||
osc.glance()
|
||||
@@ -159,13 +163,12 @@ class TestClients(base.TestCase):
|
||||
osc = clients.OpenStackClients()
|
||||
osc._cinder = None
|
||||
osc.cinder()
|
||||
mock_call.assert_called_once_with(cfg.CONF.cinder_client.api_version,
|
||||
mock_call.assert_called_once_with(CONF.cinder_client.api_version,
|
||||
session=mock_session)
|
||||
|
||||
@mock.patch.object(clients.OpenStackClients, 'session')
|
||||
def test_clients_cinder_diff_vers(self, mock_session):
|
||||
cfg.CONF.set_override('api_version', '1',
|
||||
group='cinder_client')
|
||||
CONF.set_override('api_version', '1', group='cinder_client')
|
||||
osc = clients.OpenStackClients()
|
||||
osc._cinder = None
|
||||
osc.cinder()
|
||||
@@ -186,18 +189,18 @@ class TestClients(base.TestCase):
|
||||
osc._ceilometer = None
|
||||
osc.ceilometer()
|
||||
mock_call.assert_called_once_with(
|
||||
cfg.CONF.ceilometer_client.api_version,
|
||||
CONF.ceilometer_client.api_version,
|
||||
None,
|
||||
session=mock_session)
|
||||
|
||||
@mock.patch.object(clients.OpenStackClients, 'session')
|
||||
@mock.patch.object(ceclient_v2.Client, '_get_alarm_client')
|
||||
def test_clients_ceilometer_diff_vers(self, mock_get_alarm_client,
|
||||
@mock.patch.object(ceclient_v2.Client, '_get_redirect_client')
|
||||
def test_clients_ceilometer_diff_vers(self, mock_get_redirect_client,
|
||||
mock_session):
|
||||
'''ceilometerclient currently only has one version (v2)'''
|
||||
mock_get_alarm_client.return_value = [mock.Mock(), mock.Mock()]
|
||||
cfg.CONF.set_override('api_version', '2',
|
||||
group='ceilometer_client')
|
||||
mock_get_redirect_client.return_value = [mock.Mock(), mock.Mock()]
|
||||
CONF.set_override('api_version', '2',
|
||||
group='ceilometer_client')
|
||||
osc = clients.OpenStackClients()
|
||||
osc._ceilometer = None
|
||||
osc.ceilometer()
|
||||
@@ -205,10 +208,10 @@ class TestClients(base.TestCase):
|
||||
type(osc.ceilometer()))
|
||||
|
||||
@mock.patch.object(clients.OpenStackClients, 'session')
|
||||
@mock.patch.object(ceclient_v2.Client, '_get_alarm_client')
|
||||
def test_clients_ceilometer_cached(self, mock_get_alarm_client,
|
||||
@mock.patch.object(ceclient_v2.Client, '_get_redirect_client')
|
||||
def test_clients_ceilometer_cached(self, mock_get_redirect_client,
|
||||
mock_session):
|
||||
mock_get_alarm_client.return_value = [mock.Mock(), mock.Mock()]
|
||||
mock_get_redirect_client.return_value = [mock.Mock(), mock.Mock()]
|
||||
osc = clients.OpenStackClients()
|
||||
osc._ceilometer = None
|
||||
ceilometer = osc.ceilometer()
|
||||
@@ -221,14 +224,14 @@ class TestClients(base.TestCase):
|
||||
osc = clients.OpenStackClients()
|
||||
osc._neutron = None
|
||||
osc.neutron()
|
||||
mock_call.assert_called_once_with(cfg.CONF.neutron_client.api_version,
|
||||
mock_call.assert_called_once_with(CONF.neutron_client.api_version,
|
||||
session=mock_session)
|
||||
|
||||
@mock.patch.object(clients.OpenStackClients, 'session')
|
||||
def test_clients_neutron_diff_vers(self, mock_session):
|
||||
'''neutronclient currently only has one version (v2)'''
|
||||
cfg.CONF.set_override('api_version', '2.0',
|
||||
group='neutron_client')
|
||||
CONF.set_override('api_version', '2.0',
|
||||
group='neutron_client')
|
||||
osc = clients.OpenStackClients()
|
||||
osc._neutron = None
|
||||
osc.neutron()
|
||||
@@ -242,3 +245,51 @@ class TestClients(base.TestCase):
|
||||
neutron = osc.neutron()
|
||||
neutron_cached = osc.neutron()
|
||||
self.assertEqual(neutron, neutron_cached)
|
||||
|
||||
@mock.patch.object(monclient, 'Client')
|
||||
@mock.patch.object(ka_loading, 'load_session_from_conf_options')
|
||||
def test_clients_monasca(self, mock_session, mock_call):
|
||||
mock_session.return_value = mock.Mock(
|
||||
get_endpoint=mock.Mock(return_value='test_endpoint'),
|
||||
get_token=mock.Mock(return_value='test_token'),)
|
||||
|
||||
self._register_watcher_clients_auth_opts()
|
||||
|
||||
osc = clients.OpenStackClients()
|
||||
osc._monasca = None
|
||||
osc.monasca()
|
||||
mock_call.assert_called_once_with(
|
||||
CONF.monasca_client.api_version,
|
||||
'test_endpoint',
|
||||
auth_url='http://server.ip:35357', cert_file=None, insecure=False,
|
||||
key_file=None, keystone_timeout=None, os_cacert=None,
|
||||
password='foopassword', service_type='monitoring',
|
||||
token='test_token', username='foousername')
|
||||
|
||||
@mock.patch.object(ka_loading, 'load_session_from_conf_options')
|
||||
def test_clients_monasca_diff_vers(self, mock_session):
|
||||
mock_session.return_value = mock.Mock(
|
||||
get_endpoint=mock.Mock(return_value='test_endpoint'),
|
||||
get_token=mock.Mock(return_value='test_token'),)
|
||||
|
||||
self._register_watcher_clients_auth_opts()
|
||||
|
||||
CONF.set_override('api_version', '2_0', group='monasca_client')
|
||||
osc = clients.OpenStackClients()
|
||||
osc._monasca = None
|
||||
osc.monasca()
|
||||
self.assertEqual(monclient_v2.Client, type(osc.monasca()))
|
||||
|
||||
@mock.patch.object(ka_loading, 'load_session_from_conf_options')
|
||||
def test_clients_monasca_cached(self, mock_session):
|
||||
mock_session.return_value = mock.Mock(
|
||||
get_endpoint=mock.Mock(return_value='test_endpoint'),
|
||||
get_token=mock.Mock(return_value='test_token'),)
|
||||
|
||||
self._register_watcher_clients_auth_opts()
|
||||
|
||||
osc = clients.OpenStackClients()
|
||||
osc._monasca = None
|
||||
monasca = osc.monasca()
|
||||
monasca_cached = osc.monasca()
|
||||
self.assertEqual(monasca, monasca_cached)
|
||||
|
||||
@@ -38,6 +38,7 @@ class TestNovaHelper(base.TestCase):
|
||||
self.instance_uuid = "fb5311b7-37f3-457e-9cde-6494a3c59bfe"
|
||||
self.source_node = "ldev-indeedsrv005"
|
||||
self.destination_node = "ldev-indeedsrv006"
|
||||
self.flavor_name = "x1"
|
||||
|
||||
@staticmethod
|
||||
def fake_server(*args, **kwargs):
|
||||
@@ -89,6 +90,22 @@ class TestNovaHelper(base.TestCase):
|
||||
result = nova_util.set_host_offline("rennes")
|
||||
self.assertFalse(result)
|
||||
|
||||
@mock.patch.object(time, 'sleep', mock.Mock())
|
||||
def test_resize_instance(self, mock_glance, mock_cinder,
|
||||
mock_neutron, mock_nova):
|
||||
nova_util = nova_helper.NovaHelper()
|
||||
server = self.fake_server(self.instance_uuid)
|
||||
setattr(server, 'status', 'VERIFY_RESIZE')
|
||||
self.fake_nova_find_list(nova_util, find=server, list=server)
|
||||
is_success = nova_util.resize_instance(self.instance_uuid,
|
||||
self.flavor_name)
|
||||
self.assertTrue(is_success)
|
||||
|
||||
setattr(server, 'status', 'SOMETHING_ELSE')
|
||||
is_success = nova_util.resize_instance(self.instance_uuid,
|
||||
self.flavor_name)
|
||||
self.assertFalse(is_success)
|
||||
|
||||
@mock.patch.object(time, 'sleep', mock.Mock())
|
||||
def test_live_migrate_instance(self, mock_glance, mock_cinder,
|
||||
mock_neutron, mock_nova):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user