Compare commits
79 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83fdbf7366 | ||
|
|
2db5ae31c7 | ||
|
|
2191844ebb | ||
|
|
9c71b3df88 | ||
|
|
7beb9b4c29 | ||
|
|
8b7dce4803 | ||
|
|
94c50825e4 | ||
|
|
6ff4ba991e | ||
|
|
9e5a3708e1 | ||
|
|
ca5ae03b81 | ||
|
|
b47aefac30 | ||
|
|
70c7e1c639 | ||
|
|
ef78e68022 | ||
|
|
4dfe6a3fa8 | ||
|
|
b53da21b9b | ||
|
|
4305935312 | ||
|
|
894dfa0d7e | ||
|
|
a16351d352 | ||
|
|
75772cd3db | ||
|
|
63d9500904 | ||
|
|
c8096c6148 | ||
|
|
6d0754bb65 | ||
|
|
595b13a622 | ||
|
|
8832ad78e2 | ||
|
|
be81f8aff2 | ||
|
|
e95548b1a9 | ||
|
|
62b39fefbb | ||
|
|
f23548b92f | ||
|
|
43275537ea | ||
|
|
eaed650000 | ||
|
|
0505466ea5 | ||
|
|
0e7bfe61bd | ||
|
|
096354c255 | ||
|
|
afe06e2349 | ||
|
|
1d689ef410 | ||
|
|
58fb86a3da | ||
|
|
f675003076 | ||
|
|
e34ee792a8 | ||
|
|
037f43cd04 | ||
|
|
8f87699910 | ||
|
|
c811051351 | ||
|
|
a2750c74f9 | ||
|
|
67455c6a84 | ||
|
|
a7455a8bf7 | ||
|
|
7fcb683404 | ||
|
|
65cf19a7e4 | ||
|
|
facca13dc4 | ||
|
|
e8576e8963 | ||
|
|
dfc9a3b37d | ||
|
|
24dc92091c | ||
|
|
53d6d7d131 | ||
|
|
fd6ecc1b13 | ||
|
|
8bac4fd42a | ||
|
|
55d186be58 | ||
|
|
ed438d2eb2 | ||
|
|
4898df89ca | ||
|
|
ae949148ef | ||
|
|
e0242b49f1 | ||
|
|
5b57985686 | ||
|
|
f6ef197473 | ||
|
|
61c926a10b | ||
|
|
9c33f8aaeb | ||
|
|
08fc69cfc4 | ||
|
|
dc6b8aad7e | ||
|
|
473cee8ad3 | ||
|
|
2ffeb48010 | ||
|
|
86d3c2ff89 | ||
|
|
47759202a8 | ||
|
|
c0306ea8f4 | ||
|
|
34ccb7c23e | ||
|
|
9900273988 | ||
|
|
4662f248b3 | ||
|
|
7638103203 | ||
|
|
adc5587a2b | ||
|
|
34ab93a5f2 | ||
|
|
c286e1ec4b | ||
|
|
6c1769a15e | ||
|
|
b41a2cc940 | ||
|
|
8b357ace5a |
1
.gitignore
vendored
@@ -43,6 +43,7 @@ output/*/index.html
|
||||
|
||||
# Sphinx
|
||||
doc/build
|
||||
doc/source/api
|
||||
|
||||
# pbr generates these
|
||||
AUTHORS
|
||||
|
||||
258
devstack/lib/watcher
Normal file
@@ -0,0 +1,258 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# lib/watcher
|
||||
# Functions to control the configuration and operation of the watcher services
|
||||
|
||||
# Dependencies:
|
||||
#
|
||||
# - ``functions`` file
|
||||
# - ``SERVICE_{TENANT_NAME|PASSWORD}`` must be defined
|
||||
# - ``DEST``, ``DATA_DIR``, ``STACK_USER`` must be defined
|
||||
|
||||
# ``stack.sh`` calls the entry points in this order:
|
||||
#
|
||||
# - is_watcher_enabled
|
||||
# - install_watcher
|
||||
# - configure_watcher
|
||||
# - create_watcher_conf
|
||||
# - init_watcher
|
||||
# - start_watcher
|
||||
# - stop_watcher
|
||||
# - cleanup_watcher
|
||||
|
||||
# Save trace setting
|
||||
_XTRACE_WATCHER=$(set +o | grep xtrace)
|
||||
set +o xtrace
|
||||
|
||||
|
||||
# Defaults
|
||||
# --------
|
||||
|
||||
# Set up default directories
|
||||
WATCHER_REPO=${WATCHER_REPO:-${GIT_BASE}/openstack/watcher.git}
|
||||
WATCHER_BRANCH=${WATCHER_BRANCH:-master}
|
||||
WATCHER_DIR=$DEST/watcher
|
||||
|
||||
GITREPO["python-watcherclient"]=${WATCHERCLIENT_REPO:-${GIT_BASE}/openstack/python-watcherclient.git}
|
||||
GITBRANCH["python-watcherclient"]=${WATCHERCLIENT_BRANCH:-master}
|
||||
GITDIR["python-watcherclient"]=$DEST/python-watcherclient
|
||||
|
||||
WATCHER_STATE_PATH=${WATCHER_STATE_PATH:=$DATA_DIR/watcher}
|
||||
WATCHER_AUTH_CACHE_DIR=${WATCHER_AUTH_CACHE_DIR:-/var/cache/watcher}
|
||||
|
||||
WATCHER_CONF_DIR=/etc/watcher
|
||||
WATCHER_CONF=$WATCHER_CONF_DIR/watcher.conf
|
||||
WATCHER_POLICY_JSON=$WATCHER_CONF_DIR/policy.json
|
||||
|
||||
if is_ssl_enabled_service "watcher" || is_service_enabled tls-proxy; then
|
||||
WATCHER_SERVICE_PROTOCOL="https"
|
||||
fi
|
||||
|
||||
# Public facing bits
|
||||
WATCHER_SERVICE_HOST=${WATCHER_SERVICE_HOST:-$HOST_IP}
|
||||
WATCHER_SERVICE_PORT=${WATCHER_SERVICE_PORT:-9322}
|
||||
WATCHER_SERVICE_PORT_INT=${WATCHER_SERVICE_PORT_INT:-19322}
|
||||
WATCHER_SERVICE_PROTOCOL=${WATCHER_SERVICE_PROTOCOL:-$SERVICE_PROTOCOL}
|
||||
|
||||
# Support entry points installation of console scripts
|
||||
if [[ -d $WATCHER_DIR/bin ]]; then
|
||||
WATCHER_BIN_DIR=$WATCHER_DIR/bin
|
||||
else
|
||||
WATCHER_BIN_DIR=$(get_python_exec_prefix)
|
||||
fi
|
||||
|
||||
# Entry Points
|
||||
# ------------
|
||||
|
||||
# Test if any watcher services are enabled
|
||||
# is_watcher_enabled
|
||||
function is_watcher_enabled {
|
||||
[[ ,${ENABLED_SERVICES} =~ ,"watcher-" ]] && return 0
|
||||
return 1
|
||||
}
|
||||
|
||||
# cleanup_watcher() - Remove residual data files, anything left over from previous
|
||||
# runs that a clean run would need to clean up
|
||||
function cleanup_watcher {
|
||||
sudo rm -rf $WATCHER_STATE_PATH $WATCHER_AUTH_CACHE_DIR
|
||||
}
|
||||
|
||||
# configure_watcher() - Set config files, create data dirs, etc
|
||||
function configure_watcher {
|
||||
# Put config files in ``/etc/watcher`` for everyone to find
|
||||
if [[ ! -d $WATCHER_CONF_DIR ]]; then
|
||||
sudo mkdir -p $WATCHER_CONF_DIR
|
||||
sudo chown $STACK_USER $WATCHER_CONF_DIR
|
||||
fi
|
||||
|
||||
install_default_policy watcher
|
||||
|
||||
# Rebuild the config file from scratch
|
||||
create_watcher_conf
|
||||
}
|
||||
|
||||
# create_watcher_accounts() - Set up common required watcher accounts
|
||||
#
|
||||
# Project User Roles
|
||||
# ------------------------------------------------------------------
|
||||
# SERVICE_TENANT_NAME watcher service
|
||||
function create_watcher_accounts {
|
||||
create_service_user "watcher" "admin"
|
||||
|
||||
if [[ "$KEYSTONE_CATALOG_BACKEND" = 'sql' ]]; then
|
||||
local watcher_service=$(get_or_create_service "watcher" \
|
||||
"infra-optim" "Watcher Infrastructure Optimization Service")
|
||||
get_or_create_endpoint $watcher_service \
|
||||
"$REGION_NAME" \
|
||||
"$WATCHER_SERVICE_PROTOCOL://$WATCHER_SERVICE_HOST:$WATCHER_SERVICE_PORT" \
|
||||
"$WATCHER_SERVICE_PROTOCOL://$WATCHER_SERVICE_HOST:$WATCHER_SERVICE_PORT" \
|
||||
"$WATCHER_SERVICE_PROTOCOL://$WATCHER_SERVICE_HOST:$WATCHER_SERVICE_PORT"
|
||||
fi
|
||||
}
|
||||
|
||||
# create_watcher_conf() - Create a new watcher.conf file
|
||||
function create_watcher_conf {
|
||||
# (Re)create ``watcher.conf``
|
||||
rm -f $WATCHER_CONF
|
||||
|
||||
iniset $WATCHER_CONF DEFAULT debug "$ENABLE_DEBUG_LOG_LEVEL"
|
||||
iniset $WATCHER_CONF DEFAULT control_exchange watcher
|
||||
|
||||
iniset $WATCHER_CONF database connection $(database_connection_url watcher)
|
||||
iniset $WATCHER_CONF api host "$WATCHER_SERVICE_HOST"
|
||||
iniset $WATCHER_CONF api port "$WATCHER_SERVICE_PORT"
|
||||
|
||||
iniset $WATCHER_CONF oslo_policy policy_file $WATCHER_POLICY_JSON
|
||||
|
||||
iniset $WATCHER_CONF oslo_messaging_rabbit rabbit_userid $RABBIT_USERID
|
||||
iniset $WATCHER_CONF oslo_messaging_rabbit rabbit_password $RABBIT_PASSWORD
|
||||
iniset $WATCHER_CONF oslo_messaging_rabbit rabbit_host $RABBIT_HOST
|
||||
|
||||
iniset $WATCHER_CONF keystone_authtoken admin_user watcher
|
||||
iniset $WATCHER_CONF keystone_authtoken admin_password $SERVICE_PASSWORD
|
||||
iniset $WATCHER_CONF keystone_authtoken admin_tenant_name $SERVICE_TENANT_NAME
|
||||
|
||||
configure_auth_token_middleware $WATCHER_CONF watcher $WATCHER_AUTH_CACHE_DIR
|
||||
|
||||
iniset $WATCHER_CONF keystone_authtoken auth_uri $KEYSTONE_SERVICE_URI/v3
|
||||
iniset $WATCHER_CONF keystone_authtoken auth_version v3
|
||||
|
||||
if is_fedora || is_suse; then
|
||||
# watcher defaults to /usr/local/bin, but fedora and suse pip like to
|
||||
# install things in /usr/bin
|
||||
iniset $WATCHER_CONF DEFAULT bindir "/usr/bin"
|
||||
fi
|
||||
|
||||
if [ -n "$WATCHER_STATE_PATH" ]; then
|
||||
iniset $WATCHER_CONF DEFAULT state_path "$WATCHER_STATE_PATH"
|
||||
iniset $WATCHER_CONF oslo_concurrency lock_path "$WATCHER_STATE_PATH"
|
||||
fi
|
||||
|
||||
if [ "$SYSLOG" != "False" ]; then
|
||||
iniset $WATCHER_CONF DEFAULT use_syslog "True"
|
||||
fi
|
||||
|
||||
# Format logging
|
||||
if [ "$LOG_COLOR" == "True" ] && [ "$SYSLOG" == "False" ]; then
|
||||
setup_colorized_logging $WATCHER_CONF DEFAULT
|
||||
else
|
||||
# Show user_name and project_name instead of user_id and project_id
|
||||
iniset $WATCHER_CONF DEFAULT logging_context_format_string "%(asctime)s.%(msecs)03d %(levelname)s %(name)s [%(request_id)s %(user_name)s %(project_name)s] %(instance)s%(message)s"
|
||||
fi
|
||||
|
||||
# Register SSL certificates if provided
|
||||
if is_ssl_enabled_service watcher; then
|
||||
ensure_certificates WATCHER
|
||||
|
||||
iniset $WATCHER_CONF DEFAULT ssl_cert_file "$WATCHER_SSL_CERT"
|
||||
iniset $WATCHER_CONF DEFAULT ssl_key_file "$WATCHER_SSL_KEY"
|
||||
|
||||
iniset $WATCHER_CONF DEFAULT enabled_ssl_apis "$WATCHER_ENABLED_APIS"
|
||||
fi
|
||||
|
||||
if is_service_enabled ceilometer; then
|
||||
iniset $WATCHER_CONF watcher_messaging notifier_driver "messaging"
|
||||
fi
|
||||
}
|
||||
|
||||
# create_watcher_cache_dir() - Part of the init_watcher() process
|
||||
function create_watcher_cache_dir {
|
||||
# Create cache dir
|
||||
sudo mkdir -p $WATCHER_AUTH_CACHE_DIR
|
||||
sudo chown $STACK_USER $WATCHER_AUTH_CACHE_DIR
|
||||
rm -f $WATCHER_AUTH_CACHE_DIR/*
|
||||
}
|
||||
|
||||
# init_watcher() - Initialize databases, etc.
|
||||
function init_watcher {
|
||||
# clean up from previous (possibly aborted) runs
|
||||
# create required data files
|
||||
if is_service_enabled $DATABASE_BACKENDS && is_service_enabled watcher-api; then
|
||||
# (Re)create watcher database
|
||||
recreate_database watcher
|
||||
|
||||
# Create watcher schema
|
||||
$WATCHER_BIN_DIR/watcher-db-manage --config-file $WATCHER_CONF create_schema
|
||||
fi
|
||||
create_watcher_cache_dir
|
||||
}
|
||||
|
||||
# install_watcherclient() - Collect source and prepare
|
||||
function install_watcherclient {
|
||||
if use_library_from_git "python-watcherclient"; then
|
||||
git_clone_by_name "python-watcherclient"
|
||||
setup_dev_lib "python-watcherclient"
|
||||
fi
|
||||
}
|
||||
|
||||
# install_watcher() - Collect source and prepare
|
||||
function install_watcher {
|
||||
git_clone $WATCHER_REPO $WATCHER_DIR $WATCHER_BRANCH
|
||||
setup_develop $WATCHER_DIR
|
||||
}
|
||||
|
||||
# start_watcher_api() - Start the API process ahead of other things
|
||||
function start_watcher_api {
|
||||
# Get right service port for testing
|
||||
local service_port=$WATCHER_SERVICE_PORT
|
||||
local service_protocol=$WATCHER_SERVICE_PROTOCOL
|
||||
if is_service_enabled tls-proxy; then
|
||||
service_port=$WATCHER_SERVICE_PORT_INT
|
||||
service_protocol="http"
|
||||
fi
|
||||
|
||||
run_process watcher-api "$WATCHER_BIN_DIR/watcher-api --config-file $WATCHER_CONF"
|
||||
echo "Waiting for watcher-api to start..."
|
||||
if ! wait_for_service $SERVICE_TIMEOUT $service_protocol://$WATCHER_SERVICE_HOST:$service_port; then
|
||||
die $LINENO "watcher-api did not start"
|
||||
fi
|
||||
|
||||
# Start proxies if enabled
|
||||
if is_service_enabled tls-proxy; then
|
||||
start_tls_proxy '*' $WATCHER_SERVICE_PORT $WATCHER_SERVICE_HOST $WATCHER_SERVICE_PORT_INT &
|
||||
start_tls_proxy '*' $EC2_SERVICE_PORT $WATCHER_SERVICE_HOST $WATCHER_SERVICE_PORT_INT &
|
||||
fi
|
||||
}
|
||||
|
||||
# start_watcher() - Start running processes, including screen
|
||||
function start_watcher {
|
||||
# ``run_process`` checks ``is_service_enabled``, it is not needed here
|
||||
start_watcher_api
|
||||
run_process watcher-decision-engine "$WATCHER_BIN_DIR/watcher-decision-engine --config-file $WATCHER_CONF"
|
||||
run_process watcher-applier "$WATCHER_BIN_DIR/watcher-applier --config-file $WATCHER_CONF"
|
||||
}
|
||||
|
||||
# stop_watcher() - Stop running processes (non-screen)
|
||||
function stop_watcher {
|
||||
for serv in watcher-api watcher-decision-engine watcher-applier; do
|
||||
stop_process $serv
|
||||
done
|
||||
}
|
||||
|
||||
# Restore xtrace
|
||||
$_XTRACE_WATCHER
|
||||
|
||||
# Tell emacs to use shell-script-mode
|
||||
## Local variables:
|
||||
## mode: shell-script
|
||||
## End:
|
||||
46
devstack/local.conf.compute
Normal file
@@ -0,0 +1,46 @@
|
||||
# Sample ``local.conf`` for compute node for Watcher development
|
||||
# NOTE: Copy this file to the root DevStack directory for it to work properly.
|
||||
|
||||
[[local|localrc]]
|
||||
|
||||
ADMIN_PASSWORD=nomoresecrete
|
||||
DATABASE_PASSWORD=stackdb
|
||||
RABBIT_PASSWORD=stackqueue
|
||||
SERVICE_PASSWORD=$ADMIN_PASSWORD
|
||||
SERVICE_TOKEN=azertytoken
|
||||
|
||||
HOST_IP=192.168.42.2 # Change this to this compute node's IP address
|
||||
FLAT_INTERFACE=eth0
|
||||
|
||||
FIXED_RANGE=10.254.1.0/24 # Change this to whatever your network is
|
||||
NETWORK_GATEWAY=10.254.1.1 # Change this for your network
|
||||
|
||||
MULTI_HOST=1
|
||||
|
||||
SERVICE_HOST=192.168.42.1 # Change this to the IP of your controller node
|
||||
MYSQL_HOST=$SERVICE_HOST
|
||||
RABBIT_HOST=$SERVICE_HOST
|
||||
GLANCE_HOSTPORT=${SERVICE_HOST}:9292
|
||||
|
||||
DATABASE_TYPE=mysql
|
||||
|
||||
# Enable services (including neutron)
|
||||
ENABLED_SERVICES=n-cpu,n-api-meta,c-vol,q-agt
|
||||
|
||||
NOVA_VNC_ENABLED=True
|
||||
NOVNCPROXY_URL="http://$SERVICE_HOST:6080/vnc_auto.html"
|
||||
VNCSERVER_LISTEN=0.0.0.0
|
||||
VNCSERVER_PROXYCLIENT_ADDRESS=$HOST_IP
|
||||
|
||||
NOVA_INSTANCES_PATH=/opt/stack/data/instances
|
||||
|
||||
# Enable the Ceilometer plugin for the compute agent
|
||||
enable_plugin ceilometer git://git.openstack.org/openstack/ceilometer
|
||||
disable_service ceilometer-acentral,ceilometer-collector,ceilometer-api
|
||||
|
||||
LOGFILE=$DEST/logs/stack.sh.log
|
||||
LOGDAYS=2
|
||||
|
||||
[[post-config|$NOVA_CONF]]
|
||||
[DEFAULT]
|
||||
compute_monitors=cpu.virt_driver
|
||||
48
devstack/local.conf.controller
Normal file
@@ -0,0 +1,48 @@
|
||||
# Sample ``local.conf`` for controller node for Watcher development
|
||||
# NOTE: Copy this file to the root DevStack directory for it to work properly.
|
||||
|
||||
[[local|localrc]]
|
||||
|
||||
ADMIN_PASSWORD=nomoresecrete
|
||||
DATABASE_PASSWORD=stackdb
|
||||
RABBIT_PASSWORD=stackqueue
|
||||
SERVICE_PASSWORD=$ADMIN_PASSWORD
|
||||
SERVICE_TOKEN=azertytoken
|
||||
|
||||
HOST_IP=192.168.42.1 # Change this to your controller node IP address
|
||||
FLAT_INTERFACE=eth0
|
||||
|
||||
FIXED_RANGE=10.254.1.0/24 # Change this to whatever your network is
|
||||
NETWORK_GATEWAY=10.254.1.1 # Change this for your network
|
||||
|
||||
MULTI_HOST=1
|
||||
|
||||
# This is the controller node, so disable nova-compute
|
||||
disable_service n-cpu
|
||||
|
||||
# Disable nova-network and use neutron instead
|
||||
disable_service n-net
|
||||
ENABLED_SERVICES+=,q-svc,q-dhcp,q-meta,q-agt,q-l3,neutron
|
||||
|
||||
# Enable remote console access
|
||||
enable_service n-cauth
|
||||
|
||||
# Enable the Watcher plugin
|
||||
enable_plugin watcher git://git.openstack.org/openstack/watcher
|
||||
|
||||
# Enable the Ceilometer plugin
|
||||
enable_plugin ceilometer git://git.openstack.org/openstack/ceilometer
|
||||
|
||||
# This is the controller node, so disable the ceilometer compute agent
|
||||
disable_service ceilometer-acompute
|
||||
|
||||
LOGFILE=$DEST/logs/stack.sh.log
|
||||
LOGDAYS=2
|
||||
|
||||
[[post-config|$NOVA_CONF]]
|
||||
[DEFAULT]
|
||||
compute_monitors=cpu.virt_driver
|
||||
|
||||
[[post-config|$WATCHER_CONF]]
|
||||
[watcher_goals]
|
||||
goals=BASIC_CONSOLIDATION:basic,DUMMY:dummy
|
||||
53
devstack/plugin.sh
Normal file
@@ -0,0 +1,53 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# plugin.sh - DevStack plugin script to install watcher
|
||||
|
||||
# Save trace setting
|
||||
_XTRACE_WATCHER_PLUGIN=$(set +o | grep xtrace)
|
||||
set -o xtrace
|
||||
|
||||
echo_summary "watcher's plugin.sh was called..."
|
||||
source $DEST/watcher/devstack/lib/watcher
|
||||
|
||||
# Show all of defined environment variables
|
||||
(set -o posix; set)
|
||||
|
||||
if is_service_enabled watcher-api watcher-decision-engine watcher-applier; then
|
||||
if [[ "$1" == "stack" && "$2" == "pre-install" ]]; then
|
||||
echo_summary "Before Installing watcher"
|
||||
elif [[ "$1" == "stack" && "$2" == "install" ]]; then
|
||||
echo_summary "Installing watcher"
|
||||
install_watcher
|
||||
|
||||
LIBS_FROM_GIT="${LIBS_FROM_GIT},python-watcherclient"
|
||||
|
||||
install_watcherclient
|
||||
cleanup_watcher
|
||||
elif [[ "$1" == "stack" && "$2" == "post-config" ]]; then
|
||||
echo_summary "Configuring watcher"
|
||||
configure_watcher
|
||||
|
||||
if is_service_enabled key; then
|
||||
create_watcher_accounts
|
||||
fi
|
||||
|
||||
elif [[ "$1" == "stack" && "$2" == "extra" ]]; then
|
||||
# Initialize watcher
|
||||
init_watcher
|
||||
|
||||
# Start the watcher components
|
||||
echo_summary "Starting watcher"
|
||||
start_watcher
|
||||
fi
|
||||
|
||||
if [[ "$1" == "unstack" ]]; then
|
||||
stop_watcher
|
||||
fi
|
||||
|
||||
if [[ "$1" == "clean" ]]; then
|
||||
cleanup_watcher
|
||||
fi
|
||||
fi
|
||||
|
||||
# Restore xtrace
|
||||
$_XTRACE_WATCHER_PLUGIN
|
||||
9
devstack/settings
Normal file
@@ -0,0 +1,9 @@
|
||||
# DevStack settings
|
||||
|
||||
# Make sure rabbit is enabled
|
||||
enable_service rabbit
|
||||
|
||||
# Enable Watcher services
|
||||
enable_service watcher-api
|
||||
enable_service watcher-decision-engine
|
||||
enable_service watcher-applier
|
||||
@@ -34,8 +34,8 @@ Components
|
||||
AMQP Bus
|
||||
--------
|
||||
|
||||
The AMQP message bus handles asynchronous communications between the different
|
||||
Watcher components.
|
||||
The AMQP message bus handles internal asynchronous communications between the
|
||||
different Watcher components.
|
||||
|
||||
.. _cluster_history_db_definition:
|
||||
|
||||
@@ -51,7 +51,15 @@ MongoDB,...) but will probably be more performant when using
|
||||
which are optimized for handling time series data, which are arrays of numbers
|
||||
indexed by time (a datetime or a datetime range).
|
||||
|
||||
.. _watcher_api_definition:
|
||||
.. _cluster_model_db_definition:
|
||||
|
||||
Cluster Model Database
|
||||
------------------------
|
||||
|
||||
This component stores the data related to the
|
||||
:ref:`Cluster Data Model <cluster_data_model_definition>`.
|
||||
|
||||
.. _archi_watcher_api_definition:
|
||||
|
||||
Watcher API
|
||||
-----------
|
||||
@@ -63,13 +71,13 @@ It enables the :ref:`Administrator <administrator_definition>` of a
|
||||
:ref:`Cluster <cluster_definition>` to control and monitor the Watcher system
|
||||
via any interaction mechanism connected to this API:
|
||||
|
||||
- :ref:`CLI <watcher_cli_definition>`
|
||||
- :ref:`CLI <archi_watcher_cli_definition>`
|
||||
- Horizon plugin
|
||||
- Python SDK
|
||||
|
||||
You can also read the detailed description of `Watcher API`_.
|
||||
|
||||
.. _watcher_applier_definition:
|
||||
.. _archi_watcher_applier_definition:
|
||||
|
||||
Watcher Applier
|
||||
---------------
|
||||
@@ -111,7 +119,7 @@ If the :ref:`Action <action_definition>` fails, the
|
||||
previous state of the :ref:`Managed resource <managed_resource_definition>`
|
||||
(i.e. before the command was sent to the underlying OpenStack service).
|
||||
|
||||
.. _watcher_cli_definition:
|
||||
.. _archi_watcher_cli_definition:
|
||||
|
||||
Watcher CLI
|
||||
-----------
|
||||
@@ -121,14 +129,14 @@ Watcher system in order to control it or to know its current status.
|
||||
|
||||
Please, read `the detailed documentation about Watcher CLI <https://factory.b-com.com/www/watcher/doc/python-watcherclient/>`_
|
||||
|
||||
.. _watcher_database_definition:
|
||||
.. _archi_watcher_database_definition:
|
||||
|
||||
Watcher Database
|
||||
----------------
|
||||
|
||||
This database stores all the Watcher domain objects which can be requested
|
||||
by the :ref:`Watcher API <watcher_api_definition>` or the
|
||||
:ref:`Watcher CLI <watcher_cli_definition>`:
|
||||
by the :ref:`Watcher API <archi_watcher_api_definition>` or the
|
||||
:ref:`Watcher CLI <archi_watcher_cli_definition>`:
|
||||
|
||||
- :ref:`Audit templates <audit_template_definition>`
|
||||
- :ref:`Audits <audit_definition>`
|
||||
@@ -139,7 +147,7 @@ by the :ref:`Watcher API <watcher_api_definition>` or the
|
||||
The Watcher domain being here "*optimization of some resources provided by an
|
||||
OpenStack system*".
|
||||
|
||||
.. _watcher_decision_engine_definition:
|
||||
.. _archi_watcher_decision_engine_definition:
|
||||
|
||||
Watcher Decision Engine
|
||||
-----------------------
|
||||
@@ -159,7 +167,7 @@ The :ref:`Strategy <strategy_definition>` is then dynamically loaded (via
|
||||
`stevedore <https://github.com/openstack/stevedore/>`_). The
|
||||
:ref:`Watcher Decision Engine <watcher_decision_engine_definition>` calls the
|
||||
**execute()** method of the :ref:`Strategy <strategy_definition>` class which
|
||||
generates a set of :ref:`Actions <action_definition>`.
|
||||
generates a solution composed of a set of :ref:`Actions <action_definition>`.
|
||||
|
||||
These :ref:`Actions <action_definition>` are scheduled in time by the
|
||||
:ref:`Watcher Planner <watcher_planner_definition>` (i.e., it generates an
|
||||
@@ -169,14 +177,197 @@ In order to compute the potential :ref:`Solution <solution_definition>` for the
|
||||
Audit, the :ref:`Strategy <strategy_definition>` relies on two sets of data:
|
||||
|
||||
- the current state of the
|
||||
:ref:`Managed resources <managed_resource_definition>`
|
||||
(e.g., the data stored in the Nova database)
|
||||
:ref:`Managed resources <managed_resource_definition>`
|
||||
(e.g., the data stored in the Nova database)
|
||||
- the data stored in the
|
||||
:ref:`Cluster History Database <cluster_history_db_definition>`
|
||||
:ref:`Cluster History Database <cluster_history_db_definition>`
|
||||
which provides information about the past of the
|
||||
:ref:`Cluster <cluster_definition>`
|
||||
|
||||
So far, only one :ref:`Strategy <strategy_definition>` can be associated to a
|
||||
given :ref:`Goal <goal_definition>` via the main Watcher configuration file.
|
||||
|
||||
.. _data_model:
|
||||
|
||||
Data model
|
||||
==========
|
||||
|
||||
The following diagram shows the data model of Watcher, especially the
|
||||
functional dependency of objects from the actors (Admin, Customer) point of
|
||||
view (Goals, Audits, Action Plans, ...):
|
||||
|
||||
.. image:: ./images/functional_data_model.svg
|
||||
:width: 100%
|
||||
|
||||
.. _sequence_diagrams:
|
||||
|
||||
Sequence diagrams
|
||||
=================
|
||||
|
||||
The following paragraph shows the messages exchanged between the different
|
||||
components of Watcher for the most often used scenarios.
|
||||
|
||||
.. _sequence_diagrams_create_audit_template:
|
||||
|
||||
Create a new Audit Template
|
||||
---------------------------
|
||||
|
||||
The :ref:`Administrator <administrator_definition>` first creates an
|
||||
:ref:`Audit template <audit_template_definition>` providing at least the
|
||||
following parameters:
|
||||
|
||||
- A name
|
||||
- A goal to achieve
|
||||
|
||||
.. image:: ./images/sequence_create_audit_template.png
|
||||
:width: 100%
|
||||
|
||||
The `Watcher API`_ just makes sure that the goal exists (i.e. it is declared
|
||||
in the Watcher configuration file) and stores a new audit template in the
|
||||
:ref:`Watcher Database <watcher_database_definition>`.
|
||||
|
||||
.. _sequence_diagrams_create_and_launch_audit:
|
||||
|
||||
Create and launch a new Audit
|
||||
-----------------------------
|
||||
|
||||
The :ref:`Administrator <administrator_definition>` can then launch a new
|
||||
:ref:`Audit <audit_definition>` by providing at least the unique UUID of the
|
||||
previously created :ref:`Audit template <audit_template_definition>`:
|
||||
|
||||
.. image:: ./images/sequence_create_and_launch_audit.png
|
||||
:width: 100%
|
||||
|
||||
A message is sent on the :ref:`AMQP bus <amqp_bus_definition>` which triggers
|
||||
the Audit in the
|
||||
:ref:`Watcher Decision Engine <watcher_decision_engine_definition>`:
|
||||
|
||||
.. image:: ./images/sequence_trigger_audit_in_decision_engine.png
|
||||
:width: 100%
|
||||
|
||||
The :ref:`Watcher Decision Engine <watcher_decision_engine_definition>` reads
|
||||
the Audit parameters from the
|
||||
:ref:`Watcher Database <watcher_database_definition>`. It instantiates the
|
||||
appropriate :ref:`Strategy <strategy_definition>` (using entry points)
|
||||
associated to the :ref:`Goal <goal_definition>` of the
|
||||
:ref:`Audit <audit_definition>` (it uses the information of the Watcher
|
||||
configuration file to find the mapping between the
|
||||
:ref:`Goal <goal_definition>` and the :ref:`Strategy <strategy_definition>`
|
||||
python class).
|
||||
|
||||
The :ref:`Watcher Decision Engine <watcher_decision_engine_definition>` also
|
||||
builds the :ref:`Cluster Data Model <cluster_data_model_definition>`. This
|
||||
data model is needed by the :ref:`Strategy <strategy_definition>` to know the
|
||||
current state and topology of the audited
|
||||
:ref:`Openstack cluster <cluster_definition>`.
|
||||
|
||||
The :ref:`Watcher Decision Engine <watcher_decision_engine_definition>` calls
|
||||
the **execute()** method of the instantiated
|
||||
:ref:`Strategy <strategy_definition>` and provides the data model as an input
|
||||
parameter. This method computes a :ref:`Solution <strategy_definition>` to
|
||||
achieve the goal and returns it to the
|
||||
:ref:`Decision Engine <watcher_decision_engine_definition>`. At this point,
|
||||
actions are not scheduled yet.
|
||||
|
||||
The :ref:`Watcher Decision Engine <watcher_decision_engine_definition>`
|
||||
dynamically loads the :ref:`Watcher Planner <watcher_planner_definition>`
|
||||
implementation which is configured in Watcher (via entry points) and calls the
|
||||
**schedule()** method of this class with the solution as an input parameter.
|
||||
This method finds an appropriate scheduling of
|
||||
:ref:`Actions <action_definition>` taking into account some scheduling rules
|
||||
(such as priorities between actions).
|
||||
It generates a new :ref:`Action Plan <action_plan_definition>` with status
|
||||
**RECOMMENDED** and saves it into the
|
||||
:ref:`Watcher Database <watcher_database_definition>`. The saved action plan is
|
||||
now a scheduled flow of actions.
|
||||
|
||||
If every step executed successfully, the
|
||||
:ref:`Watcher Decision Engine <watcher_decision_engine_definition>` updates
|
||||
the current status of the Audit to **SUCCEEDED** in the
|
||||
:ref:`Watcher Database <watcher_database_definition>` and sends a notification
|
||||
on the bus to inform other components that the :ref:`Audit <audit_definition>`
|
||||
was successful.
|
||||
|
||||
|
||||
.. _sequence_diagrams_launch_action_plan:
|
||||
|
||||
Launch Action Plan
|
||||
------------------
|
||||
|
||||
The :ref:`Administrator <administrator_definition>` can then launch the
|
||||
recommended :ref:`Action Plan <action_plan_definition>`:
|
||||
|
||||
.. image:: ./images/sequence_launch_action_plan.png
|
||||
:width: 100%
|
||||
|
||||
A message is sent on the :ref:`AMQP bus <amqp_bus_definition>` which triggers
|
||||
the :ref:`Action Plan <action_plan_definition>` in the
|
||||
:ref:`Watcher Applier <watcher_applier_definition>`:
|
||||
|
||||
.. image:: ./images/sequence_launch_action_plan_in_applier.png
|
||||
:width: 100%
|
||||
|
||||
The :ref:`Watcher Applier <watcher_applier_definition>` will get the
|
||||
description of the flow of :ref:`Actions <action_definition>` from the
|
||||
:ref:`Watcher Database <watcher_database_definition>` and for each
|
||||
:ref:`Action <action_definition>` it will instantiate a corresponding
|
||||
:ref:`Action <action_definition>` handler python class.
|
||||
|
||||
The :ref:`Watcher Applier <watcher_applier_definition>` will then call the
|
||||
following methods of the :ref:`Action <action_definition>` handler:
|
||||
|
||||
- **validate_parameters()**: this method will make sure that all the
|
||||
provided input parameters are valid:
|
||||
|
||||
- If all parameters are valid, the Watcher Applier moves on to the next
|
||||
step.
|
||||
- If it is not, an error is raised and the action is not executed. A
|
||||
notification is sent on the bus informing other components of the
|
||||
failure.
|
||||
|
||||
- **preconditions()**: this method will make sure that all conditions are met
|
||||
before executing the action (for example, it makes sure that an instance
|
||||
still exists before trying to migrate it).
|
||||
- **execute()**: this method is what triggers real commands on other
|
||||
OpenStack services (such as Nova, ...) in order to change target resource
|
||||
state. If the action is successfully executed, a notification message is
|
||||
sent on the bus indicating that the new state of the action is
|
||||
**SUCCEEDED**.
|
||||
|
||||
If every action of the action flow has been executed successfully, a
|
||||
notification is sent on the bus to indicate that the whole
|
||||
:ref:`Action Plan <action_plan_definition>` has **SUCCEEDED**.
|
||||
|
||||
|
||||
.. _state_machine_diagrams:
|
||||
|
||||
State Machine diagrams
|
||||
======================
|
||||
|
||||
.. _audit_state_machine:
|
||||
|
||||
Audit State Machine
|
||||
-------------------
|
||||
|
||||
The following diagram shows the different possible states of an
|
||||
:ref:`Audit <audit_definition>` and what event makes the state change to a new
|
||||
value:
|
||||
|
||||
.. image:: ./images/audit_state_machine.png
|
||||
:width: 100%
|
||||
|
||||
.. _action_plan_state_machine:
|
||||
|
||||
Action Plan State Machine
|
||||
-------------------------
|
||||
|
||||
The following diagram shows the different possible states of an
|
||||
:ref:`Action Plan <action_plan_definition>` and what event makes the state
|
||||
change to a new value:
|
||||
|
||||
.. image:: ./images/action_plan_state_machine.png
|
||||
:width: 100%
|
||||
|
||||
|
||||
|
||||
.. _Watcher API: webapi/v1.html
|
||||
|
||||
@@ -11,19 +11,14 @@
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
import sys
|
||||
from watcher import version as watcher_version
|
||||
|
||||
sys.path.insert(0, os.path.abspath('../..'))
|
||||
# -- General configuration ----------------------------------------------------
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
# 'sphinx.ext.intersphinx',
|
||||
'sphinx.ext.viewcode',
|
||||
'sphinxcontrib.httpdomain',
|
||||
'sphinxcontrib.pecanwsme.rest',
|
||||
@@ -46,8 +41,8 @@ source_suffix = '.rst'
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'watcher'
|
||||
copyright = u'2015, OpenStack Foundation'
|
||||
project = u'Watcher'
|
||||
copyright = u'OpenStack Foundation'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
@@ -62,6 +57,14 @@ version = watcher_version.version_info.version_string()
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
modindex_common_prefix = ['watcher.']
|
||||
|
||||
exclude_patterns = [
|
||||
# The man directory includes some snippet files that are included
|
||||
# in other documents during the build but that should not be
|
||||
# included in the toctree themselves, so tell Sphinx to ignore
|
||||
# them when scanning for input files.
|
||||
'man/footer.rst',
|
||||
'man/general-options.rst',
|
||||
]
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
add_function_parentheses = True
|
||||
@@ -73,6 +76,22 @@ add_module_names = True
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# -- Options for man page output --------------------------------------------
|
||||
|
||||
# Grouping the document tree for man pages.
|
||||
# List of tuples 'sourcefile', 'target', u'title', u'Authors name', 'manual'
|
||||
|
||||
man_pages = [
|
||||
('man/watcher-api', 'watcher-api', u'Watcher API Server',
|
||||
[u'OpenStack'], 1),
|
||||
('man/watcher-applier', 'watcher-applier', u'Watcher Applier',
|
||||
[u'OpenStack'], 1),
|
||||
('man/watcher-db-manage', 'watcher-db-manage',
|
||||
u'Watcher Db Management Utility', [u'OpenStack'], 1),
|
||||
('man/watcher-decision-engine', 'watcher-decision-engine',
|
||||
u'Watcher Decision Engine', [u'OpenStack'], 1),
|
||||
]
|
||||
|
||||
# -- Options for HTML output --------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. Major themes that come with
|
||||
|
||||
@@ -318,3 +318,33 @@ pick any database system you prefer.
|
||||
The original implementation has been based on MongoDB but you can create your
|
||||
own storage driver using whatever technology you want.
|
||||
For more information : https://wiki.openstack.org/wiki/Gnocchi
|
||||
|
||||
|
||||
Workers
|
||||
=======
|
||||
|
||||
You can define a number of workers for the Decision Engine and the Applier.
|
||||
|
||||
If you want to create and run more audits simultaneously, you have to raise
|
||||
the number of workers used by the Decision Engine::
|
||||
|
||||
[watcher_decision_engine]
|
||||
|
||||
...
|
||||
|
||||
# The maximum number of threads that can be used to execute strategies
|
||||
# (integer value)
|
||||
#max_workers = 2
|
||||
|
||||
|
||||
If you want to execute simultaneously more recommended action plans, you
|
||||
have to raise the number of workers used by the Applier::
|
||||
|
||||
[watcher_applier]
|
||||
|
||||
...
|
||||
|
||||
# Number of workers for applier, default value is 1. (integer value)
|
||||
# Minimum value: 1
|
||||
#workers = 1
|
||||
|
||||
|
||||
@@ -6,21 +6,30 @@
|
||||
|
||||
.. _user-guide:
|
||||
|
||||
=================================
|
||||
Welcome to the Watcher User Guide
|
||||
=================================
|
||||
==================
|
||||
Watcher User Guide
|
||||
==================
|
||||
|
||||
See the
|
||||
`architecture page <https://factory.b-com.com/www/watcher/doc/watcher/architecture.html>`_
|
||||
for an architectural overview of the different components of Watcher and how
|
||||
they fit together.
|
||||
|
||||
In the `architecture <https://wiki.openstack.org/wiki/WatcherArchitecture>`_
|
||||
you got information about how it works.
|
||||
In this guide we're going to take you through the fundamentals of using
|
||||
Watcher.
|
||||
|
||||
The following diagram shows the main interactions between the
|
||||
:ref:`Administrator <administrator_definition>` and the Watcher system:
|
||||
|
||||
.. image:: ../images/sequence_overview_watcher_usage.png
|
||||
:width: 100%
|
||||
|
||||
|
||||
Getting started with Watcher
|
||||
----------------------------
|
||||
This guide assumes you have a working installation of Watcher. If you get
|
||||
"*watcher: command not found*" you may have to verify your installation.
|
||||
Please refer to the :doc:`installation guide <installation>`.
|
||||
Please refer to the `installation guide`_.
|
||||
In order to use Watcher, you have to configure your credentials suitable for
|
||||
watcher command-line tools.
|
||||
If you need help on a specific command, you can use:
|
||||
@@ -29,6 +38,8 @@ If you need help on a specific command, you can use:
|
||||
|
||||
$ watcher help COMMAND
|
||||
|
||||
.. _`installation guide`: https://factory.b-com.com/www/watcher/doc/python-watcherclient
|
||||
|
||||
Seeing what the Watcher CLI can do ?
|
||||
------------------------------------
|
||||
We can see all of the commands available with Watcher CLI by running the
|
||||
@@ -90,7 +101,7 @@ configuration file.
|
||||
$ watcher action-plan-list --audit <the_audit_uuid>
|
||||
|
||||
- Have a look on the list of optimization :ref:`actions <action_definition>`
|
||||
contained in this new :ref:`action plan <action_plan_definition>`:
|
||||
contained in this new :ref:`action plan <action_plan_definition>`:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
@@ -119,3 +130,4 @@ You can also obtain more detailed information about a specific action:
|
||||
.. code:: bash
|
||||
|
||||
$ watcher action-show <the_action_uuid>
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
|
||||
.. _contributing:
|
||||
|
||||
======================
|
||||
=======================
|
||||
Contributing to Watcher
|
||||
======================
|
||||
=======================
|
||||
|
||||
If you're interested in contributing to the Watcher project,
|
||||
the following will help get you started.
|
||||
@@ -43,7 +43,7 @@ notifications of important events.
|
||||
|
||||
|
||||
Project Hosting Details
|
||||
-------------------------
|
||||
-----------------------
|
||||
|
||||
Bug tracker
|
||||
http://launchpad.net/watcher
|
||||
|
||||
133
doc/source/dev/devstack.rst
Normal file
@@ -0,0 +1,133 @@
|
||||
..
|
||||
Except where otherwise noted, this document is licensed under Creative
|
||||
Commons Attribution 3.0 License. You can view the license at:
|
||||
|
||||
https://creativecommons.org/licenses/by/3.0/
|
||||
|
||||
=============================================
|
||||
Set up a development environment via DevStack
|
||||
=============================================
|
||||
|
||||
Watcher is currently able to optimize compute resources - specifically Nova
|
||||
compute hosts - via operations such as live migrations. In order for you to
|
||||
fully be able to exercise what Watcher can do, it is necessary to have a
|
||||
multinode environment to use. If you have no experience with DevStack, you
|
||||
should check out the `DevStack documentation`_ and be comfortable with the
|
||||
basics of DevStack before attempting to get a multinode DevStack setup with
|
||||
the Watcher plugin.
|
||||
|
||||
You can set up the Watcher services quickly and easily using a Watcher
|
||||
DevStack plugin. See `PluginModelDocs`_ for information on DevStack's plugin
|
||||
model.
|
||||
|
||||
.. _DevStack documentation: http://docs.openstack.org/developer/devstack/
|
||||
.. _PluginModelDocs: http://docs.openstack.org/developer/devstack/plugins.html
|
||||
|
||||
It is recommended that you build off of the provided example local.conf files
|
||||
(`local.conf.controller`_, `local.conf.compute`_). You'll likely want to
|
||||
configure something to obtain metrics, such as Ceilometer. Ceilometer is used
|
||||
in the example local.conf files.
|
||||
|
||||
To configure the Watcher services with DevStack, add the following to the
|
||||
`[[local|localrc]]` section of your controller's `local.conf` to enable the
|
||||
Watcher plugin::
|
||||
|
||||
enable_plugin watcher git://git.openstack.org/openstack/watcher
|
||||
|
||||
Then run devstack normally::
|
||||
|
||||
cd /opt/stack/devstack
|
||||
./stack.sh
|
||||
|
||||
.. _local.conf.controller: https://github.com/openstack/watcher/tree/master/devstack/local.conf.controller
|
||||
.. _local.conf.compute: https://github.com/openstack/watcher/tree/master/devstack/local.conf.compute
|
||||
|
||||
Multi-Node DevStack Environment
|
||||
===============================
|
||||
|
||||
Since deploying Watcher with only a single compute node is not very useful, a
|
||||
few tips are given here for enabling a multi-node environment with live
|
||||
migration.
|
||||
|
||||
Configuring NFS Server
|
||||
----------------------
|
||||
|
||||
If you would like to use live migration for shared storage, then the controller
|
||||
can serve as the NFS server if needed::
|
||||
|
||||
sudo apt-get install nfs-kernel-server
|
||||
sudo mkdir -p /nfs/instances
|
||||
sudo chown stack:stack /nfs/instances
|
||||
|
||||
Add an entry to `/etc/exports` with the appropriate gateway and netmask
|
||||
information::
|
||||
|
||||
/nfs/instances <gateway>/<netmask>(rw,fsid=0,insecure,no_subtree_check,async,no_root_squash)
|
||||
|
||||
Export the NFS directories::
|
||||
|
||||
sudo exportfs -ra
|
||||
|
||||
Make sure the NFS server is running::
|
||||
|
||||
sudo service nfs-kernel-server status
|
||||
|
||||
If the server is not running, then start it::
|
||||
|
||||
sudo service nfs-kernel-server start
|
||||
|
||||
Configuring NFS on Compute Node
|
||||
-------------------------------
|
||||
|
||||
Each compute node needs to use the NFS server to hold the instance data::
|
||||
|
||||
sudo apt-get install rpcbind nfs-common
|
||||
mkdir -p /opt/stack/data/instances
|
||||
sudo mount <nfs-server-ip>:/nfs/instances /opt/stack/data/instances
|
||||
|
||||
If you would like to have the NFS directory automatically mounted on reboot,
|
||||
then add the following to `/etc/fstab`::
|
||||
|
||||
<nfs-server-ip>:/nfs/instances /opt/stack/data/instances nfs auto 0 0
|
||||
|
||||
Edit `/etc/libvirt/libvirtd.conf` to make sure the following values are set::
|
||||
|
||||
listen_tls = 0
|
||||
listen_tcp = 1
|
||||
auth_tcp = "none"
|
||||
|
||||
Edit `/etc/default/libvirt-bin`::
|
||||
|
||||
libvirt_opts="-d -l"
|
||||
|
||||
Restart the libvirt service::
|
||||
|
||||
sudo service libvirt-bin restart
|
||||
|
||||
|
||||
Setting up SSH keys between compute nodes to enable live migration
|
||||
------------------------------------------------------------------
|
||||
|
||||
In order for live migration to work, SSH keys need to be exchanged between
|
||||
each compute node:
|
||||
|
||||
1. The SOURCE root user's public RSA key (likely in /root/.ssh/id_rsa.pub)
|
||||
needs to be in the DESTINATION stack user's authorized_keys file
|
||||
(~stack/.ssh/authorized_keys). This can be accomplished by manually
|
||||
copying the contents from the file on the SOURCE to the DESTINATION. If
|
||||
you have a password configured for the stack user, then you can use the
|
||||
following command to accomplish the same thing::
|
||||
|
||||
ssh-copy-id -i /root/.ssh/id_rsa.pub stack@DESTINATION
|
||||
|
||||
2. The DESTINATION host's public ECDSA key (/etc/ssh/ssh_host_ecdsa_key.pub)
|
||||
needs to be in the SOURCE root user's known_hosts file
|
||||
(/root/.ssh/known_hosts). This can be accomplished by running the
|
||||
following on the SOURCE machine (hostname must be used)::
|
||||
|
||||
ssh-keyscan -H DEST_HOSTNAME | sudo tee -a /root/.ssh/known_hosts
|
||||
|
||||
In essence, this means that every compute node's root user's public RSA key
|
||||
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.
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
https://creativecommons.org/licenses/by/3.0/
|
||||
|
||||
============================================
|
||||
Setting up a Watcher development environment
|
||||
============================================
|
||||
=========================================
|
||||
Set up a development environment manually
|
||||
=========================================
|
||||
|
||||
This document describes getting the source from watcher `Git repository`_
|
||||
for development purposes.
|
||||
@@ -75,17 +75,13 @@ extension, PyPi) cannot satisfy. These dependencies should be installed
|
||||
prior to using `pip`, and the installation method may vary depending on
|
||||
your platform.
|
||||
|
||||
* Ubuntu 14.04:
|
||||
* Ubuntu 14.04::
|
||||
|
||||
.. code-block:: bash
|
||||
$ sudo apt-get install python-dev libssl-dev libmysqlclient-dev libffi-dev
|
||||
|
||||
$ sudo apt-get install python-dev libssl-dev libmysqlclient-dev libffi-dev
|
||||
* Fedora 19+::
|
||||
|
||||
* Fedora 19+:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ sudo yum install openssl-devel libffi-devel mysql-devel
|
||||
$ sudo yum install openssl-devel libffi-devel mysql-devel
|
||||
|
||||
|
||||
PyPi Packages and VirtualEnv
|
||||
@@ -287,4 +283,3 @@ template files to easily play with Watcher services within a minimal OpenStack
|
||||
isolated environment (Identity, Message Bus, SQL database, Horizon, ...).
|
||||
|
||||
.. _`watcher-tools`: https://github.com/b-com/watcher-tools
|
||||
|
||||
|
||||
@@ -4,22 +4,20 @@
|
||||
|
||||
https://creativecommons.org/licenses/by/3.0/
|
||||
|
||||
===============
|
||||
Watcher plugins
|
||||
===============
|
||||
=================================
|
||||
Build a new optimization strategy
|
||||
=================================
|
||||
|
||||
Writing a Watcher Decision Engine plugin
|
||||
========================================
|
||||
|
||||
Watcher has an external :ref:`strategy <strategy_definition>` plugin interface
|
||||
which gives anyone the ability to integrate an external :ref:`strategy
|
||||
<strategy_definition>` in order to make use of placement algorithms.
|
||||
Watcher Decision Engine has an external :ref:`strategy <strategy_definition>`
|
||||
plugin interface which gives anyone the ability to integrate an external
|
||||
:ref:`strategy <strategy_definition>` in order to make use of placement
|
||||
algorithms.
|
||||
|
||||
This section gives some guidelines on how to implement and integrate custom
|
||||
Stategies with Watcher.
|
||||
|
||||
Pre-requisites
|
||||
--------------
|
||||
==============
|
||||
|
||||
Before using any strategy, you should make sure you have your Telemetry service
|
||||
configured so that it would provide you all the metrics you need to be able to
|
||||
@@ -27,7 +25,7 @@ use your strategy.
|
||||
|
||||
|
||||
Creating a new plugin
|
||||
---------------------
|
||||
=====================
|
||||
|
||||
First of all you have to:
|
||||
|
||||
@@ -65,20 +63,21 @@ your ``__init__`` method.
|
||||
|
||||
|
||||
Abstract Plugin Class
|
||||
---------------------
|
||||
=====================
|
||||
|
||||
Here below is the abstract ``BaseStrategy`` class that every single strategy
|
||||
should implement:
|
||||
|
||||
.. automodule:: watcher.decision_engine.strategy.base
|
||||
.. automodule:: watcher.decision_engine.strategy.strategies.base
|
||||
:noindex:
|
||||
|
||||
.. autoclass:: BaseStrategy
|
||||
:members:
|
||||
:noindex:
|
||||
|
||||
|
||||
Add a new entry point
|
||||
---------------------
|
||||
=====================
|
||||
|
||||
In order for the Watcher Decision Engine to load your new strategy, the
|
||||
strategy must be registered as a named entry point under the
|
||||
@@ -102,7 +101,7 @@ have a look at the :py:class:`BasicConsolidation` class.
|
||||
.. _pbr: http://docs.openstack.org/developer/pbr/
|
||||
|
||||
Using strategy plugins
|
||||
----------------------
|
||||
======================
|
||||
|
||||
The Watcher Decision Engine service will automatically discover any installed
|
||||
plugins when it is run. If a Python package containing a custom plugin is
|
||||
@@ -127,18 +126,32 @@ Telemetry service. In such a case, please do make sure that you first
|
||||
check/configure the latter so your new strategy can be fully functional.
|
||||
|
||||
Querying metrics
|
||||
~~~~~~~~~~~~~~~~
|
||||
----------------
|
||||
|
||||
The metrics available depend on the hypervisors that OpenStack manages on
|
||||
the specific implementation. You can find the metrics available per hypervisor
|
||||
and OpenStack release on the OpenStack site.
|
||||
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.
|
||||
|
||||
There are different possible ways to obtain usage metrics in Watcher, you can
|
||||
use the default Ceilometer API or our Helper.
|
||||
The Helper attempted to make the Ceilometer API more reusable and easy to use.
|
||||
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.
|
||||
|
||||
|
||||
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/metrics_engine/cluster_history/ceilometer.py#L31
|
||||
.. _`Ceilometer developer guide`: http://docs.openstack.org/developer/ceilometer/architecture.html#storing-the-data
|
||||
.. _`here`: http://docs.openstack.org/developer/ceilometer/install/dbreco.html#choosing-a-database-backend
|
||||
.. _`plugin`: http://docs.openstack.org/developer/ceilometer/plugins.html
|
||||
.. _`Ceilosca`: https://github.com/openstack/monasca-ceilometer/blob/master/ceilosca/ceilometer/storage/impl_monasca.py
|
||||
|
||||
Read usage metrics using the Python binding
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
-------------------------------------------
|
||||
|
||||
You can find the information about the Ceilometer Python binding on the
|
||||
OpenStack `ceilometer client python API documentation
|
||||
@@ -160,7 +173,7 @@ Using that you can now query the values for that specific metric:
|
||||
value_cpu = cclient.samples.list(meter_name='cpu_util', limit=10, q=query)
|
||||
|
||||
Read usage metrics using the Watcher Cluster History Helper
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
-----------------------------------------------------------
|
||||
|
||||
Here below is the abstract ``BaseClusterHistory`` class of the Helper.
|
||||
|
||||
@@ -169,6 +182,7 @@ Here below is the abstract ``BaseClusterHistory`` class of the Helper.
|
||||
|
||||
.. autoclass:: BaseClusterHistory
|
||||
:members:
|
||||
:noindex:
|
||||
|
||||
|
||||
The following snippet code shows how to create a Cluster History class:
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
https://creativecommons.org/licenses/by/3.0/
|
||||
|
||||
==========
|
||||
Glossary
|
||||
==========
|
||||
========
|
||||
Glossary
|
||||
========
|
||||
|
||||
.. glossary::
|
||||
:sorted:
|
||||
@@ -20,97 +20,14 @@ They are sorted in alphabetical order.
|
||||
Action
|
||||
======
|
||||
|
||||
An :ref:`Action <action_definition>` is what enables Watcher to transform the
|
||||
current state of a :ref:`Cluster <cluster_definition>` after an
|
||||
:ref:`Audit <audit_definition>`.
|
||||
|
||||
An :ref:`Action <action_definition>` is an atomic task which changes the
|
||||
current state of a target :ref:`Managed resource <managed_resource_definition>`
|
||||
of the OpenStack :ref:`Cluster <cluster_definition>` such as:
|
||||
|
||||
- Live migration of an instance from one compute node to another compute
|
||||
node with Nova
|
||||
- Changing the power level of a compute node (ACPI level, ...)
|
||||
- Changing the current state of an hypervisor (enable or disable) with Nova
|
||||
|
||||
In most cases, an :ref:`Action <action_definition>` triggers some concrete
|
||||
commands on an existing OpenStack module (Nova, Neutron, Cinder, Ironic, etc.)
|
||||
via a :ref:`Primitive <primitive_definition>`.
|
||||
|
||||
An :ref:`Action <action_definition>` has a life-cycle and its current state may
|
||||
be one of the following:
|
||||
|
||||
- **PENDING** : the :ref:`Action <action_definition>` has not been executed
|
||||
yet by the :ref:`Watcher Applier <watcher_applier_definition>`
|
||||
- **ONGOING** : the :ref:`Action <action_definition>` is currently being
|
||||
processed by the :ref:`Watcher Applier <watcher_applier_definition>`
|
||||
- **SUCCEEDED** : the :ref:`Action <action_definition>` has been executed
|
||||
successfully
|
||||
- **FAILED** : an error occured while trying to execute the
|
||||
:ref:`Action <action_definition>`
|
||||
- **DELETED** : the :ref:`Action <action_definition>` is still stored in the
|
||||
:ref:`Watcher database <watcher_database_definition>` but is not returned
|
||||
any more through the Watcher APIs.
|
||||
- **CANCELLED** : the :ref:`Action <action_definition>` was in **PENDING** or
|
||||
**ONGOING** state and was cancelled by the
|
||||
:ref:`Administrator <administrator_definition>`
|
||||
.. watcher-term:: watcher.api.controllers.v1.action
|
||||
|
||||
.. _action_plan_definition:
|
||||
|
||||
Action Plan
|
||||
===========
|
||||
|
||||
An :ref:`Action Plan <action_plan_definition>` is a flow of
|
||||
:ref:`Actions <action_definition>` that should be executed in order to satisfy
|
||||
a given :ref:`Goal <goal_definition>`.
|
||||
|
||||
An :ref:`Action Plan <action_plan_definition>` is generated by Watcher when an
|
||||
:ref:`Audit <audit_definition>` is successful which implies that the
|
||||
:ref:`Strategy <strategy_definition>`
|
||||
which was used has found a :ref:`Solution <solution_definition>` to achieve the
|
||||
:ref:`Goal <goal_definition>` of this :ref:`Audit <audit_definition>`.
|
||||
|
||||
In the default implementation of Watcher, an
|
||||
:ref:`Action Plan <action_plan_definition>`
|
||||
is only composed of successive :ref:`Actions <action_definition>`
|
||||
(i.e., a Workflow of :ref:`Actions <action_definition>` belonging to a unique
|
||||
branch).
|
||||
|
||||
However, Watcher provides abstract interfaces for many of its components,
|
||||
allowing other implementations to generate and handle more complex
|
||||
:ref:`Action Plan(s) <action_plan_definition>`
|
||||
composed of two types of Action Item(s):
|
||||
|
||||
- simple :ref:`Actions <action_definition>`: atomic tasks, which means it
|
||||
can not be split into smaller tasks or commands from an OpenStack point of
|
||||
view.
|
||||
- composite Actions: which are composed of several simple
|
||||
:ref:`Actions <action_definition>`
|
||||
ordered in sequential and/or parallel flows.
|
||||
|
||||
An :ref:`Action Plan <action_plan_definition>` may be described using
|
||||
standard workflow model description formats such as
|
||||
`Business Process Model and Notation 2.0 (BPMN 2.0) <http://www.omg.org/spec/BPMN/2.0/>`_
|
||||
or `Unified Modeling Language (UML) <http://www.uml.org/>`_.
|
||||
|
||||
An :ref:`Action Plan <action_plan_definition>` has a life-cycle and its current
|
||||
state may be one of the following:
|
||||
|
||||
- **RECOMMENDED** : the :ref:`Action Plan <action_plan_definition>` is waiting
|
||||
for a validation from the :ref:`Administrator <administrator_definition>`
|
||||
- **ONGOING** : the :ref:`Action Plan <action_plan_definition>` is currently
|
||||
being processed by the :ref:`Watcher Applier <watcher_applier_definition>`
|
||||
- **SUCCEEDED** : the :ref:`Action Plan <action_plan_definition>` has been
|
||||
executed successfully (i.e. all :ref:`Actions <action_definition>` that it
|
||||
contains have been executed successfully)
|
||||
- **FAILED** : an error occured while executing the
|
||||
:ref:`Action Plan <action_plan_definition>`
|
||||
- **DELETED** : the :ref:`Action Plan <action_plan_definition>` is still
|
||||
stored in the :ref:`Watcher database <watcher_database_definition>` but is
|
||||
not returned any more through the Watcher APIs.
|
||||
- **CANCELLED** : the :ref:`Action Plan <action_plan_definition>` was in
|
||||
**PENDING** or **ONGOING** state and was cancelled by the
|
||||
:ref:`Administrator <administrator_definition>`
|
||||
.. watcher-term:: watcher.api.controllers.v1.action_plan
|
||||
|
||||
.. _administrator_definition:
|
||||
|
||||
@@ -144,73 +61,14 @@ any Watcher configuration files and to restart Watcher services.
|
||||
Audit
|
||||
=====
|
||||
|
||||
In the Watcher system, an :ref:`Audit <audit_definition>` is a request for
|
||||
optimizing a :ref:`Cluster <cluster_definition>`.
|
||||
|
||||
The optimization is done in order to satisfy one :ref:`Goal <goal_definition>`
|
||||
on a given :ref:`Cluster <cluster_definition>`.
|
||||
|
||||
For each :ref:`Audit <audit_definition>`, the Watcher system generates an
|
||||
:ref:`Action Plan <action_plan_definition>`.
|
||||
|
||||
An :ref:`Audit <audit_definition>` has a life-cycle and its current state may
|
||||
be one of the following:
|
||||
|
||||
- **PENDING** : a request for an :ref:`Audit <audit_definition>` has been
|
||||
submitted (either manually by the
|
||||
:ref:`Administrator <administrator_definition>` or automatically via some
|
||||
event handling mechanism) and is in the queue for being processed by the
|
||||
:ref:`Watcher Decision Engine <watcher_decision_engine_definition>`
|
||||
- **ONGOING** : the :ref:`Audit <audit_definition>` is currently being
|
||||
processed by the
|
||||
:ref:`Watcher Decision Engine <watcher_decision_engine_definition>`
|
||||
- **SUCCEEDED** : the :ref:`Audit <audit_definition>` has been executed
|
||||
successfully (note that it may not necessarily produce a
|
||||
:ref:`Solution <solution_definition>`).
|
||||
- **FAILED** : an error occured while executing the
|
||||
:ref:`Audit <audit_definition>`
|
||||
- **DELETED** : the :ref:`Audit <audit_definition>` is still stored in the
|
||||
:ref:`Watcher database <watcher_database_definition>` but is not returned
|
||||
any more through the Watcher APIs.
|
||||
- **CANCELLED** : the :ref:`Audit <audit_definition>` was in **PENDING** or
|
||||
**ONGOING** state and was cancelled by the
|
||||
:ref:`Administrator <administrator_definition>`
|
||||
.. watcher-term:: watcher.api.controllers.v1.audit
|
||||
|
||||
.. _audit_template_definition:
|
||||
|
||||
Audit Template
|
||||
==============
|
||||
|
||||
An :ref:`Audit <audit_definition>` may be launched several times with the same
|
||||
settings (:ref:`Goal <goal_definition>`, thresholds, ...). Therefore it makes
|
||||
sense to save those settings in some sort of Audit preset object, which is
|
||||
known as an :ref:`Audit Template <audit_template_definition>`.
|
||||
|
||||
An :ref:`Audit Template <audit_template_definition>` contains at least the
|
||||
:ref:`Goal <goal_definition>` of the :ref:`Audit <audit_definition>`.
|
||||
|
||||
It may also contain some error handling settings indicating whether:
|
||||
|
||||
- :ref:`Watcher Applier <watcher_applier_definition>` stops the
|
||||
entire operation
|
||||
- :ref:`Watcher Applier <watcher_applier_definition>` performs a rollback
|
||||
|
||||
and how many retries should be attempted before failure occurs (also the latter
|
||||
can be complex: for example the scenario in which there are many first-time
|
||||
failures on ultimately successful :ref:`Actions <action_definition>`).
|
||||
|
||||
Moreover, an :ref:`Audit Template <audit_template_definition>` may contain some
|
||||
settings related to the level of automation for the
|
||||
:ref:`Action Plan <action_plan_definition>` that will be generated by the
|
||||
:ref:`Audit <audit_definition>`.
|
||||
A flag will indicate whether the :ref:`Action Plan <action_plan_definition>`
|
||||
will be launched automatically or will need a manual confirmation from the
|
||||
:ref:`Administrator <administrator_definition>`.
|
||||
|
||||
Last but not least, an :ref:`Audit Template <audit_template_definition>` may
|
||||
contain a list of extra parameters related to the
|
||||
:ref:`Strategy <strategy_definition>` configuration. These parameters can be
|
||||
provided as a list of key-value pairs.
|
||||
.. watcher-term:: watcher.api.controllers.v1.audit_template
|
||||
|
||||
.. _availability_zone_definition:
|
||||
|
||||
@@ -241,136 +99,14 @@ The :ref:`Cluster <cluster_definition>` may be divided in one or several
|
||||
Cluster Data Model
|
||||
==================
|
||||
|
||||
A :ref:`Cluster Data Model <cluster_data_model_definition>` is a logical
|
||||
representation of the current state and topology of the
|
||||
:ref:`Cluster <cluster_definition>`
|
||||
:ref:`Managed resources <managed_resource_definition>`.
|
||||
|
||||
It is represented as a set of
|
||||
:ref:`Managed resources <managed_resource_definition>`
|
||||
(which may be a simple tree or a flat list of key-value pairs)
|
||||
which enables Watcher :ref:`Strategies <strategy_definition>` to know the
|
||||
current relationships between the different
|
||||
:ref:`resources <managed_resource_definition>`) of the
|
||||
:ref:`Cluster <cluster_definition>` during an :ref:`Audit <audit_definition>`
|
||||
and enables the :ref:`Strategy <strategy_definition>` to request information
|
||||
such as:
|
||||
|
||||
- What compute nodes are in a given
|
||||
:ref:`Availability Zone <availability_zone_definition>`
|
||||
or a given :ref:`Host Aggregate <host_aggregates_definition>` ?
|
||||
- What :ref:`Instances <instance_definition>` are hosted on a given compute
|
||||
node ?
|
||||
- What is the current load of a compute node ?
|
||||
- What is the current free memory of a compute node ?
|
||||
- What is the network link between two compute nodes ?
|
||||
- What is the available bandwidth on a given network link ?
|
||||
- What is the current space available on a given virtual disk of a given
|
||||
:ref:`Instance <instance_definition>` ?
|
||||
- What is the current state of a given :ref:`Instance <instance_definition>`?
|
||||
- ...
|
||||
|
||||
In a word, this data model enables the :ref:`Strategy <strategy_definition>`
|
||||
to know:
|
||||
|
||||
- the current topology of the :ref:`Cluster <cluster_definition>`
|
||||
- the current capacity for each
|
||||
:ref:`Managed resource <managed_resource_definition>`
|
||||
- the current amount of used/free space for each
|
||||
:ref:`Managed resource <managed_resource_definition>`
|
||||
- the current state of each
|
||||
:ref:`Managed resources <managed_resource_definition>`
|
||||
|
||||
In the Watcher project, we aim at providing a generic and very basic
|
||||
:ref:`Cluster Data Model <cluster_data_model_definition>` for each
|
||||
:ref:`Goal <goal_definition>`, usable in the associated
|
||||
:ref:`Strategies <strategy_definition>` through some helper classes in order
|
||||
to:
|
||||
|
||||
- simplify the development of a new
|
||||
:ref:`Strategy <strategy_definition>` for a given
|
||||
:ref:`Goal <goal_definition>` when there already are some existing
|
||||
:ref:`Strategies <strategy_definition>` associated to the same
|
||||
:ref:`Goal <goal_definition>`
|
||||
- avoid duplicating the same code in several
|
||||
:ref:`Strategies <strategy_definition>` associated to the same
|
||||
:ref:`Goal <goal_definition>`
|
||||
- have a better consistency between the different
|
||||
:ref:`Strategies <strategy_definition>` for a given
|
||||
:ref:`Goal <goal_definition>`
|
||||
- avoid any strong coupling with any external
|
||||
:ref:`Cluster Data Model <cluster_data_model_definition>`
|
||||
(the proposed data model acts as a pivot data model)
|
||||
|
||||
There may be various
|
||||
:ref:`generic and basic Cluster Data Models <cluster_data_model_definition>`
|
||||
proposed in Watcher helpers, each of them being adapted to achieving a given
|
||||
:ref:`Goal <goal_definition>`:
|
||||
|
||||
- For example, for a
|
||||
:ref:`Goal <goal_definition>` which aims at optimizing the network
|
||||
:ref:`resources <managed_resource_definition>` the
|
||||
:ref:`Strategy <strategy_definition>` may need to know which
|
||||
:ref:`resources <managed_resource_definition>` are communicating together.
|
||||
- Whereas for a :ref:`Goal <goal_definition>` which aims at optimizing thermal
|
||||
and power conditions, the :ref:`Strategy <strategy_definition>` may need to
|
||||
know the location of each compute node in the racks and the location of each
|
||||
rack in the room.
|
||||
|
||||
Note however that a developer can use his/her own
|
||||
:ref:`Cluster Data Model <cluster_data_model_definition>` if the proposed data
|
||||
model 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>`.
|
||||
For example, a developer could rely on the Nova Data Model to optimize some
|
||||
compute resources.
|
||||
|
||||
The :ref:`Cluster Data Model <cluster_data_model_definition>` may be persisted
|
||||
in any appropriate storage system (SQL database, NoSQL database, JSON file,
|
||||
XML File, In Memory Database, ...).
|
||||
.. watcher-term:: watcher.metrics_engine.cluster_model_collector.api
|
||||
|
||||
.. _cluster_history_definition:
|
||||
|
||||
Cluster History
|
||||
===============
|
||||
|
||||
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,...).
|
||||
.. watcher-term:: watcher.metrics_engine.cluster_history.api
|
||||
|
||||
.. _controller_node_definition:
|
||||
|
||||
@@ -419,20 +155,7 @@ them, or at least reported to them.
|
||||
Goal
|
||||
====
|
||||
|
||||
A :ref:`Goal <goal_definition>` is a human readable, observable and measurable
|
||||
end result having one objective to be achieved.
|
||||
|
||||
Here are some examples of :ref:`Goals <goal_definition>`:
|
||||
|
||||
- minimize the energy consumption
|
||||
- minimize the number of compute nodes (consolidation)
|
||||
- balance the workload among compute nodes
|
||||
- minimize the license cost (some softwares have a licensing model which is
|
||||
based on the number of sockets or cores where the software is deployed)
|
||||
- find the most appropriate moment for a planned maintenance on a
|
||||
given group of host (which may be an entire availability zone):
|
||||
power supply replacement, cooling system replacement, hardware
|
||||
modification, ...
|
||||
.. watcher-term:: watcher.api.controllers.v1.goal
|
||||
|
||||
|
||||
.. _host_aggregates_definition:
|
||||
@@ -539,23 +262,6 @@ specific domain.
|
||||
Please, read `the official OpenStack definition of a Project <http://docs.openstack.org/glossary/content/glossary.html>`_.
|
||||
|
||||
|
||||
.. _primitive_definition
|
||||
|
||||
Primitive
|
||||
=========
|
||||
|
||||
A :ref:`Primitive <primitive_definition>` is the component that carries out a
|
||||
certain type of atomic :ref:`Actions <action_definition>` on a given
|
||||
:ref:`Managed resource <managed_resource_definition>` (nova, swift, neutron,
|
||||
glance,..). A :ref:`Primitive <primitive_definition>` is a part of the
|
||||
:ref:`Watcher Applier <watcher_applier_definition>` module.
|
||||
|
||||
For example, there can be a :ref:`Primitive <primitive_definition>` which is
|
||||
responsible for creating a snapshot of a given instance on a Nova compute node.
|
||||
This :ref:`Primitive <primitive_definition>` knows exactly how to send
|
||||
the appropriate commands to Nova for this type of
|
||||
:ref:`Actions <action_definition>`.
|
||||
|
||||
.. _sla_definition:
|
||||
|
||||
SLA
|
||||
@@ -610,67 +316,21 @@ which provides a good definition.
|
||||
Solution
|
||||
========
|
||||
|
||||
A :ref:`Solution <solution_definition>` is a set of
|
||||
:ref:`Actions <action_definition>` generated by a
|
||||
:ref:`Strategy <strategy_definition>` (i.e., an algorithm) in order to achieve
|
||||
the :ref:`Goal <goal_definition>` of an :ref:`Audit <audit_definition>`.
|
||||
|
||||
A :ref:`Solution <solution_definition>` is different from an
|
||||
:ref:`Action Plan <action_plan_definition>` because it contains the
|
||||
non-scheduled list of :ref:`Actions <action_definition>` which is produced by a
|
||||
:ref:`Strategy <strategy_definition>`. In other words, the list of Actions in
|
||||
a :ref:`Solution <solution_definition>` has not yet been re-ordered by the
|
||||
:ref:`Watcher Planner <watcher_planner_definition>`.
|
||||
|
||||
Note that some algorithms (i.e. :ref:`Strategies <strategy_definition>`) may
|
||||
generate several :ref:`Solutions <solution_definition>`. This gives rise to the
|
||||
problem of determining which :ref:`Solution <solution_definition>` should be
|
||||
applied.
|
||||
|
||||
Two approaches to dealing with this can be envisaged:
|
||||
|
||||
- **fully automated mode**: only the :ref:`Solution <solution_definition>`
|
||||
with the highest ranking (i.e., the highest
|
||||
:ref:`Optimization Efficiency <efficiency_definition>`)
|
||||
will be sent to the :ref:`Watcher Planner <watcher_planner_definition>` and
|
||||
translated into concrete :ref:`Actions <action_definition>`.
|
||||
- **manual mode**: several :ref:`Solutions <solution_definition>` are proposed
|
||||
to the :ref:`Administrator <administrator_definition>` with a detailed
|
||||
measurement of the estimated
|
||||
:ref:`Optimization Efficiency <efficiency_definition>` and he/she decides
|
||||
which one will be launched.
|
||||
.. watcher-term:: watcher.decision_engine.solution.base
|
||||
|
||||
.. _strategy_definition:
|
||||
|
||||
Strategy
|
||||
========
|
||||
|
||||
A :ref:`Strategy <strategy_definition>` is an algorithm implementation which is
|
||||
able to find a :ref:`Solution <solution_definition>` for a given
|
||||
:ref:`Goal <goal_definition>`.
|
||||
|
||||
There may be several potential strategies which are able to achieve the same
|
||||
:ref:`Goal <goal_definition>`. This is why it is possible to configure which
|
||||
specific :ref:`Strategy <strategy_definition>` should be used for each
|
||||
:ref:`Goal <goal_definition>`.
|
||||
|
||||
Some strategies may provide better optimization results but may take more time
|
||||
to find an optimal :ref:`Solution <solution_definition>`.
|
||||
|
||||
When a new :ref:`Goal <goal_definition>` is added to the Watcher configuration,
|
||||
at least one default associated :ref:`Strategy <strategy_definition>` should be
|
||||
provided as well.
|
||||
.. watcher-term:: watcher.decision_engine.strategy.strategies.base
|
||||
|
||||
.. _watcher_applier_definition:
|
||||
|
||||
Watcher Applier
|
||||
===============
|
||||
|
||||
This component is in charge of executing the
|
||||
:ref:`Action Plan <action_plan_definition>` built by the
|
||||
:ref:`Watcher Decision Engine <watcher_decision_engine_definition>`.
|
||||
|
||||
See :doc:`architecture` for more details on this component.
|
||||
.. watcher-term:: watcher.applier.base
|
||||
|
||||
.. _watcher_database_definition:
|
||||
|
||||
@@ -696,47 +356,12 @@ See :doc:`architecture` for more details on this component.
|
||||
Watcher Decision Engine
|
||||
=======================
|
||||
|
||||
This component is responsible for computing a set of potential optimization
|
||||
:ref:`Actions <action_definition>` in order to fulfill the
|
||||
:ref:`Goal <goal_definition>` of an :ref:`Audit <audit_definition>`.
|
||||
|
||||
It first reads the parameters of the :ref:`Audit <audit_definition>` from the
|
||||
associated :ref:`Audit Template <audit_template_definition>` and knows the
|
||||
:ref:`Goal <goal_definition>` to achieve.
|
||||
|
||||
It then selects the most appropriate :ref:`Strategy <strategy_definition>`
|
||||
depending on how Watcher was configured for this :ref:`Goal <goal_definition>`.
|
||||
|
||||
The :ref:`Strategy <strategy_definition>` is then executed and generates a set
|
||||
of :ref:`Actions <action_definition>` which are scheduled in time by the
|
||||
:ref:`Watcher Planner <watcher_planner_definition>` (i.e., it generates an
|
||||
:ref:`Action Plan <action_plan_definition>`).
|
||||
|
||||
See :doc:`architecture` for more details on this component.
|
||||
.. watcher-term:: watcher.decision_engine.manager
|
||||
|
||||
.. _watcher_planner_definition:
|
||||
|
||||
Watcher Planner
|
||||
===============
|
||||
|
||||
The :ref:`Watcher Planner <watcher_planner_definition>` is part of the
|
||||
:ref:`Watcher Decision Engine <watcher_decision_engine_definition>`.
|
||||
|
||||
This module takes the set of :ref:`Actions <action_definition>` generated by a
|
||||
:ref:`Strategy <strategy_definition>` and builds the design of a workflow which
|
||||
defines how-to schedule in time those different
|
||||
:ref:`Actions <action_definition>` and for each
|
||||
:ref:`Action <action_definition>` what are the prerequisite conditions.
|
||||
|
||||
It is important to schedule :ref:`Actions <action_definition>` in time in order
|
||||
to prevent overload of the :ref:`Cluster <cluster_definition>` while applying
|
||||
the :ref:`Action Plan <action_plan_definition>`. For example, it is important
|
||||
not to migrate too many instances at the same time in order to avoid a network
|
||||
congestion which may decrease the :ref:`SLA <sla_definition>` for
|
||||
:ref:`Customers <customer_definition>`.
|
||||
|
||||
It is also important to schedule :ref:`Actions <action_definition>` in order to
|
||||
avoid security issues such as denial of service on core OpenStack services.
|
||||
|
||||
See :doc:`architecture` for more details on this component.
|
||||
.. watcher-term:: watcher.decision_engine.planner.base
|
||||
|
||||
|
||||
BIN
doc/source/image_src/dia/architecture.dia
Normal file
BIN
doc/source/image_src/dia/functional_data_model.dia
Normal file
16
doc/source/image_src/plantuml/action_plan_state_machine.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
@startuml
|
||||
|
||||
[*] --> RECOMMENDED: The Watcher Planner\ncreates the Action Plan
|
||||
RECOMMENDED --> TRIGGERED: Administrator launches\nthe Action Plan
|
||||
TRIGGERED --> ONGOING: The Watcher Applier receives the request\nto launch the Action Plan
|
||||
ONGOING --> FAILED: Something failed while executing\nthe Action Plan in the Watcher Applier
|
||||
ONGOING --> SUCCEEDED: The Watcher Applier executed\nthe Action Plan successfully
|
||||
FAILED --> DELETED : Administrator removes\nAction Plan
|
||||
SUCCEEDED --> DELETED : Administrator removes\nAction Plan
|
||||
ONGOING --> CANCELLED : Administrator cancels\nAction Plan
|
||||
RECOMMENDED --> CANCELLED : Administrator cancels\nAction Plan
|
||||
TRIGGERED --> CANCELLED : Administrator cancels\nAction Plan
|
||||
CANCELLED --> DELETED
|
||||
DELETED --> [*]
|
||||
|
||||
@enduml
|
||||
14
doc/source/image_src/plantuml/audit_state_machine.txt
Normal file
@@ -0,0 +1,14 @@
|
||||
@startuml
|
||||
|
||||
[*] --> PENDING: Audit requested by Administrator
|
||||
PENDING --> ONGOING: Audit request is received\nby the Watcher Decision Engine
|
||||
ONGOING --> FAILED: Audit fails\n(no solution found, technical error, ...)
|
||||
ONGOING --> SUCCEEDED: The Watcher Decision Engine\ncould find at least one Solution
|
||||
FAILED --> DELETED : Administrator wants to\narchive/delete the Audit
|
||||
SUCCEEDED --> DELETED : Administrator wants to\narchive/delete the Audit
|
||||
PENDING --> CANCELLED : Administrator cancels\nthe Audit
|
||||
ONGOING --> CANCELLED : Administrator cancels\nthe Audit
|
||||
CANCELLED --> DELETED : Administrator wants to\narchive/delete the Audit
|
||||
DELETED --> [*]
|
||||
|
||||
@enduml
|
||||
@@ -0,0 +1,24 @@
|
||||
@startuml
|
||||
|
||||
|
||||
actor Administrator
|
||||
|
||||
Administrator -> "Watcher CLI" : watcher audit-create -a <audit_template_uuid>
|
||||
|
||||
"Watcher CLI" -> "Watcher API" : POST audit(parameters)
|
||||
"Watcher API" -> "Watcher Database" : create new audit in database (status=PENDING)
|
||||
|
||||
"Watcher API" <-- "Watcher Database" : new audit uuid
|
||||
"Watcher CLI" <-- "Watcher API" : return new audit URL
|
||||
|
||||
Administrator <-- "Watcher CLI" : new audit uuid
|
||||
|
||||
"Watcher API" -> "AMQP Bus" : trigger_audit(new_audit.uuid)
|
||||
"AMQP Bus" -> "Watcher Decision Engine" : trigger_audit(new_audit.uuid)
|
||||
|
||||
ref over "Watcher Decision Engine"
|
||||
Trigger audit in the
|
||||
Watcher Decision Engine
|
||||
end ref
|
||||
|
||||
@enduml
|
||||
@@ -0,0 +1,16 @@
|
||||
@startuml
|
||||
|
||||
actor Administrator
|
||||
|
||||
Administrator -> "Watcher CLI" : watcher audit-template-create <name> <goal>
|
||||
"Watcher CLI" -> "Watcher API" : POST audit_template(parameters)
|
||||
|
||||
"Watcher API" -> "Watcher API" : make sure goal exist in configuration
|
||||
"Watcher API" -> "Watcher Database" : create new audit_template in database
|
||||
|
||||
"Watcher API" <-- "Watcher Database" : new audit template uuid
|
||||
"Watcher CLI" <-- "Watcher API" : return new audit template URL in HTTP Location Header
|
||||
Administrator <-- "Watcher CLI" : new audit template uuid
|
||||
|
||||
@enduml
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
@startuml
|
||||
|
||||
actor Administrator
|
||||
|
||||
Administrator -> "Watcher CLI" : watcher action-plan-start <action_plan_uuid>
|
||||
|
||||
"Watcher CLI" -> "Watcher API" : PATCH action_plan(state=TRIGGERED)
|
||||
"Watcher API" -> "Watcher Database" : action_plan.state=TRIGGERED
|
||||
|
||||
"Watcher CLI" <-- "Watcher API" : HTTP 200
|
||||
|
||||
Administrator <-- "Watcher CLI" : OK
|
||||
|
||||
"Watcher API" -> "AMQP Bus" : launch_action_plan(action_plan.uuid)
|
||||
"AMQP Bus" -> "Watcher Applier" : launch_action_plan(action_plan.uuid)
|
||||
|
||||
ref over "Watcher Applier"
|
||||
Launch Action Plan in the
|
||||
Watcher Applier
|
||||
end ref
|
||||
|
||||
@enduml
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
@startuml
|
||||
|
||||
"AMQP Bus" -> "Watcher Applier" : launch_action_plan(action_plan.uuid)
|
||||
"Watcher Applier" -> "Watcher Database" : action_plan.state=ONGOING
|
||||
"Watcher Applier" -[#blue]> "AMQP Bus" : notify action plan state = ONGOING
|
||||
"Watcher Applier" -> "Watcher Database" : get_action_list(action_plan.uuid)
|
||||
"Watcher Applier" <-- "Watcher Database" : actions
|
||||
loop for each action of the action flow
|
||||
create Action
|
||||
"Watcher Applier" -> Action : instantiate Action object with target resource id\n and input parameters
|
||||
"Watcher Applier" -> Action : validate_parameters()
|
||||
"Watcher Applier" <-- Action : OK
|
||||
"Watcher Applier" -[#blue]> "AMQP Bus" : notify action state = ONGOING
|
||||
"Watcher Applier" -> Action : preconditions()
|
||||
"Watcher Applier" <-- Action : OK
|
||||
"Watcher Applier" -> Action : execute()
|
||||
alt action is "migrate instance"
|
||||
Action -> "Nova API" : migrate(instance_id, dest_host_id)
|
||||
Action <-- "Nova API" : OK
|
||||
else action is "disable hypervisor"
|
||||
Action -> "Nova API" : host-update(host_id, maintenance=true)
|
||||
Action <-- "Nova API" : OK
|
||||
end
|
||||
"Watcher Applier" <-- Action : OK
|
||||
"Watcher Applier" -> "Watcher Database" : action.state=SUCCEEDED
|
||||
"Watcher Applier" -[#blue]> "AMQP Bus" : notify action state = SUCCEEDED
|
||||
end
|
||||
"Watcher Applier" -> "Watcher Database" : action_plan.state=SUCCEEDED
|
||||
"Watcher Applier" -[#blue]> "AMQP Bus" : notify action plan state = SUCCEEDED
|
||||
|
||||
@enduml
|
||||
@@ -0,0 +1,37 @@
|
||||
@startuml
|
||||
|
||||
actor Administrator
|
||||
|
||||
== Create some Audit settings ==
|
||||
|
||||
Administrator -> Watcher : create new Audit Template (i.e. Audit settings : goal, scope, deadline,...)
|
||||
Watcher -> Watcher : save Audit Template in database
|
||||
Administrator <-- Watcher : Audit Template UUID
|
||||
|
||||
== Launch a new Audit ==
|
||||
|
||||
Administrator -> Watcher : launch new Audit of the Openstack infrastructure resources\nwith a previously created Audit Template
|
||||
Administrator <-- Watcher : Audit UUID
|
||||
Administrator -> Watcher : get the Audit state
|
||||
Administrator <-- Watcher : ONGOING
|
||||
Watcher -> Watcher : compute a solution to achieve optimization goal
|
||||
Administrator -> Watcher : get the Audit state
|
||||
Administrator <-- Watcher : SUCCEEDED
|
||||
|
||||
== Get the result of the Audit ==
|
||||
|
||||
Administrator -> Watcher : get Action Plan
|
||||
Administrator <-- Watcher : recommended Action Plan and estimated efficacy
|
||||
Administrator -> Administrator : verify the recommended actions\nand evaluate the estimated gain vs aggressiveness of the solution
|
||||
|
||||
== Launch the recommended Action Plan ==
|
||||
|
||||
Administrator -> Watcher : launch the Action Plan
|
||||
Administrator <-- Watcher : Action Plan has been launched
|
||||
Watcher -> Watcher : trigger Actions on Openstack services
|
||||
Administrator -> Watcher : get the Action Plan state
|
||||
Administrator <-- Watcher : ONGOING
|
||||
Administrator -> Watcher : get the Action Plan state
|
||||
Administrator <-- Watcher : SUCCEEDED
|
||||
|
||||
@enduml
|
||||
@@ -0,0 +1,31 @@
|
||||
@startuml
|
||||
|
||||
"AMQP Bus" -> "Watcher Decision Engine" : trigger_audit(new_audit.uuid)
|
||||
"Watcher Decision Engine" -> "Watcher Database" : update audit.state = ONGOING
|
||||
"AMQP Bus" <[#blue]- "Watcher Decision Engine" : notify new audit state = ONGOING
|
||||
"Watcher Decision Engine" -> "Watcher Database" : get audit parameters(goal, ...)
|
||||
"Watcher Decision Engine" <-- "Watcher Database" : audit parameters(goal, ...)
|
||||
create Strategy
|
||||
"Watcher Decision Engine" -[#red]> "Strategy": select appropriate\noptimization strategy
|
||||
loop while enough data to build cluster data model
|
||||
"Watcher Decision Engine" -> "Nova API" : get resource state (host, instance, ...)
|
||||
"Watcher Decision Engine" <-- "Nova API" : resource state
|
||||
end
|
||||
"Watcher Decision Engine" -[#red]> "Watcher Decision Engine": build cluster_data_model
|
||||
"Watcher Decision Engine" -> "Strategy" : execute(cluster_data_model)
|
||||
loop while enough history data for the strategy
|
||||
"Strategy" -> "Ceilometer API": get_aggregated_metrics\n(resource_id,meter_name,period,aggregate_method)
|
||||
"Strategy" <-- "Ceilometer API": aggregated metrics
|
||||
end
|
||||
"Strategy" -> "Strategy" : compute solution to achieve goal
|
||||
"Watcher Decision Engine" <-- "Strategy" : solution = array of actions (i.e. not scheduled yet)
|
||||
create "Watcher Planner"
|
||||
"Watcher Decision Engine" -[#red]> "Watcher Planner": select appropriate actions scheduler (i.e. Planner implementation)
|
||||
"Watcher Decision Engine" -> "Watcher Planner": schedule(audit_id, solution)
|
||||
"Watcher Planner" -> "Watcher Planner": schedule actions according to\nscheduling rules/policies
|
||||
"Watcher Decision Engine" <-- "Watcher Planner": new action_plan
|
||||
"Watcher Decision Engine" -> "Watcher Database" : save new action_plan in database
|
||||
"Watcher Decision Engine" -> "Watcher Database" : update audit.state = SUCCEEDED
|
||||
"AMQP Bus" <[#blue]- "Watcher Decision Engine" : notify new audit state = SUCCEEDED
|
||||
|
||||
@enduml
|
||||
BIN
doc/source/images/action_plan_state_machine.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
@@ -41,36 +41,31 @@
|
||||
</text>
|
||||
</g>
|
||||
<g>
|
||||
<rect style="fill: #ffb400" x="407" y="-0.832" width="162.3" height="89"/>
|
||||
<path style="fill: #ffb400" d="M 407,-0.832 A 10,10 0 0 0 397,9.168 L 407,9.168 z"/>
|
||||
<path style="fill: #ffb400" d="M 579.3,9.168 A 10,10 0 0 0 569.3,-0.832 L 569.3,9.168 z"/>
|
||||
<rect style="fill: #ffb400" x="397" y="9.168" width="182.3" height="69"/>
|
||||
<rect style="fill: #ffb400" x="407" y="-38.25" width="325.386" height="126.418"/>
|
||||
<path style="fill: #ffb400" d="M 407,-38.25 A 10,10 0 0 0 397,-28.25 L 407,-28.25 z"/>
|
||||
<path style="fill: #ffb400" d="M 742.386,-28.25 A 10,10 0 0 0 732.386,-38.25 L 732.386,-28.25 z"/>
|
||||
<rect style="fill: #ffb400" x="397" y="-28.25" width="345.386" height="106.418"/>
|
||||
<path style="fill: #ffb400" d="M 397,78.168 A 10,10 0 0 0 407,88.168 L 407,78.168 z"/>
|
||||
<path style="fill: #ffb400" d="M 569.3,88.168 A 10,10 0 0 0 579.3,78.168 L 569.3,78.168 z"/>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="407" y1="-0.832" x2="569.3" y2="-0.832"/>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="407" y1="88.168" x2="569.3" y2="88.168"/>
|
||||
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 407,-0.832 A 10,10 0 0 0 397,9.168"/>
|
||||
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 579.3,9.168 A 10,10 0 0 0 569.3,-0.832"/>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="397" y1="9.168" x2="397" y2="78.168"/>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="579.3" y1="9.168" x2="579.3" y2="78.168"/>
|
||||
<path style="fill: #ffb400" d="M 732.386,88.168 A 10,10 0 0 0 742.386,78.168 L 732.386,78.168 z"/>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="407" y1="-38.25" x2="732.386" y2="-38.25"/>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="407" y1="88.168" x2="732.386" y2="88.168"/>
|
||||
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 407,-38.25 A 10,10 0 0 0 397,-28.25"/>
|
||||
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 742.386,-28.25 A 10,10 0 0 0 732.386,-38.25"/>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="397" y1="-28.25" x2="397" y2="78.168"/>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="742.386" y1="-28.25" x2="742.386" y2="78.168"/>
|
||||
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 397,78.168 A 10,10 0 0 0 407,88.168"/>
|
||||
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 569.3,88.168 A 10,10 0 0 0 579.3,78.168"/>
|
||||
<text font-size="14.1111" style="fill: #ffffff;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:700" x="488.15" y="39.1291">
|
||||
<tspan x="488.15" y="39.1291">Watcher</tspan>
|
||||
<tspan x="488.15" y="56.768">Decision Engine</tspan>
|
||||
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 732.386,88.168 A 10,10 0 0 0 742.386,78.168"/>
|
||||
<text font-size="14.1111" style="fill: #ffffff;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:700" x="569.693" y="20.4201">
|
||||
<tspan x="569.693" y="20.4201">Watcher</tspan>
|
||||
<tspan x="569.693" y="38.059">Decision Engine</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x1="341.176" y1="21" x2="387.264" y2="21.3451"/>
|
||||
<polygon style="fill: #000000" points="394.764,21.4013 384.727,26.3262 387.264,21.3451 384.802,16.3265 "/>
|
||||
<polygon style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="394.764,21.4013 384.727,26.3262 387.264,21.3451 384.802,16.3265 "/>
|
||||
</g>
|
||||
<g>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke-dasharray: 6; stroke: #000000" x1="349.122" y1="65.5706" x2="387.264" y2="65.8474"/>
|
||||
<polygon style="fill: #000000" points="341.622,65.5162 351.658,60.5889 349.122,65.5706 351.585,70.5886 "/>
|
||||
<polygon style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="341.622,65.5162 351.658,60.5889 349.122,65.5706 351.585,70.5886 "/>
|
||||
<polygon style="fill: #000000" points="394.764,65.9018 384.728,70.8291 387.264,65.8474 384.801,60.8294 "/>
|
||||
<polygon style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="394.764,65.9018 384.728,70.8291 387.264,65.8474 384.801,60.8294 "/>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke-dasharray: 6; stroke: #000000" x1="346.122" y1="37.0905" x2="387.264" y2="37.4729"/>
|
||||
<polygon style="fill: #000000" points="338.622,37.0208 348.668,32.1139 346.122,37.0905 348.575,42.1135 "/>
|
||||
<polygon style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="338.622,37.0208 348.668,32.1139 346.122,37.0905 348.575,42.1135 "/>
|
||||
<polygon style="fill: #000000" points="394.764,37.5426 384.718,42.4495 387.264,37.4729 384.811,32.4499 "/>
|
||||
<polygon style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="394.764,37.5426 384.718,42.4495 387.264,37.4729 384.811,32.4499 "/>
|
||||
</g>
|
||||
<g>
|
||||
<rect style="fill: #ffb400" x="407" y="-159" width="160" height="89"/>
|
||||
@@ -160,10 +155,10 @@
|
||||
</text>
|
||||
</g>
|
||||
<g>
|
||||
<rect style="fill: #ffffff" x="639.986" y="-66.4" width="117" height="56"/>
|
||||
<rect style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x="639.986" y="-66.4" width="117" height="56"/>
|
||||
<text font-size="12.8" style="fill: #000000;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:700" x="698.486" y="-34.5">
|
||||
<tspan x="698.486" y="-34.5">Nova</tspan>
|
||||
<rect style="fill: #ffffff" x="644.986" y="-121.4" width="117" height="56"/>
|
||||
<rect style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x="644.986" y="-121.4" width="117" height="56"/>
|
||||
<text font-size="12.8" style="fill: #000000;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:700" x="703.486" y="-89.5">
|
||||
<tspan x="703.486" y="-89.5">Nova</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g>
|
||||
@@ -174,14 +169,9 @@
|
||||
</text>
|
||||
</g>
|
||||
<g>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x1="149.386" y1="-56.9998" x2="216.651" y2="-56.1259"/>
|
||||
<polygon style="fill: #000000" points="224.15,-56.0284 214.086,-51.1588 216.651,-56.1259 214.216,-61.1579 "/>
|
||||
<polygon style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="224.15,-56.0284 214.086,-51.1588 216.651,-56.1259 214.216,-61.1579 "/>
|
||||
</g>
|
||||
<g>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x1="341.294" y1="-128.975" x2="381.652" y2="-128.189"/>
|
||||
<polygon style="fill: #000000" points="389.15,-128.043 379.055,-123.238 381.652,-128.189 379.25,-133.236 "/>
|
||||
<polygon style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="389.15,-128.043 379.055,-123.238 381.652,-128.189 379.25,-133.236 "/>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x1="337" y1="-121" x2="387.264" y2="-121"/>
|
||||
<polygon style="fill: #000000" points="394.764,-121 384.764,-116 387.264,-121 384.764,-126 "/>
|
||||
<polygon style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="394.764,-121 384.764,-116 387.264,-121 384.764,-126 "/>
|
||||
</g>
|
||||
<g>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke-dasharray: 6; stroke: #000000" x1="346.121" y1="-92.8795" x2="387.265" y2="-92.3705"/>
|
||||
@@ -191,21 +181,16 @@
|
||||
<polygon style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="394.764,-92.2777 384.703,-87.4018 387.265,-92.3705 384.827,-97.401 "/>
|
||||
</g>
|
||||
<g>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x1="577" y1="-114.5" x2="633.053" y2="-59.2355"/>
|
||||
<polygon style="fill: #000000" points="638.394,-53.9699 627.762,-57.4302 633.053,-59.2355 634.783,-64.5512 "/>
|
||||
<polygon style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="638.394,-53.9699 627.762,-57.4302 633.053,-59.2355 634.783,-64.5512 "/>
|
||||
</g>
|
||||
<g>
|
||||
<rect style="fill: #ffffff" x="654.61" y="121.001" width="137.55" height="38"/>
|
||||
<rect style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x="654.61" y="121.001" width="137.55" height="38"/>
|
||||
<text font-size="12.8" style="fill: #000000;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="723.385" y="143.901">
|
||||
<tspan x="723.385" y="143.901">Ceilometer API</tspan>
|
||||
<rect style="fill: #ffffff" x="522.61" y="138.001" width="137.55" height="38"/>
|
||||
<rect style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x="522.61" y="138.001" width="137.55" height="38"/>
|
||||
<text font-size="12.8" style="fill: #000000;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="591.385" y="160.901">
|
||||
<tspan x="591.385" y="160.901">Ceilometer API</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x1="584.386" y1="53.5" x2="833.65" y2="54.4624"/>
|
||||
<polygon style="fill: #000000" points="841.15,54.4914 831.131,59.4527 833.65,54.4624 831.169,49.4528 "/>
|
||||
<polygon style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="841.15,54.4914 831.131,59.4527 833.65,54.4624 831.169,49.4528 "/>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x1="409.386" y1="106.75" x2="374.606" y2="138.218"/>
|
||||
<polygon style="fill: #000000" points="369.044,143.25 373.105,132.833 374.606,138.218 379.814,140.248 "/>
|
||||
<polygon style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="369.044,143.25 373.105,132.833 374.606,138.218 379.814,140.248 "/>
|
||||
</g>
|
||||
<g>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x1="-23.614" y1="-89.95" x2="44.0985" y2="-61.7438"/>
|
||||
@@ -218,38 +203,62 @@
|
||||
<polygon style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="51.4628,-17.2121 47.6424,-6.70471 46.0184,-12.0538 40.7647,-13.9639 "/>
|
||||
</g>
|
||||
<g>
|
||||
<rect style="fill: #ffa200" x="506.36" y="72.1008" width="92.025" height="49.9"/>
|
||||
<path style="fill: #ffa200" d="M 506.36,72.1008 A 10,10 0 0 0 496.36,82.1008 L 506.36,82.1008 z"/>
|
||||
<path style="fill: #ffa200" d="M 608.385,82.1008 A 10,10 0 0 0 598.385,72.1008 L 598.385,82.1008 z"/>
|
||||
<rect style="fill: #ffa200" x="496.36" y="82.1008" width="112.025" height="29.9"/>
|
||||
<path style="fill: #ffa200" d="M 496.36,112.001 A 10,10 0 0 0 506.36,122.001 L 506.36,112.001 z"/>
|
||||
<path style="fill: #ffa200" d="M 598.385,122.001 A 10,10 0 0 0 608.385,112.001 L 598.385,112.001 z"/>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="506.36" y1="72.1008" x2="598.385" y2="72.1008"/>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="506.36" y1="122.001" x2="598.385" y2="122.001"/>
|
||||
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 506.36,72.1008 A 10,10 0 0 0 496.36,82.1008"/>
|
||||
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 608.385,82.1008 A 10,10 0 0 0 598.385,72.1008"/>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="496.36" y1="82.1008" x2="496.36" y2="112.001"/>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="608.385" y1="82.1008" x2="608.385" y2="112.001"/>
|
||||
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 496.36,112.001 A 10,10 0 0 0 506.36,122.001"/>
|
||||
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 598.385,122.001 A 10,10 0 0 0 608.385,112.001"/>
|
||||
<text font-size="14.1111" style="fill: #ffffff;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:700" x="552.372" y="101.331">
|
||||
<tspan x="552.372" y="101.331">Strategy</tspan>
|
||||
<rect style="fill: #ffa200" x="422.36" y="60.1008" width="92.025" height="49.9"/>
|
||||
<path style="fill: #ffa200" d="M 422.36,60.1008 A 10,10 0 0 0 412.36,70.1008 L 422.36,70.1008 z"/>
|
||||
<path style="fill: #ffa200" d="M 524.385,70.1008 A 10,10 0 0 0 514.385,60.1008 L 514.385,70.1008 z"/>
|
||||
<rect style="fill: #ffa200" x="412.36" y="70.1008" width="112.025" height="29.9"/>
|
||||
<path style="fill: #ffa200" d="M 412.36,100.001 A 10,10 0 0 0 422.36,110.001 L 422.36,100.001 z"/>
|
||||
<path style="fill: #ffa200" d="M 514.385,110.001 A 10,10 0 0 0 524.385,100.001 L 514.385,100.001 z"/>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="422.36" y1="60.1008" x2="514.385" y2="60.1008"/>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="422.36" y1="110.001" x2="514.385" y2="110.001"/>
|
||||
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 422.36,60.1008 A 10,10 0 0 0 412.36,70.1008"/>
|
||||
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 524.385,70.1008 A 10,10 0 0 0 514.385,60.1008"/>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="412.36" y1="70.1008" x2="412.36" y2="100.001"/>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="524.385" y1="70.1008" x2="524.385" y2="100.001"/>
|
||||
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 412.36,100.001 A 10,10 0 0 0 422.36,110.001"/>
|
||||
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 514.385,110.001 A 10,10 0 0 0 524.385,100.001"/>
|
||||
<text font-size="14.1111" style="fill: #ffffff;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:700" x="468.372" y="89.3314">
|
||||
<tspan x="468.372" y="89.3314">Strategy</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x1="611.386" y1="115.526" x2="646.095" y2="136.896"/>
|
||||
<polygon style="fill: #000000" points="652.482,140.829 641.345,139.843 646.095,136.896 646.588,131.328 "/>
|
||||
<polygon style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="652.482,140.829 641.345,139.843 646.095,136.896 646.588,131.328 "/>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x1="529.386" y1="109.75" x2="550.79" y2="126.911"/>
|
||||
<polygon style="fill: #000000" points="556.641,131.602 545.712,129.248 550.79,126.911 551.967,121.446 "/>
|
||||
<polygon style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="556.641,131.602 545.712,129.248 550.79,126.911 551.967,121.446 "/>
|
||||
</g>
|
||||
<g>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="577,-136.75 874.386,-136.75 874.386,3.76393 "/>
|
||||
<polygon style="fill: #000000" points="874.386,11.2639 869.386,1.26393 874.386,3.76393 879.386,1.26393 "/>
|
||||
<polygon style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="874.386,11.2639 869.386,1.26393 874.386,3.76393 879.386,1.26393 "/>
|
||||
<rect style="fill: #ffa200" x="625.488" y="60.1008" width="80.5125" height="49.9"/>
|
||||
<path style="fill: #ffa200" d="M 625.488,60.1008 A 10,10 0 0 0 615.488,70.1008 L 625.488,70.1008 z"/>
|
||||
<path style="fill: #ffa200" d="M 716,70.1008 A 10,10 0 0 0 706,60.1008 L 706,70.1008 z"/>
|
||||
<rect style="fill: #ffa200" x="615.488" y="70.1008" width="100.512" height="29.9"/>
|
||||
<path style="fill: #ffa200" d="M 615.488,100.001 A 10,10 0 0 0 625.488,110.001 L 625.488,100.001 z"/>
|
||||
<path style="fill: #ffa200" d="M 706,110.001 A 10,10 0 0 0 716,100.001 L 706,100.001 z"/>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="625.488" y1="60.1008" x2="706" y2="60.1008"/>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="625.488" y1="110.001" x2="706" y2="110.001"/>
|
||||
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 625.488,60.1008 A 10,10 0 0 0 615.488,70.1008"/>
|
||||
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 716,70.1008 A 10,10 0 0 0 706,60.1008"/>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="615.488" y1="70.1008" x2="615.488" y2="100.001"/>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" x1="716" y1="70.1008" x2="716" y2="100.001"/>
|
||||
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 615.488,100.001 A 10,10 0 0 0 625.488,110.001"/>
|
||||
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 706,110.001 A 10,10 0 0 0 716,100.001"/>
|
||||
<text font-size="14.1111" style="fill: #ffffff;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:700" x="665.744" y="89.3314">
|
||||
<tspan x="665.744" y="89.3314">Planner</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x1="579.3" y1="21.418" x2="632.216" y2="-18.5335"/>
|
||||
<polygon style="fill: #000000" points="638.201,-23.0527 633.233,-13.0367 632.216,-18.5335 627.208,-21.0175 "/>
|
||||
<polygon style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="638.201,-23.0527 633.233,-13.0367 632.216,-18.5335 627.208,-21.0175 "/>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x1="337" y1="4" x2="387.264" y2="4"/>
|
||||
<polygon style="fill: #000000" points="394.764,4 384.764,9 387.264,4 384.764,-1 "/>
|
||||
<polygon style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="394.764,4 384.764,9 387.264,4 384.764,-1 "/>
|
||||
</g>
|
||||
<g>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x1="148.386" y1="-60.25" x2="218.65" y2="-60.25"/>
|
||||
<polygon style="fill: #000000" points="226.15,-60.25 216.15,-55.25 218.65,-60.25 216.15,-65.25 "/>
|
||||
<polygon style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="226.15,-60.25 216.15,-55.25 218.65,-60.25 216.15,-65.25 "/>
|
||||
</g>
|
||||
<g>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x1="581.486" y1="-96.9139" x2="633.15" y2="-96.4987"/>
|
||||
<polygon style="fill: #000000" points="640.65,-96.4384 630.61,-91.519 633.15,-96.4987 630.691,-101.519 "/>
|
||||
<polygon style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="640.65,-96.4384 630.61,-91.519 633.15,-96.4987 630.691,-101.519 "/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill: #ffb400" d="M 844 38.3333 C 865.877,23.0833 876.816,18 898.693,18 C 920.57,18 931.509,23.0833 953.386,38.3333 L 953.386,119.667 C 931.509,134.917 920.57,140 898.693,140 C 876.816,140 865.877,134.917 844,119.667 L 844,38.3333z"/>
|
||||
@@ -260,16 +269,34 @@
|
||||
</text>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill: #ff5050" d="M 651.176 172.933 C 680.618,157.683 695.339,152.6 724.78,152.6 C 754.222,152.6 768.943,157.683 798.385,172.933 L 798.385,254.267 C 768.943,269.517 754.222,274.6 724.78,274.6 C 695.339,274.6 680.618,269.517 651.176,254.267 L 651.176,172.933z"/>
|
||||
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 651.176 172.933 C 680.618,157.683 695.339,152.6 724.78,152.6 C 754.222,152.6 768.943,157.683 798.385,172.933 L 798.385,254.267 C 768.943,269.517 754.222,274.6 724.78,274.6 C 695.339,274.6 680.618,269.517 651.176,254.267 L 651.176,172.933"/>
|
||||
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 651.176 172.933 C 680.618,188.183 695.339,193.267 724.78,193.267 C 754.222,193.267 768.943,188.183 798.385,172.933"/>
|
||||
<text font-size="12.8" style="fill: #ffffff;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:700" x="724.78" y="227.767">
|
||||
<tspan x="724.78" y="227.767">Cluster History DB</tspan>
|
||||
<path style="fill: #ff5050" d="M 285.054 172.933 C 312.92,157.683 326.854,152.6 354.72,152.6 C 382.586,152.6 396.52,157.683 424.386,172.933 L 424.386,254.267 C 396.52,269.517 382.586,274.6 354.72,274.6 C 326.854,274.6 312.92,269.517 285.054,254.267 L 285.054,172.933z"/>
|
||||
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 285.054 172.933 C 312.92,157.683 326.854,152.6 354.72,152.6 C 382.586,152.6 396.52,157.683 424.386,172.933 L 424.386,254.267 C 396.52,269.517 382.586,274.6 354.72,274.6 C 326.854,274.6 312.92,269.517 285.054,254.267 L 285.054,172.933"/>
|
||||
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 285.054 172.933 C 312.92,188.183 326.854,193.267 354.72,193.267 C 382.586,193.267 396.52,188.183 424.386,172.933"/>
|
||||
<text font-size="12.8" style="fill: #ffffff;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:700" x="354.72" y="227.767">
|
||||
<tspan x="354.72" y="227.767">Cluster Model DB</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="96.386,-142.999 96.386,-205.998 899.386,-205.998 899.386,5.26493 "/>
|
||||
<polygon style="fill: #000000" points="899.386,12.7649 894.386,2.76493 899.386,5.26493 904.386,2.76493 "/>
|
||||
<polygon style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="899.386,12.7649 894.386,2.76493 899.386,5.26493 904.386,2.76493 "/>
|
||||
<path style="fill: #ff5050" d="M 522.176 185.933 C 551.618,170.683 566.339,165.6 595.78,165.6 C 625.222,165.6 639.943,170.683 669.385,185.933 L 669.385,267.267 C 639.943,282.517 625.222,287.6 595.78,287.6 C 566.339,287.6 551.618,282.517 522.176,267.267 L 522.176,185.933z"/>
|
||||
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 522.176 185.933 C 551.618,170.683 566.339,165.6 595.78,165.6 C 625.222,165.6 639.943,170.683 669.385,185.933 L 669.385,267.267 C 639.943,282.517 625.222,287.6 595.78,287.6 C 566.339,287.6 551.618,282.517 522.176,267.267 L 522.176,185.933"/>
|
||||
<path style="fill: none; fill-opacity:0; stroke-width: 4; stroke: #ffffff" d="M 522.176 185.933 C 551.618,201.183 566.339,206.267 595.78,206.267 C 625.222,206.267 639.943,201.183 669.385,185.933"/>
|
||||
<text font-size="12.8" style="fill: #ffffff;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:700" x="595.78" y="240.767">
|
||||
<tspan x="595.78" y="240.767">Cluster History DB</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="96.386,-142.999 96.386,-205.998 898.692,-205.998 898.692,8.26393 "/>
|
||||
<polygon style="fill: #000000" points="898.692,15.7639 893.692,5.76393 898.692,8.26393 903.692,5.76393 "/>
|
||||
<polygon style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="898.692,15.7639 893.692,5.76393 898.692,8.26393 903.692,5.76393 "/>
|
||||
</g>
|
||||
<g>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="583,-142.25 583,-141.25 870,-141.25 870,7.01393 "/>
|
||||
<polygon style="fill: #000000" points="870,14.5139 865,4.51393 870,7.01393 875,4.51393 "/>
|
||||
<polygon style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="870,14.5139 865,4.51393 870,7.01393 875,4.51393 "/>
|
||||
</g>
|
||||
<g>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x1="752.386" y1="60.75" x2="824.64" y2="60.9708"/>
|
||||
<polygon style="fill: #000000" points="832.14,60.9938 822.125,65.9632 824.64,60.9708 822.156,55.9632 "/>
|
||||
<polygon style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="832.14,60.9938 822.125,65.9632 824.64,60.9708 822.156,55.9632 "/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 27 KiB |
BIN
doc/source/images/audit_state_machine.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
139
doc/source/images/functional_data_model.svg
Normal file
@@ -0,0 +1,139 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/PR-SVG-20010719/DTD/svg10.dtd">
|
||||
<svg width="58cm" height="28cm" viewBox="26 8 1147 549" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g>
|
||||
<rect style="fill: #ffffff" x="570" y="99" width="148.6" height="28"/>
|
||||
<rect style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x="570" y="99" width="148.6" height="28"/>
|
||||
<text font-size="16" style="fill: #000000;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:700" x="644.3" y="118">Audit Template</text>
|
||||
</g>
|
||||
<g>
|
||||
<rect style="fill: #ffffff" x="212" y="140" width="177.5" height="28"/>
|
||||
<rect style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x="212" y="140" width="177.5" height="28"/>
|
||||
<text font-size="16" style="fill: #000000;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:700" x="300.75" y="159">OpenStack Cluster</text>
|
||||
</g>
|
||||
<g>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke-dasharray: 8; stroke: #000000" points="569.006,113 446,113 446,154 397.19,154 "/>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="409.838,149 393.838,154 409.838,159 "/>
|
||||
<text font-size="12.7998" style="fill: #000000;text-anchor:start;font-family:monospace;font-style:normal;font-weight:normal" x="448" y="130.5">Applies to</text>
|
||||
</g>
|
||||
<g>
|
||||
<rect style="fill: #ffffff" x="615" y="227" width="58.4" height="28"/>
|
||||
<rect style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x="615" y="227" width="58.4" height="28"/>
|
||||
<text font-size="16" style="fill: #000000;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:700" x="644.2" y="246">Audit</text>
|
||||
</g>
|
||||
<g>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke-dasharray: 8; stroke: #000000" points="644.2,225.996 644.2,188.497 644.3,188.497 644.3,133.705 "/>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="649.3,146.353 644.3,130.353 639.3,146.353 "/>
|
||||
<text font-size="12.7998" style="fill: #000000;text-anchor:middle;font-family:monospace;font-style:normal;font-weight:normal" x="644.25" y="185.497">gets configuration from</text>
|
||||
</g>
|
||||
<g>
|
||||
<rect style="fill: #ffffff" x="916" y="9" width="50.45" height="28"/>
|
||||
<rect style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x="916" y="9" width="50.45" height="28"/>
|
||||
<text font-size="16" style="fill: #000000;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:700" x="941.225" y="28">Goal</text>
|
||||
</g>
|
||||
<g>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke-dasharray: 8; stroke: #000000" points="644.3,97.9927 644.3,72 941.225,72 941.225,44.7125 "/>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="946.225,57.3599 941.225,41.3599 936.225,57.3599 "/>
|
||||
<text font-size="12.7998" style="fill: #000000;text-anchor:middle;font-family:monospace;font-style:normal;font-weight:normal" x="792.763" y="69">Achieves</text>
|
||||
</g>
|
||||
<g>
|
||||
<rect style="fill: #ffffff" x="495" y="367" width="112.45" height="28"/>
|
||||
<rect style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x="495" y="367" width="112.45" height="28"/>
|
||||
<text font-size="16" style="fill: #000000;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:700" x="551.225" y="386">Action Plan</text>
|
||||
</g>
|
||||
<g>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke-dasharray: 8; stroke: #000000" points="644.2,256 644.2,298.5 523,298.5 523,356.295 "/>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="518,343.647 523,359.647 528,343.647 "/>
|
||||
<text font-size="12.7998" style="fill: #000000;text-anchor:middle;font-family:monospace;font-style:normal;font-weight:normal" x="583.6" y="295.5">Generates</text>
|
||||
</g>
|
||||
<g>
|
||||
<rect style="fill: #ffffff" x="682" y="471" width="67.45" height="28"/>
|
||||
<rect style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x="682" y="471" width="67.45" height="28"/>
|
||||
<text font-size="16" style="fill: #000000;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:700" x="715.725" y="490">Action</text>
|
||||
</g>
|
||||
<g>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="551.225,420.945 551.225,443 715.725,443 715.725,470.029 "/>
|
||||
<polygon style="fill: #000000" points="551.225,395.773 556.025,409.773 551.225,423.773 546.425,409.773 "/>
|
||||
<polygon style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="551.225,395.773 556.025,409.773 551.225,423.773 546.425,409.773 "/>
|
||||
<text font-size="12.7998" style="fill: #000000;text-anchor:middle;font-family:monospace;font-style:normal;font-weight:normal" x="633.475" y="440">is composed of</text>
|
||||
<text font-size="12.7998" style="fill: #000000;text-anchor:start;font-family:monospace;font-style:normal;font-weight:normal" x="562.225" y="407.773"></text>
|
||||
<text font-size="12.7998" style="fill: #000000;text-anchor:start;font-family:monospace;font-style:normal;font-weight:normal" x="719.725" y="466.029"></text>
|
||||
</g>
|
||||
<g>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="749.45,499 749.45,517 862,517 862,485 749.45,485 "/>
|
||||
<text font-size="12.7998" style="fill: #000000;text-anchor:middle;font-family:monospace;font-style:normal;font-weight:normal" x="805.725" y="514">Next action</text>
|
||||
<polygon style="fill: #000000" points="850.075,514 850.075,506 858.075,510 "/>
|
||||
<text font-size="12.7998" style="fill: #000000;text-anchor:start;font-family:monospace;font-style:normal;font-weight:normal" x="753.45" y="511"></text>
|
||||
<text font-size="12.7998" style="fill: #000000;text-anchor:start;font-family:monospace;font-style:normal;font-weight:normal" x="753.45" y="482"></text>
|
||||
</g>
|
||||
<g>
|
||||
<ellipse style="fill: #ffffff" cx="1036" cy="219" rx="6" ry="6"/>
|
||||
<ellipse style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #ff0000" cx="1036" cy="219" rx="6" ry="6"/>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #ff0000" x1="1012" y1="231" x2="1060" y2="231"/>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #ff0000" x1="1036" y1="225" x2="1036" y2="255"/>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #ff0000" x1="1036" y1="255" x2="1012" y2="281"/>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #ff0000" x1="1036" y1="255" x2="1060" y2="281"/>
|
||||
<text font-size="12.8" style="fill: #ff0000;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:700" x="1036" y="304.9">
|
||||
<tspan x="1036" y="304.9">Administrator</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke-dasharray: 8; stroke: #000000" points="985.869,255 834.138,255 834.138,241 681.113,241 "/>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="693.76,236 677.76,241 693.76,246 "/>
|
||||
<text font-size="12.7998" style="fill: #000000;text-anchor:start;font-family:monospace;font-style:normal;font-weight:normal" x="836.138" y="245">Triggers</text>
|
||||
</g>
|
||||
<g>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke-dasharray: 8; stroke: #000000" points="1038,192.993 882.3,192.993 882.3,113 725.305,113 "/>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="737.953,108 721.953,113 737.953,118 "/>
|
||||
<text font-size="12.7998" style="fill: #000000;text-anchor:start;font-family:monospace;font-style:normal;font-weight:normal" x="884.3" y="149.997">Defines Audit configuration in</text>
|
||||
</g>
|
||||
<g>
|
||||
<ellipse style="fill: #ffffff" cx="103" cy="28" rx="6" ry="6"/>
|
||||
<ellipse style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #ff0000" cx="103" cy="28" rx="6" ry="6"/>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #ff0000" x1="79" y1="40" x2="127" y2="40"/>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #ff0000" x1="103" y1="34" x2="103" y2="64"/>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #ff0000" x1="103" y1="64" x2="79" y2="90"/>
|
||||
<line style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #ff0000" x1="103" y1="64" x2="127" y2="90"/>
|
||||
<text font-size="12.8" style="fill: #ff0000;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:700" x="103" y="113.9">
|
||||
<tspan x="103" y="113.9">Customer</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke-dasharray: 8; stroke: #000000" points="137.683,64 300.75,64 300.75,133.295 "/>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="295.75,120.647 300.75,136.647 305.75,120.647 "/>
|
||||
<text font-size="12.7998" style="fill: #000000;text-anchor:middle;font-family:monospace;font-style:normal;font-weight:normal" x="219.217" y="61">Consumes resources</text>
|
||||
</g>
|
||||
<g>
|
||||
<rect style="fill: #ffffff" x="27" y="258" width="102.8" height="28"/>
|
||||
<rect style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x="27" y="258" width="102.8" height="28"/>
|
||||
<text font-size="16" style="fill: #000000;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:700" x="78.4" y="277">Resources</text>
|
||||
</g>
|
||||
<g>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="186.828,154 78.4,154 78.4,258 "/>
|
||||
<polygon style="fill: #000000" points="212,154 198,158.8 184,154 198,149.2 "/>
|
||||
<polygon style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="212,154 198,158.8 184,154 198,149.2 "/>
|
||||
<text font-size="12.7998" style="fill: #000000;text-anchor:middle;font-family:monospace;font-style:normal;font-weight:normal" x="145.2" y="151"></text>
|
||||
<text font-size="12.7998" style="fill: #000000;text-anchor:end;font-family:monospace;font-style:normal;font-weight:normal" x="180" y="151"></text>
|
||||
<text font-size="12.7998" style="fill: #000000;text-anchor:start;font-family:monospace;font-style:normal;font-weight:normal" x="82.4" y="254"></text>
|
||||
</g>
|
||||
<g>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke-dasharray: 8; stroke: #000000" points="715.724,499 715.724,540 78.4,540 78.4,292.705 "/>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="83.4,305.353 78.4,289.353 73.4,305.353 "/>
|
||||
<text font-size="12.7998" style="fill: #000000;text-anchor:middle;font-family:monospace;font-style:normal;font-weight:normal" x="397.062" y="537">Modifies</text>
|
||||
</g>
|
||||
<g>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke-dasharray: 8; stroke: #000000" points="1036,309.94 1036,381 614.155,381 "/>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="626.803,376 610.803,381 626.803,386 "/>
|
||||
<text font-size="12.7998" style="fill: #000000;text-anchor:middle;font-family:monospace;font-style:normal;font-weight:normal" x="821.725" y="378">Launches</text>
|
||||
</g>
|
||||
<g>
|
||||
<rect style="fill: #ffffff" x="1082.9" y="43.1" width="88.25" height="28"/>
|
||||
<rect style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x="1082.9" y="43.1" width="88.25" height="28"/>
|
||||
<text font-size="16" style="fill: #000000;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:700" x="1127.02" y="62.1">Strategy</text>
|
||||
</g>
|
||||
<g>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke-dasharray: 8; stroke: #000000" points="966.45,23 1020.17,23 1020.17,57.1 1075.22,57.1 "/>
|
||||
<polyline style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" points="1062.57,62.1 1078.57,57.1 1062.57,52.1 "/>
|
||||
<text font-size="12.7998" style="fill: #000000;text-anchor:start;font-family:monospace;font-style:normal;font-weight:normal" x="1022.17" y="37.05">uses</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 12 KiB |
BIN
doc/source/images/sequence_create_and_launch_audit.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
doc/source/images/sequence_create_audit_template.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
doc/source/images/sequence_launch_action_plan.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
doc/source/images/sequence_launch_action_plan_in_applier.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
doc/source/images/sequence_overview_watcher_usage.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
doc/source/images/sequence_trigger_audit_in_decision_engine.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
@@ -4,20 +4,32 @@
|
||||
|
||||
https://creativecommons.org/licenses/by/3.0/
|
||||
|
||||
============================================
|
||||
Welcome to Watcher's developer documentation
|
||||
============================================
|
||||
================================
|
||||
Welcome to Watcher documentation
|
||||
================================
|
||||
|
||||
Mission
|
||||
=======
|
||||
OpenStack Watcher provides a flexible and scalable resource optimization
|
||||
service for multi-tenant OpenStack-based clouds.
|
||||
Watcher provides a complete optimization loop—including everything from a
|
||||
metrics receiver, complex event processor and profiler, optimization processor
|
||||
and an action plan applier. This provides a robust framework to realize a wide
|
||||
range of cloud optimization goals, including the reduction of data center
|
||||
operating costs, increased system performance via intelligent virtual machine
|
||||
migration, increased energy efficiency—and more!
|
||||
|
||||
Provide an auditing service for OpenStack to improve workload placement
|
||||
on-the-go.
|
||||
Watcher project consists of several source code repositories:
|
||||
|
||||
The developer documentation provided here is continually kept up-to-date based
|
||||
* `watcher`_ - is the main repository. It contains code for Watcher API server,
|
||||
Watcher Decision Engine and Watcher Applier.
|
||||
* `python-watcherclient`_ - Client library and CLI client for Watcher.
|
||||
|
||||
The documentation provided here is continually kept up-to-date based
|
||||
on the latest code, and may not represent the state of the project at any
|
||||
specific prior release.
|
||||
|
||||
.. _watcher: https://git.openstack.org/cgit/openstack/watcher/
|
||||
.. _python-watcherclient: https://git.openstack.org/cgit/openstack/python-watcherclient/
|
||||
|
||||
Developer Guide
|
||||
===============
|
||||
|
||||
@@ -29,9 +41,18 @@ Introduction
|
||||
|
||||
glossary
|
||||
architecture
|
||||
dev/environment
|
||||
dev/contributing
|
||||
dev/plugins
|
||||
|
||||
|
||||
Getting Started
|
||||
---------------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
dev/environment
|
||||
dev/devstack
|
||||
deploy/configuration
|
||||
|
||||
|
||||
API References
|
||||
@@ -42,25 +63,44 @@ API References
|
||||
|
||||
webapi/v1
|
||||
|
||||
Plugins
|
||||
-------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
dev/strategy-plugin
|
||||
|
||||
|
||||
Admin Guide
|
||||
===========
|
||||
|
||||
Overview
|
||||
--------
|
||||
Introduction
|
||||
------------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
deploy/user-guide
|
||||
deploy/installation
|
||||
deploy/user-guide
|
||||
|
||||
Commands
|
||||
--------
|
||||
Watcher Manual Pages
|
||||
====================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:glob:
|
||||
:maxdepth: 1
|
||||
|
||||
cmds/watcher-db-manage
|
||||
man/*
|
||||
|
||||
.. # NOTE(mriedem): This is the section where we hide things that we don't
|
||||
# actually want in the table of contents but sphinx build would fail if
|
||||
# they aren't in the toctree somewhere. For example, we hide api/autoindex
|
||||
# since that's already covered with modindex below.
|
||||
.. toctree::
|
||||
:hidden:
|
||||
|
||||
api/autoindex
|
||||
|
||||
|
||||
Indices and tables
|
||||
|
||||
5
doc/source/man/footer.rst
Normal file
@@ -0,0 +1,5 @@
|
||||
BUGS
|
||||
====
|
||||
|
||||
* Watcher bugs are tracked in Launchpad at `OpenStack Watcher
|
||||
<http://bugs.launchpad.net/watcher>`__
|
||||
66
doc/source/man/general-options.rst
Normal file
@@ -0,0 +1,66 @@
|
||||
**-h, --help**
|
||||
Show the help message and exit
|
||||
|
||||
**--version**
|
||||
Print the version number and exit
|
||||
|
||||
**-v, --verbose**
|
||||
Print more verbose output
|
||||
|
||||
**--noverbose**
|
||||
Disable verbose output
|
||||
|
||||
**-d, --debug**
|
||||
Print debugging output (set logging level to DEBUG instead of
|
||||
default WARNING level)
|
||||
|
||||
**--nodebug**
|
||||
Disable debugging output
|
||||
|
||||
**--use-syslog**
|
||||
Use syslog for logging
|
||||
|
||||
**--nouse-syslog**
|
||||
Disable the use of syslog for logging
|
||||
|
||||
**--syslog-log-facility SYSLOG_LOG_FACILITY**
|
||||
syslog facility to receive log lines
|
||||
|
||||
**--config-dir DIR**
|
||||
Path to a config directory to pull \*.conf files from. This
|
||||
file set is sorted, to provide a predictable parse order
|
||||
if individual options are over-ridden. The set is parsed after
|
||||
the file(s) specified via previous --config-file, arguments hence
|
||||
over-ridden options in the directory take precedence. This means
|
||||
that configuration from files in a specified config-dir will
|
||||
always take precedence over configuration from files specified
|
||||
by --config-file, regardless to argument order.
|
||||
|
||||
**--config-file PATH**
|
||||
Path to a config file to use. Multiple config files can be
|
||||
specified by using this flag multiple times, for example,
|
||||
--config-file <file1> --config-file <file2>. Values in latter
|
||||
files take precedence.
|
||||
|
||||
**--log-config-append PATH** **--log-config PATH**
|
||||
The name of logging configuration file. It does not
|
||||
disable existing loggers, but just appends specified
|
||||
logging configuration to any other existing logging
|
||||
options. Please see the Python logging module documentation
|
||||
for details on logging configuration files. The log-config
|
||||
name for this option is depcrecated.
|
||||
|
||||
**--log-format FORMAT**
|
||||
A logging.Formatter log message format string which may use any
|
||||
of the available logging.LogRecord attributes. Default: None
|
||||
|
||||
**--log-date-format DATE_FORMAT**
|
||||
Format string for %(asctime)s in log records. Default: None
|
||||
|
||||
**--log-file PATH, --logfile PATH**
|
||||
(Optional) Name of log file to output to. If not set, logging
|
||||
will go to stdout.
|
||||
|
||||
**--log-dir LOG_DIR, --logdir LOG_DIR**
|
||||
(Optional) The directory to keep log files in (will be prepended
|
||||
to --log-file)
|
||||
39
doc/source/man/watcher-api.rst
Normal file
@@ -0,0 +1,39 @@
|
||||
===========
|
||||
watcher-api
|
||||
===========
|
||||
|
||||
---------------------------
|
||||
Service for the Watcher API
|
||||
---------------------------
|
||||
|
||||
:Author: openstack@lists.launchpad.net
|
||||
:Date:
|
||||
:Copyright: OpenStack Foundation
|
||||
:Version:
|
||||
:Manual section: 1
|
||||
:Manual group: cloud computing
|
||||
|
||||
SYNOPSIS
|
||||
========
|
||||
|
||||
watcher-api [options]
|
||||
|
||||
DESCRIPTION
|
||||
===========
|
||||
|
||||
watcher-api is a server daemon that serves the Watcher API
|
||||
|
||||
OPTIONS
|
||||
=======
|
||||
|
||||
**General options**
|
||||
|
||||
.. include:: general-options.rst
|
||||
|
||||
FILES
|
||||
=====
|
||||
|
||||
**/etc/watcher/watcher.conf**
|
||||
Default configuration file for Watcher API
|
||||
|
||||
.. include:: footer.rst
|
||||
39
doc/source/man/watcher-applier.rst
Normal file
@@ -0,0 +1,39 @@
|
||||
===============
|
||||
watcher-applier
|
||||
===============
|
||||
|
||||
-------------------------------
|
||||
Service for the Watcher Applier
|
||||
-------------------------------
|
||||
|
||||
:Author: openstack@lists.launchpad.net
|
||||
:Date:
|
||||
:Copyright: OpenStack Foundation
|
||||
:Version:
|
||||
:Manual section: 1
|
||||
:Manual group: cloud computing
|
||||
|
||||
SYNOPSIS
|
||||
========
|
||||
|
||||
watcher-applier [options]
|
||||
|
||||
DESCRIPTION
|
||||
===========
|
||||
|
||||
:ref:`Watcher Applier <watcher_applier_definition>`
|
||||
|
||||
OPTIONS
|
||||
=======
|
||||
|
||||
**General options**
|
||||
|
||||
.. include:: general-options.rst
|
||||
|
||||
FILES
|
||||
=====
|
||||
|
||||
**/etc/watcher/watcher.conf**
|
||||
Default configuration file for Watcher Applier
|
||||
|
||||
.. include:: footer.rst
|
||||
@@ -6,9 +6,9 @@
|
||||
|
||||
.. _watcher-db-manage:
|
||||
|
||||
=============
|
||||
=================
|
||||
watcher-db-manage
|
||||
=============
|
||||
=================
|
||||
|
||||
The :command:`watcher-db-manage` utility is used to create the database schema
|
||||
tables that the watcher services will use for storage. It can also be used to
|
||||
39
doc/source/man/watcher-decision-engine.rst
Normal file
@@ -0,0 +1,39 @@
|
||||
=======================
|
||||
watcher-decision-engine
|
||||
=======================
|
||||
|
||||
---------------------------------------
|
||||
Service for the Watcher Decision Engine
|
||||
---------------------------------------
|
||||
|
||||
:Author: openstack@lists.launchpad.net
|
||||
:Date:
|
||||
:Copyright: OpenStack Foundation
|
||||
:Version:
|
||||
:Manual section: 1
|
||||
:Manual group: cloud computing
|
||||
|
||||
SYNOPSIS
|
||||
========
|
||||
|
||||
watcher-decision-engine [options]
|
||||
|
||||
DESCRIPTION
|
||||
===========
|
||||
|
||||
:ref:`Watcher Decision Engine <watcher_decision_engine_definition>`
|
||||
|
||||
OPTIONS
|
||||
=======
|
||||
|
||||
**General options**
|
||||
|
||||
.. include:: general-options.rst
|
||||
|
||||
FILES
|
||||
=====
|
||||
|
||||
**/etc/watcher/watcher.conf**
|
||||
Default configuration file for Watcher Decision Engine
|
||||
|
||||
.. include:: footer.rst
|
||||
@@ -1 +0,0 @@
|
||||
.. include:: ../../README.rst
|
||||
@@ -1,13 +0,0 @@
|
||||
..
|
||||
Except where otherwise noted, this document is licensed under Creative
|
||||
Commons Attribution 3.0 License. You can view the license at:
|
||||
|
||||
https://creativecommons.org/licenses/by/3.0/
|
||||
|
||||
========
|
||||
Usage
|
||||
========
|
||||
|
||||
To use watcher in a project::
|
||||
|
||||
import watcher
|
||||
@@ -4,15 +4,15 @@
|
||||
|
||||
https://creativecommons.org/licenses/by/3.0/
|
||||
|
||||
=====================
|
||||
RESTful Web API (v1)
|
||||
=====================
|
||||
====================
|
||||
RESTful Web API (v1)
|
||||
====================
|
||||
|
||||
Audit Templates
|
||||
===============
|
||||
|
||||
.. rest-controller:: watcher.api.controllers.v1.audit_template:AuditTemplatesController
|
||||
:webprefix: /v1/audit_template
|
||||
:webprefix: /v1/audit_templates
|
||||
|
||||
.. autotype:: watcher.api.controllers.v1.audit_template.AuditTemplateCollection
|
||||
:members:
|
||||
@@ -20,7 +20,6 @@ Audit Templates
|
||||
.. autotype:: watcher.api.controllers.v1.audit_template.AuditTemplate
|
||||
:members:
|
||||
|
||||
|
||||
Audits
|
||||
======
|
||||
|
||||
@@ -33,24 +32,22 @@ Audits
|
||||
.. autotype:: watcher.api.controllers.v1.audit.Audit
|
||||
:members:
|
||||
|
||||
|
||||
Links
|
||||
=====
|
||||
|
||||
.. autotype:: watcher.api.controllers.link.Link
|
||||
:members:
|
||||
|
||||
|
||||
ActionPlans
|
||||
===========
|
||||
Action Plans
|
||||
============
|
||||
|
||||
.. rest-controller:: watcher.api.controllers.v1.action_plan:ActionPlansController
|
||||
:webprefix: /v1/action_plans
|
||||
|
||||
.. autotype:: watcher.api.controllers.v1.action_plan.ActionPlan
|
||||
.. autotype:: watcher.api.controllers.v1.action_plan.ActionPlanCollection
|
||||
:members:
|
||||
|
||||
.. autotype:: watcher.api.controllers.v1.action_plan.ActionPlanCollection
|
||||
.. autotype:: watcher.api.controllers.v1.action_plan.ActionPlan
|
||||
:members:
|
||||
|
||||
|
||||
|
||||
@@ -26,4 +26,5 @@ python-openstackclient>=1.5.0
|
||||
six>=1.9.0
|
||||
SQLAlchemy>=0.9.9,<1.1.0
|
||||
stevedore>=1.5.0 # Apache-2.0
|
||||
taskflow>=1.25.0 # Apache-2.0
|
||||
WSME>=0.7
|
||||
|
||||
28
setup.cfg
@@ -21,6 +21,7 @@ classifier =
|
||||
[files]
|
||||
packages =
|
||||
watcher
|
||||
watcher_tempest_plugin
|
||||
data_files =
|
||||
etc/ = etc/*
|
||||
|
||||
@@ -38,6 +39,9 @@ console_scripts =
|
||||
watcher-decision-engine = watcher.cmd.decisionengine:main
|
||||
watcher-applier = watcher.cmd.applier:main
|
||||
|
||||
tempest.test_plugins =
|
||||
watcher_tests = watcher_tempest_plugin.plugin:WatcherTempestPlugin
|
||||
|
||||
watcher.database.migration_backend =
|
||||
sqlalchemy = watcher.db.sqlalchemy.migration
|
||||
|
||||
@@ -46,6 +50,28 @@ watcher_strategies =
|
||||
basic = watcher.decision_engine.strategy.strategies.basic_consolidation:BasicConsolidation
|
||||
outlet_temp_control = watcher.decision_engine.strategy.strategies.outlet_temp_control:OutletTempControl
|
||||
|
||||
watcher_actions =
|
||||
migrate = watcher.applier.actions.migration:Migrate
|
||||
nop = watcher.applier.actions.nop:Nop
|
||||
sleep = watcher.applier.actions.sleep:Sleep
|
||||
change_nova_service_state = watcher.applier.actions.change_nova_service_state:ChangeNovaServiceState
|
||||
|
||||
watcher_workflow_engines =
|
||||
taskflow = watcher.applier.workflow_engine.default:DefaultWorkFlowEngine
|
||||
|
||||
watcher_planners =
|
||||
default = watcher.decision_engine.planner.default:DefaultPlanner
|
||||
|
||||
[pbr]
|
||||
autodoc_index_modules = True
|
||||
autodoc_exclude_modules =
|
||||
watcher.db.sqlalchemy.alembic.env
|
||||
watcher.db.sqlalchemy.alembic.versions.*
|
||||
watcher.tests.*
|
||||
watcher_tempest_plugin.*
|
||||
watcher.doc
|
||||
|
||||
|
||||
[build_sphinx]
|
||||
source-dir = doc/source
|
||||
build-dir = doc/build
|
||||
@@ -66,6 +92,6 @@ output_dir = watcher/locale
|
||||
input_file = watcher/locale/watcher.pot
|
||||
|
||||
[extract_messages]
|
||||
keywords = _ gettext ngettext l_ lazy_gettext
|
||||
keywords = _ gettext ngettext l_ lazy_gettext _LI _LW _LE _LC
|
||||
mapping_file = babel.cfg
|
||||
output_file = watcher/locale/watcher.pot
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
|
||||
coverage>=3.6
|
||||
discover
|
||||
doc8 # Apache-2.0
|
||||
hacking>=0.10.2,<0.11
|
||||
mock>=1.2
|
||||
oslotest>=1.10.0 # Apache-2.0
|
||||
os-testr>=0.1.0
|
||||
python-subunit>=0.0.18
|
||||
testrepository>=0.0.18
|
||||
testscenarios>=0.4
|
||||
@@ -19,3 +21,4 @@ sphinxcontrib-pecanwsme>=0.8
|
||||
|
||||
# For PyPI distribution
|
||||
twine
|
||||
|
||||
|
||||
21
tox.ini
@@ -14,19 +14,25 @@ deps = -r{toxinidir}/requirements.txt
|
||||
commands =
|
||||
find . -type f -name "*.pyc" -delete
|
||||
find . -type d -name "__pycache__" -delete
|
||||
python setup.py testr --slowest --testr-args='{posargs}'
|
||||
ostestr --concurrency=6 {posargs}
|
||||
|
||||
[testenv:pep8]
|
||||
commands = flake8
|
||||
commands =
|
||||
doc8 doc/source/ CONTRIBUTING.rst HACKING.rst README.rst
|
||||
flake8
|
||||
|
||||
[testenv:venv]
|
||||
setenv = PYTHONHASHSEED=0
|
||||
commands = {posargs}
|
||||
|
||||
[testenv:cover]
|
||||
commands = python setup.py testr --coverage --omit="watcher/tests/*" --testr-args='{posargs}'
|
||||
|
||||
[testenv:docs]
|
||||
commands = python setup.py build_sphinx
|
||||
setenv = PYTHONHASHSEED=0
|
||||
commands =
|
||||
doc8 doc/source/ CONTRIBUTING.rst HACKING.rst README.rst
|
||||
python setup.py build_sphinx
|
||||
|
||||
[testenv:debug]
|
||||
commands = oslo_debug_helper {posargs}
|
||||
@@ -42,10 +48,8 @@ commands =
|
||||
--output-file etc/watcher/watcher.conf.sample
|
||||
|
||||
[flake8]
|
||||
# E123, E125 skipped as they are invalid PEP-8.
|
||||
|
||||
show-source=True
|
||||
ignore=E123,E125
|
||||
ignore=
|
||||
builtins= _
|
||||
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,*sqlalchemy/alembic/versions/*,demo/
|
||||
|
||||
@@ -59,3 +63,8 @@ commands = python setup.py bdist_wheel
|
||||
|
||||
[hacking]
|
||||
import_exceptions = watcher._i18n
|
||||
|
||||
[doc8]
|
||||
extension=.rst
|
||||
# todo: stop ignoring doc/source/man when https://bugs.launchpad.net/doc8/+bug/1502391 is fixed
|
||||
ignore-path=doc/source/image_src,doc/source/man
|
||||
|
||||
@@ -15,6 +15,42 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
An :ref:`Action <action_definition>` is what enables Watcher to transform the
|
||||
current state of a :ref:`Cluster <cluster_definition>` after an
|
||||
:ref:`Audit <audit_definition>`.
|
||||
|
||||
An :ref:`Action <action_definition>` is an atomic task which changes the
|
||||
current state of a target :ref:`Managed resource <managed_resource_definition>`
|
||||
of the OpenStack :ref:`Cluster <cluster_definition>` such as:
|
||||
|
||||
- Live migration of an instance from one compute node to another compute
|
||||
node with Nova
|
||||
- Changing the power level of a compute node (ACPI level, ...)
|
||||
- Changing the current state of an hypervisor (enable or disable) with Nova
|
||||
|
||||
In most cases, an :ref:`Action <action_definition>` triggers some concrete
|
||||
commands on an existing OpenStack module (Nova, Neutron, Cinder, Ironic, etc.).
|
||||
|
||||
An :ref:`Action <action_definition>` has a life-cycle and its current state may
|
||||
be one of the following:
|
||||
|
||||
- **PENDING** : the :ref:`Action <action_definition>` has not been executed
|
||||
yet by the :ref:`Watcher Applier <watcher_applier_definition>`
|
||||
- **ONGOING** : the :ref:`Action <action_definition>` is currently being
|
||||
processed by the :ref:`Watcher Applier <watcher_applier_definition>`
|
||||
- **SUCCEEDED** : the :ref:`Action <action_definition>` has been executed
|
||||
successfully
|
||||
- **FAILED** : an error occured while trying to execute the
|
||||
:ref:`Action <action_definition>`
|
||||
- **DELETED** : the :ref:`Action <action_definition>` is still stored in the
|
||||
:ref:`Watcher database <watcher_database_definition>` but is not returned
|
||||
any more through the Watcher APIs.
|
||||
- **CANCELLED** : the :ref:`Action <action_definition>` was in **PENDING** or
|
||||
**ONGOING** state and was cancelled by the
|
||||
:ref:`Administrator <administrator_definition>`
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
import pecan
|
||||
@@ -87,9 +123,6 @@ class Action(base.APIBase):
|
||||
mandatory=True)
|
||||
"""The action plan this action belongs to """
|
||||
|
||||
description = wtypes.text
|
||||
"""Description of this action"""
|
||||
|
||||
state = wtypes.text
|
||||
"""This audit state"""
|
||||
|
||||
@@ -99,17 +132,11 @@ class Action(base.APIBase):
|
||||
applies_to = wtypes.text
|
||||
"""Applies to"""
|
||||
|
||||
src = wtypes.text
|
||||
"""Hypervisor source"""
|
||||
|
||||
dst = wtypes.text
|
||||
"""Hypervisor source"""
|
||||
|
||||
action_type = wtypes.text
|
||||
"""Action type"""
|
||||
|
||||
parameter = wtypes.text
|
||||
"""Additionnal parameter"""
|
||||
input_parameters = wtypes.DictType(wtypes.text, wtypes.text)
|
||||
"""One or more key/value pairs """
|
||||
|
||||
next_uuid = wsme.wsproperty(types.uuid, _get_next_uuid,
|
||||
_set_next_uuid,
|
||||
|
||||
@@ -15,6 +15,60 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
An :ref:`Action Plan <action_plan_definition>` is a flow of
|
||||
:ref:`Actions <action_definition>` that should be executed in order to satisfy
|
||||
a given :ref:`Goal <goal_definition>`.
|
||||
|
||||
An :ref:`Action Plan <action_plan_definition>` is generated by Watcher when an
|
||||
:ref:`Audit <audit_definition>` is successful which implies that the
|
||||
:ref:`Strategy <strategy_definition>`
|
||||
which was used has found a :ref:`Solution <solution_definition>` to achieve the
|
||||
:ref:`Goal <goal_definition>` of this :ref:`Audit <audit_definition>`.
|
||||
|
||||
In the default implementation of Watcher, an
|
||||
:ref:`Action Plan <action_plan_definition>`
|
||||
is only composed of successive :ref:`Actions <action_definition>`
|
||||
(i.e., a Workflow of :ref:`Actions <action_definition>` belonging to a unique
|
||||
branch).
|
||||
|
||||
However, Watcher provides abstract interfaces for many of its components,
|
||||
allowing other implementations to generate and handle more complex
|
||||
:ref:`Action Plan(s) <action_plan_definition>`
|
||||
composed of two types of Action Item(s):
|
||||
|
||||
- simple :ref:`Actions <action_definition>`: atomic tasks, which means it
|
||||
can not be split into smaller tasks or commands from an OpenStack point of
|
||||
view.
|
||||
- composite Actions: which are composed of several simple
|
||||
:ref:`Actions <action_definition>`
|
||||
ordered in sequential and/or parallel flows.
|
||||
|
||||
An :ref:`Action Plan <action_plan_definition>` may be described using
|
||||
standard workflow model description formats such as
|
||||
`Business Process Model and Notation 2.0 (BPMN 2.0) <http://www.omg.org/spec/BPMN/2.0/>`_
|
||||
or `Unified Modeling Language (UML) <http://www.uml.org/>`_.
|
||||
|
||||
An :ref:`Action Plan <action_plan_definition>` has a life-cycle and its current
|
||||
state may be one of the following:
|
||||
|
||||
- **RECOMMENDED** : the :ref:`Action Plan <action_plan_definition>` is waiting
|
||||
for a validation from the :ref:`Administrator <administrator_definition>`
|
||||
- **ONGOING** : the :ref:`Action Plan <action_plan_definition>` is currently
|
||||
being processed by the :ref:`Watcher Applier <watcher_applier_definition>`
|
||||
- **SUCCEEDED** : the :ref:`Action Plan <action_plan_definition>` has been
|
||||
executed successfully (i.e. all :ref:`Actions <action_definition>` that it
|
||||
contains have been executed successfully)
|
||||
- **FAILED** : an error occured while executing the
|
||||
:ref:`Action Plan <action_plan_definition>`
|
||||
- **DELETED** : the :ref:`Action Plan <action_plan_definition>` is still
|
||||
stored in the :ref:`Watcher database <watcher_database_definition>` but is
|
||||
not returned any more through the Watcher APIs.
|
||||
- **CANCELLED** : the :ref:`Action Plan <action_plan_definition>` was in
|
||||
**PENDING** or **ONGOING** state and was cancelled by the
|
||||
:ref:`Administrator <administrator_definition>`
|
||||
""" # noqa
|
||||
|
||||
import datetime
|
||||
|
||||
import pecan
|
||||
@@ -23,21 +77,45 @@ import wsme
|
||||
from wsme import types as wtypes
|
||||
import wsmeext.pecan as wsme_pecan
|
||||
|
||||
from watcher._i18n import _
|
||||
from watcher.api.controllers import base
|
||||
from watcher.api.controllers import link
|
||||
from watcher.api.controllers.v1 import collection
|
||||
from watcher.api.controllers.v1 import types
|
||||
from watcher.api.controllers.v1 import utils as api_utils
|
||||
from watcher.applier.rpcapi import ApplierAPI
|
||||
from watcher.applier import rpcapi
|
||||
from watcher.common import exception
|
||||
from watcher import objects
|
||||
from watcher.objects import action_plan as ap_objects
|
||||
|
||||
|
||||
class ActionPlanPatchType(types.JsonPatchType):
|
||||
|
||||
@staticmethod
|
||||
def _validate_state(patch):
|
||||
serialized_patch = {'path': patch.path, 'op': patch.op}
|
||||
if patch.value is not wsme.Unset:
|
||||
serialized_patch['value'] = patch.value
|
||||
# todo: use state machines to handle state transitions
|
||||
state_value = patch.value
|
||||
if state_value and not hasattr(ap_objects.State, state_value):
|
||||
msg = _("Invalid state: %(state)s")
|
||||
raise exception.PatchError(
|
||||
patch=serialized_patch, reason=msg % dict(state=state_value))
|
||||
|
||||
@staticmethod
|
||||
def validate(patch):
|
||||
if patch.path == "/state":
|
||||
ActionPlanPatchType._validate_state(patch)
|
||||
return types.JsonPatchType.validate(patch)
|
||||
|
||||
@staticmethod
|
||||
def internal_attrs():
|
||||
return types.JsonPatchType.internal_attrs()
|
||||
|
||||
@staticmethod
|
||||
def mandatory_attrs():
|
||||
return []
|
||||
return ["audit_id", "state", "first_action_id"]
|
||||
|
||||
|
||||
class ActionPlan(base.APIBase):
|
||||
@@ -230,9 +308,9 @@ class ActionPlansController(rest.RestController):
|
||||
sort_key=sort_key,
|
||||
sort_dir=sort_dir)
|
||||
|
||||
@wsme_pecan.wsexpose(ActionPlanCollection, types.uuid, types.uuid,
|
||||
int, wtypes.text, wtypes.text, types.uuid)
|
||||
def get_all(self, action_plan_uuid=None, marker=None, limit=None,
|
||||
@wsme_pecan.wsexpose(ActionPlanCollection, types.uuid, int, wtypes.text,
|
||||
wtypes.text, types.uuid)
|
||||
def get_all(self, marker=None, limit=None,
|
||||
sort_key='id', sort_dir='asc', audit_uuid=None):
|
||||
"""Retrieve a list of action plans.
|
||||
|
||||
@@ -241,25 +319,23 @@ class ActionPlansController(rest.RestController):
|
||||
:param sort_key: column to sort results by. Default: id.
|
||||
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
|
||||
:param audit_uuid: Optional UUID of an audit, to get only actions
|
||||
for that audit.
|
||||
for that audit.
|
||||
"""
|
||||
return self._get_action_plans_collection(
|
||||
marker, limit, sort_key, sort_dir, audit_uuid=audit_uuid)
|
||||
|
||||
@wsme_pecan.wsexpose(ActionPlanCollection, types.uuid, types.uuid,
|
||||
int, wtypes.text, wtypes.text, types.uuid)
|
||||
def detail(self, action_plan_uuid=None, marker=None, limit=None,
|
||||
@wsme_pecan.wsexpose(ActionPlanCollection, types.uuid, int, wtypes.text,
|
||||
wtypes.text, types.uuid)
|
||||
def detail(self, marker=None, limit=None,
|
||||
sort_key='id', sort_dir='asc', audit_uuid=None):
|
||||
"""Retrieve a list of action_plans with detail.
|
||||
|
||||
:param action_plan_uuid: UUID of a action plan, to get only
|
||||
:action_plans for that action.
|
||||
:param marker: pagination marker for large data sets.
|
||||
:param limit: maximum number of resources to return in a single result.
|
||||
:param sort_key: column to sort results by. Default: id.
|
||||
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
|
||||
:param audit_uuid: Optional UUID of an audit, to get only actions
|
||||
for that audit.
|
||||
for that audit.
|
||||
"""
|
||||
# NOTE(lucasagomes): /detail should only work agaist collections
|
||||
parent = pecan.request.path.split('/')[:-1][-1]
|
||||
@@ -302,10 +378,10 @@ class ActionPlansController(rest.RestController):
|
||||
@wsme_pecan.wsexpose(ActionPlan, types.uuid,
|
||||
body=[ActionPlanPatchType])
|
||||
def patch(self, action_plan_uuid, patch):
|
||||
"""Update an existing audit template.
|
||||
"""Update an existing action plan.
|
||||
|
||||
:param audit template_uuid: UUID of a audit template.
|
||||
:param patch: a json PATCH document to apply to this audit template.
|
||||
:param action_plan_uuid: UUID of a action plan.
|
||||
:param patch: a json PATCH document to apply to this action plan.
|
||||
"""
|
||||
launch_action_plan = True
|
||||
if self.from_actionsPlans:
|
||||
@@ -322,6 +398,34 @@ class ActionPlansController(rest.RestController):
|
||||
raise exception.PatchError(patch=patch, reason=e)
|
||||
|
||||
launch_action_plan = False
|
||||
|
||||
# transitions that are allowed via PATCH
|
||||
allowed_patch_transitions = [
|
||||
(ap_objects.State.RECOMMENDED,
|
||||
ap_objects.State.TRIGGERED),
|
||||
(ap_objects.State.RECOMMENDED,
|
||||
ap_objects.State.CANCELLED),
|
||||
(ap_objects.State.ONGOING,
|
||||
ap_objects.State.CANCELLED),
|
||||
(ap_objects.State.TRIGGERED,
|
||||
ap_objects.State.CANCELLED),
|
||||
]
|
||||
|
||||
# todo: improve this in blueprint watcher-api-validation
|
||||
if hasattr(action_plan, 'state'):
|
||||
transition = (action_plan_to_update.state, action_plan.state)
|
||||
if transition not in allowed_patch_transitions:
|
||||
error_message = _("State transition not allowed: "
|
||||
"(%(initial_state)s -> %(new_state)s)")
|
||||
raise exception.PatchError(
|
||||
patch=patch,
|
||||
reason=error_message % dict(
|
||||
initial_state=action_plan_to_update.state,
|
||||
new_state=action_plan.state))
|
||||
|
||||
if action_plan.state == ap_objects.State.TRIGGERED:
|
||||
launch_action_plan = True
|
||||
|
||||
# Update only the fields that have changed
|
||||
for field in objects.ActionPlan.fields:
|
||||
try:
|
||||
@@ -334,13 +438,14 @@ class ActionPlansController(rest.RestController):
|
||||
if action_plan_to_update[field] != patch_val:
|
||||
action_plan_to_update[field] = patch_val
|
||||
|
||||
if field == 'state' and patch_val == 'STARTING':
|
||||
if (field == 'state'
|
||||
and patch_val == objects.action_plan.State.TRIGGERED):
|
||||
launch_action_plan = True
|
||||
|
||||
action_plan_to_update.save()
|
||||
|
||||
if launch_action_plan:
|
||||
applier_client = ApplierAPI()
|
||||
applier_client = rpcapi.ApplierAPI()
|
||||
applier_client.launch_action_plan(pecan.request.context,
|
||||
action_plan.uuid)
|
||||
|
||||
|
||||
@@ -15,6 +15,40 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
In the Watcher system, an :ref:`Audit <audit_definition>` is a request for
|
||||
optimizing a :ref:`Cluster <cluster_definition>`.
|
||||
|
||||
The optimization is done in order to satisfy one :ref:`Goal <goal_definition>`
|
||||
on a given :ref:`Cluster <cluster_definition>`.
|
||||
|
||||
For each :ref:`Audit <audit_definition>`, the Watcher system generates an
|
||||
:ref:`Action Plan <action_plan_definition>`.
|
||||
|
||||
An :ref:`Audit <audit_definition>` has a life-cycle and its current state may
|
||||
be one of the following:
|
||||
|
||||
- **PENDING** : a request for an :ref:`Audit <audit_definition>` has been
|
||||
submitted (either manually by the
|
||||
:ref:`Administrator <administrator_definition>` or automatically via some
|
||||
event handling mechanism) and is in the queue for being processed by the
|
||||
:ref:`Watcher Decision Engine <watcher_decision_engine_definition>`
|
||||
- **ONGOING** : the :ref:`Audit <audit_definition>` is currently being
|
||||
processed by the
|
||||
:ref:`Watcher Decision Engine <watcher_decision_engine_definition>`
|
||||
- **SUCCEEDED** : the :ref:`Audit <audit_definition>` has been executed
|
||||
successfully (note that it may not necessarily produce a
|
||||
:ref:`Solution <solution_definition>`).
|
||||
- **FAILED** : an error occured while executing the
|
||||
:ref:`Audit <audit_definition>`
|
||||
- **DELETED** : the :ref:`Audit <audit_definition>` is still stored in the
|
||||
:ref:`Watcher database <watcher_database_definition>` but is not returned
|
||||
any more through the Watcher APIs.
|
||||
- **CANCELLED** : the :ref:`Audit <audit_definition>` was in **PENDING** or
|
||||
**ONGOING** state and was cancelled by the
|
||||
:ref:`Administrator <administrator_definition>`
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
import pecan
|
||||
@@ -23,6 +57,7 @@ import wsme
|
||||
from wsme import types as wtypes
|
||||
import wsmeext.pecan as wsme_pecan
|
||||
|
||||
from watcher._i18n import _
|
||||
from watcher.api.controllers import base
|
||||
from watcher.api.controllers import link
|
||||
from watcher.api.controllers.v1 import collection
|
||||
@@ -181,6 +216,7 @@ class AuditCollection(collection.Collection):
|
||||
"""A list containing audits objects"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(AuditCollection, self).__init__()
|
||||
self._type = 'audits'
|
||||
|
||||
@staticmethod
|
||||
@@ -257,10 +293,9 @@ class AuditsController(rest.RestController):
|
||||
sort_key=sort_key,
|
||||
sort_dir=sort_dir)
|
||||
|
||||
@wsme_pecan.wsexpose(AuditCollection, types.uuid,
|
||||
types.uuid, int, wtypes.text,
|
||||
@wsme_pecan.wsexpose(AuditCollection, types.uuid, int, wtypes.text,
|
||||
wtypes.text, wtypes.text)
|
||||
def get_all(self, audit_uuid=None, marker=None, limit=None,
|
||||
def get_all(self, marker=None, limit=None,
|
||||
sort_key='id', sort_dir='asc', audit_template=None):
|
||||
"""Retrieve a list of audits.
|
||||
|
||||
@@ -275,13 +310,12 @@ class AuditsController(rest.RestController):
|
||||
sort_dir,
|
||||
audit_template=audit_template)
|
||||
|
||||
@wsme_pecan.wsexpose(AuditCollection, types.uuid,
|
||||
types.uuid, int, wtypes.text, wtypes.text)
|
||||
def detail(self, audit_uuid=None, marker=None, limit=None,
|
||||
@wsme_pecan.wsexpose(AuditCollection, types.uuid, int, wtypes.text,
|
||||
wtypes.text)
|
||||
def detail(self, marker=None, limit=None,
|
||||
sort_key='id', sort_dir='asc'):
|
||||
"""Retrieve a list of audits with detail.
|
||||
|
||||
:param audit_uuid: UUID of a audit, to get only audits for that audit.
|
||||
:param marker: pagination marker for large data sets.
|
||||
:param limit: maximum number of resources to return in a single result.
|
||||
:param sort_key: column to sort results by. Default: id.
|
||||
@@ -320,6 +354,11 @@ class AuditsController(rest.RestController):
|
||||
if self.from_audits:
|
||||
raise exception.OperationNotPermitted
|
||||
|
||||
if not audit._audit_template_uuid:
|
||||
raise exception.Invalid(
|
||||
message=_('The audit template UUID or name specified is '
|
||||
'invalid'))
|
||||
|
||||
audit_dict = audit.as_dict()
|
||||
context = pecan.request.context
|
||||
new_audit = objects.Audit(context, **audit_dict)
|
||||
|
||||
@@ -15,6 +15,39 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
An :ref:`Audit <audit_definition>` may be launched several times with the same
|
||||
settings (:ref:`Goal <goal_definition>`, thresholds, ...). Therefore it makes
|
||||
sense to save those settings in some sort of Audit preset object, which is
|
||||
known as an :ref:`Audit Template <audit_template_definition>`.
|
||||
|
||||
An :ref:`Audit Template <audit_template_definition>` contains at least the
|
||||
:ref:`Goal <goal_definition>` of the :ref:`Audit <audit_definition>`.
|
||||
|
||||
It may also contain some error handling settings indicating whether:
|
||||
|
||||
- :ref:`Watcher Applier <watcher_applier_definition>` stops the
|
||||
entire operation
|
||||
- :ref:`Watcher Applier <watcher_applier_definition>` performs a rollback
|
||||
|
||||
and how many retries should be attempted before failure occurs (also the latter
|
||||
can be complex: for example the scenario in which there are many first-time
|
||||
failures on ultimately successful :ref:`Actions <action_definition>`).
|
||||
|
||||
Moreover, an :ref:`Audit Template <audit_template_definition>` may contain some
|
||||
settings related to the level of automation for the
|
||||
:ref:`Action Plan <action_plan_definition>` that will be generated by the
|
||||
:ref:`Audit <audit_definition>`.
|
||||
A flag will indicate whether the :ref:`Action Plan <action_plan_definition>`
|
||||
will be launched automatically or will need a manual confirmation from the
|
||||
:ref:`Administrator <administrator_definition>`.
|
||||
|
||||
Last but not least, an :ref:`Audit Template <audit_template_definition>` may
|
||||
contain a list of extra parameters related to the
|
||||
:ref:`Strategy <strategy_definition>` configuration. These parameters can be
|
||||
provided as a list of key-value pairs.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
import pecan
|
||||
@@ -131,6 +164,7 @@ class AuditTemplateCollection(collection.Collection):
|
||||
"""A list containing audit templates objects"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(AuditTemplateCollection, self).__init__()
|
||||
self._type = 'audit_templates'
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -15,6 +15,23 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
A :ref:`Goal <goal_definition>` is a human readable, observable and measurable
|
||||
end result having one objective to be achieved.
|
||||
|
||||
Here are some examples of :ref:`Goals <goal_definition>`:
|
||||
|
||||
- minimize the energy consumption
|
||||
- minimize the number of compute nodes (consolidation)
|
||||
- balance the workload among compute nodes
|
||||
- minimize the license cost (some softwares have a licensing model which is
|
||||
based on the number of sockets or cores where the software is deployed)
|
||||
- find the most appropriate moment for a planned maintenance on a
|
||||
given group of host (which may be an entire availability zone):
|
||||
power supply replacement, cooling system replacement, hardware
|
||||
modification, ...
|
||||
"""
|
||||
|
||||
from oslo_config import cfg
|
||||
|
||||
import pecan
|
||||
@@ -94,6 +111,7 @@ class GoalCollection(collection.Collection):
|
||||
"""A list containing goals objects"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(GoalCollection, self).__init__()
|
||||
self._type = 'goals'
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -69,8 +69,7 @@ class ParsableErrorMiddleware(object):
|
||||
if (state['status_code'] // 100) not in (2, 3):
|
||||
req = webob.Request(environ)
|
||||
if (req.accept.best_match(['application/json', 'application/xml']
|
||||
) == 'application/xml'
|
||||
):
|
||||
) == 'application/xml'):
|
||||
try:
|
||||
# simple check xml is valid
|
||||
body = [et.ElementTree.tostring(
|
||||
|
||||
@@ -18,51 +18,51 @@
|
||||
#
|
||||
from oslo_log import log
|
||||
|
||||
from watcher.applier.action_plan.base import BaseActionPlanHandler
|
||||
from watcher.applier.default import DefaultApplier
|
||||
from watcher.applier.messaging.events import Events
|
||||
from watcher.common.messaging.events.event import Event
|
||||
from watcher.objects.action_plan import ActionPlan
|
||||
from watcher.objects.action_plan import Status
|
||||
from watcher.applier.action_plan import base
|
||||
from watcher.applier import default
|
||||
from watcher.applier.messaging import event_types
|
||||
from watcher.common.messaging.events import event
|
||||
from watcher.objects import action_plan as ap_objects
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class DefaultActionPlanHandler(BaseActionPlanHandler):
|
||||
def __init__(self, context, manager_applier, action_plan_uuid):
|
||||
class DefaultActionPlanHandler(base.BaseActionPlanHandler):
|
||||
def __init__(self, context, applier_manager, action_plan_uuid):
|
||||
super(DefaultActionPlanHandler, self).__init__()
|
||||
self.ctx = context
|
||||
self.action_plan_uuid = action_plan_uuid
|
||||
self.manager_applier = manager_applier
|
||||
self.applier_manager = applier_manager
|
||||
|
||||
def notify(self, uuid, event_type, state):
|
||||
action_plan = ActionPlan.get_by_uuid(self.ctx, uuid)
|
||||
action_plan = ap_objects.ActionPlan.get_by_uuid(self.ctx, uuid)
|
||||
action_plan.state = state
|
||||
action_plan.save()
|
||||
event = Event()
|
||||
event.type = event_type
|
||||
event.data = {}
|
||||
ev = event.Event()
|
||||
ev.type = event_type
|
||||
ev.data = {}
|
||||
payload = {'action_plan__uuid': uuid,
|
||||
'action_plan_state': state}
|
||||
self.manager_applier.topic_status.publish_event(event.type.name,
|
||||
self.applier_manager.topic_status.publish_event(ev.type.name,
|
||||
payload)
|
||||
|
||||
def execute(self):
|
||||
try:
|
||||
# update state
|
||||
self.notify(self.action_plan_uuid,
|
||||
Events.LAUNCH_ACTION_PLAN,
|
||||
Status.ONGOING)
|
||||
applier = DefaultApplier(self.manager_applier, self.ctx)
|
||||
event_types.EventTypes.LAUNCH_ACTION_PLAN,
|
||||
ap_objects.State.ONGOING)
|
||||
applier = default.DefaultApplier(self.applier_manager, self.ctx)
|
||||
result = applier.execute(self.action_plan_uuid)
|
||||
except Exception as e:
|
||||
LOG.exception(e)
|
||||
result = False
|
||||
LOG.error("Launch Action Plan " + unicode(e))
|
||||
finally:
|
||||
if result is True:
|
||||
status = Status.SUCCEEDED
|
||||
status = ap_objects.State.SUCCEEDED
|
||||
else:
|
||||
status = Status.FAILED
|
||||
status = ap_objects.State.FAILED
|
||||
# update state
|
||||
self.notify(self.action_plan_uuid, Events.LAUNCH_ACTION_PLAN,
|
||||
self.notify(self.action_plan_uuid,
|
||||
event_types.EventTypes.LAUNCH_ACTION_PLAN,
|
||||
status)
|
||||
|
||||
@@ -16,30 +16,47 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
|
||||
import abc
|
||||
|
||||
import six
|
||||
from watcher.decision_engine.strategy.common.level import StrategyLevel
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class BaseAction(object):
|
||||
def __init__(self):
|
||||
self._level = StrategyLevel.conservative
|
||||
self._priority = 0
|
||||
self._input_parameters = {}
|
||||
self._applies_to = ""
|
||||
|
||||
@property
|
||||
def level(self):
|
||||
return self._level
|
||||
def input_parameters(self):
|
||||
return self._input_parameters
|
||||
|
||||
@level.setter
|
||||
def level(self, l):
|
||||
self._level = l
|
||||
@input_parameters.setter
|
||||
def input_parameters(self, p):
|
||||
self._input_parameters = p
|
||||
|
||||
@property
|
||||
def priority(self):
|
||||
return self._priority
|
||||
def applies_to(self):
|
||||
return self._applies_to
|
||||
|
||||
@priority.setter
|
||||
def priority(self, p):
|
||||
self._priority = p
|
||||
@applies_to.setter
|
||||
def applies_to(self, a):
|
||||
self._applies_to = a
|
||||
|
||||
@abc.abstractmethod
|
||||
def execute(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def revert(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def precondition(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def postcondition(self):
|
||||
raise NotImplementedError()
|
||||
@@ -18,66 +18,55 @@
|
||||
#
|
||||
|
||||
|
||||
from oslo_config import cfg
|
||||
|
||||
|
||||
from watcher._i18n import _
|
||||
from watcher.applier.primitives.base import BasePrimitive
|
||||
from watcher.applier.promise import Promise
|
||||
from watcher.common.exception import IllegalArgumentException
|
||||
from watcher.common.keystone import KeystoneClient
|
||||
from watcher.common.nova import NovaClient
|
||||
from watcher.decision_engine.model.hypervisor_state import HypervisorState
|
||||
|
||||
CONF = cfg.CONF
|
||||
from watcher.applier.actions import base
|
||||
from watcher.common import exception
|
||||
from watcher.common import keystone as kclient
|
||||
from watcher.common import nova as nclient
|
||||
from watcher.decision_engine.model import hypervisor_state as hstate
|
||||
|
||||
|
||||
class ChangeNovaServiceState(BasePrimitive):
|
||||
def __init__(self, host, state):
|
||||
"""This class allows us to change the state of nova-compute service.
|
||||
|
||||
:param host: the uuid of the host
|
||||
:param state: (enabled/disabled)
|
||||
"""
|
||||
super(BasePrimitive, self).__init__()
|
||||
self._host = host
|
||||
self._state = state
|
||||
class ChangeNovaServiceState(base.BaseAction):
|
||||
|
||||
@property
|
||||
def host(self):
|
||||
return self._host
|
||||
return self.applies_to
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
return self._state
|
||||
return self.input_parameters.get('state')
|
||||
|
||||
@Promise
|
||||
def execute(self):
|
||||
target_state = None
|
||||
if self.state == HypervisorState.OFFLINE.value:
|
||||
if self.state == hstate.HypervisorState.OFFLINE.value:
|
||||
target_state = False
|
||||
elif self.status == HypervisorState.ONLINE.value:
|
||||
elif self.status == hstate.HypervisorState.ONLINE.value:
|
||||
target_state = True
|
||||
return self.nova_manage_service(target_state)
|
||||
|
||||
@Promise
|
||||
def undo(self):
|
||||
def revert(self):
|
||||
target_state = None
|
||||
if self.state == HypervisorState.OFFLINE.value:
|
||||
if self.state == hstate.HypervisorState.OFFLINE.value:
|
||||
target_state = True
|
||||
elif self.state == HypervisorState.ONLINE.value:
|
||||
elif self.state == hstate.HypervisorState.ONLINE.value:
|
||||
target_state = False
|
||||
return self.nova_manage_service(target_state)
|
||||
|
||||
def nova_manage_service(self, state):
|
||||
if state is None:
|
||||
raise IllegalArgumentException(
|
||||
_("The target state is not defined"))
|
||||
raise exception.IllegalArgumentException(
|
||||
message=_("The target state is not defined"))
|
||||
|
||||
keystone = KeystoneClient()
|
||||
wrapper = NovaClient(keystone.get_credentials(),
|
||||
session=keystone.get_session())
|
||||
keystone = kclient.KeystoneClient()
|
||||
wrapper = nclient.NovaClient(keystone.get_credentials(),
|
||||
session=keystone.get_session())
|
||||
if state is True:
|
||||
return wrapper.enable_service_nova_compute(self.host)
|
||||
else:
|
||||
return wrapper.disable_service_nova_compute(self.host)
|
||||
|
||||
def precondition(self):
|
||||
pass
|
||||
|
||||
def postcondition(self):
|
||||
pass
|
||||
36
watcher/applier/actions/factory.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# -*- 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.
|
||||
# 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
|
||||
|
||||
from oslo_log import log
|
||||
|
||||
from watcher.applier.actions.loading import default
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class ActionFactory(object):
|
||||
def __init__(self):
|
||||
self.action_loader = default.DefaultActionLoader()
|
||||
|
||||
def make_action(self, object_action):
|
||||
LOG.debug("Creating instance of %s", object_action.action_type)
|
||||
loaded_action = self.action_loader.load(name=object_action.action_type)
|
||||
loaded_action.input_parameters = object_action.input_parameters
|
||||
loaded_action.applies_to = object_action.applies_to
|
||||
return loaded_action
|
||||
@@ -13,12 +13,17 @@
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from watcher.decision_engine.solution.default import DefaultSolution
|
||||
from watcher.tests import base
|
||||
#
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from oslo_log import log
|
||||
|
||||
from watcher.common.loader import default
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class TestDefaultSolution(base.BaseTestCase):
|
||||
def test_default_solution(self):
|
||||
solution = DefaultSolution()
|
||||
solution.add_change_request("BLA")
|
||||
self.assertEqual(solution.actions[0], "BLA")
|
||||
class DefaultActionLoader(default.DefaultLoader):
|
||||
def __init__(self):
|
||||
super(DefaultActionLoader, self).__init__(namespace='watcher_actions')
|
||||
76
watcher/applier/actions/migration.py
Normal file
@@ -0,0 +1,76 @@
|
||||
# -*- 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_log import log
|
||||
|
||||
from watcher.applier.actions import base
|
||||
from watcher.common import exception
|
||||
from watcher.common import keystone as kclient
|
||||
from watcher.common import nova as nclient
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class Migrate(base.BaseAction):
|
||||
@property
|
||||
def instance_uuid(self):
|
||||
return self.applies_to
|
||||
|
||||
@property
|
||||
def migration_type(self):
|
||||
return self.input_parameters.get('migration_type')
|
||||
|
||||
@property
|
||||
def dst_hypervisor(self):
|
||||
return self.input_parameters.get('dst_hypervisor')
|
||||
|
||||
@property
|
||||
def src_hypervisor(self):
|
||||
return self.input_parameters.get('src_hypervisor')
|
||||
|
||||
def migrate(self, destination):
|
||||
keystone = kclient.KeystoneClient()
|
||||
wrapper = nclient.NovaClient(keystone.get_credentials(),
|
||||
session=keystone.get_session())
|
||||
LOG.debug("Migrate instance %s to %s ", self.instance_uuid,
|
||||
destination)
|
||||
instance = wrapper.find_instance(self.instance_uuid)
|
||||
if instance:
|
||||
if self.migration_type == 'live':
|
||||
return wrapper.live_migrate_instance(
|
||||
instance_id=self.instance_uuid, dest_hostname=destination)
|
||||
else:
|
||||
raise exception.InvalidParameterValue(err=self.migration_type)
|
||||
else:
|
||||
raise exception.InstanceNotFound(name=self.instance_uuid)
|
||||
|
||||
def execute(self):
|
||||
return self.migrate(destination=self.dst_hypervisor)
|
||||
|
||||
def revert(self):
|
||||
return self.migrate(destination=self.src_hypervisor)
|
||||
|
||||
def precondition(self):
|
||||
# todo(jed) check if the instance exist/ check if the instance is on
|
||||
# the src_hypervisor
|
||||
pass
|
||||
|
||||
def postcondition(self):
|
||||
# todo(jed) we can image to check extra parameters (nework reponse,ect)
|
||||
pass
|
||||
@@ -19,22 +19,28 @@
|
||||
|
||||
from oslo_log import log
|
||||
|
||||
|
||||
from watcher.applier.primitives.base import BasePrimitive
|
||||
from watcher.applier.promise import Promise
|
||||
from watcher.applier.actions import base
|
||||
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class Nop(BasePrimitive):
|
||||
class Nop(base.BaseAction):
|
||||
|
||||
@property
|
||||
def message(self):
|
||||
return self.input_parameters.get('message')
|
||||
|
||||
@Promise
|
||||
def execute(self):
|
||||
LOG.debug("executing NOP command")
|
||||
LOG.debug("executing action NOP message:%s ", self.message)
|
||||
return True
|
||||
|
||||
@Promise
|
||||
def undo(self):
|
||||
LOG.debug("undo NOP command")
|
||||
def revert(self):
|
||||
LOG.debug("revert action NOP")
|
||||
return True
|
||||
|
||||
def precondition(self):
|
||||
pass
|
||||
|
||||
def postcondition(self):
|
||||
pass
|
||||
@@ -16,17 +16,33 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
import time
|
||||
|
||||
from watcher.applier.primitives.base import BasePrimitive
|
||||
from watcher.applier.promise import Promise
|
||||
from oslo_log import log
|
||||
|
||||
from watcher.applier.actions import base
|
||||
|
||||
|
||||
class ChangePowerState(BasePrimitive):
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class Sleep(base.BaseAction):
|
||||
|
||||
@property
|
||||
def duration(self):
|
||||
return int(self.input_parameters.get('duration'))
|
||||
|
||||
@Promise
|
||||
def execute(self):
|
||||
raise NotImplementedError # pragma:no cover
|
||||
LOG.debug("Starting action Sleep duration:%s ", self.duration)
|
||||
time.sleep(self.duration)
|
||||
return True
|
||||
|
||||
@Promise
|
||||
def undo(self):
|
||||
raise NotImplementedError # pragma:no cover
|
||||
def revert(self):
|
||||
LOG.debug("revert action Sleep")
|
||||
return True
|
||||
|
||||
def precondition(self):
|
||||
pass
|
||||
|
||||
def postcondition(self):
|
||||
pass
|
||||
@@ -17,6 +17,14 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
"""
|
||||
This component is in charge of executing the
|
||||
:ref:`Action Plan <action_plan_definition>` built by the
|
||||
:ref:`Watcher Decision Engine <watcher_decision_engine_definition>`.
|
||||
|
||||
See: :doc:`../architecture` for more details on this component.
|
||||
"""
|
||||
|
||||
import abc
|
||||
import six
|
||||
|
||||
|
||||
@@ -16,25 +16,48 @@
|
||||
# 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.applier.base import BaseApplier
|
||||
from watcher.applier.execution.executor import ActionPlanExecutor
|
||||
from watcher.objects import Action
|
||||
from watcher.objects import ActionPlan
|
||||
from watcher.applier import base
|
||||
from watcher.applier.workflow_engine.loading import default
|
||||
from watcher import objects
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class DefaultApplier(BaseApplier):
|
||||
def __init__(self, manager_applier, context):
|
||||
class DefaultApplier(base.BaseApplier):
|
||||
def __init__(self, applier_manager, context):
|
||||
super(DefaultApplier, self).__init__()
|
||||
self.manager_applier = manager_applier
|
||||
self.context = context
|
||||
self.executor = ActionPlanExecutor(manager_applier, context)
|
||||
self._applier_manager = applier_manager
|
||||
self._loader = default.DefaultWorkFlowEngineLoader()
|
||||
self._engine = None
|
||||
self._context = context
|
||||
|
||||
@property
|
||||
def context(self):
|
||||
return self._context
|
||||
|
||||
@property
|
||||
def applier_manager(self):
|
||||
return self._applier_manager
|
||||
|
||||
@property
|
||||
def engine(self):
|
||||
if self._engine is None:
|
||||
selected_workflow_engine = CONF.watcher_applier.workflow_engine
|
||||
LOG.debug("Loading workflow engine %s ", selected_workflow_engine)
|
||||
self._engine = self._loader.load(name=selected_workflow_engine)
|
||||
self._engine.context = self.context
|
||||
self._engine.applier_manager = self.applier_manager
|
||||
return self._engine
|
||||
|
||||
def execute(self, action_plan_uuid):
|
||||
action_plan = ActionPlan.get_by_uuid(self.context, action_plan_uuid)
|
||||
LOG.debug("Executing action plan %s ", action_plan_uuid)
|
||||
action_plan = objects.ActionPlan.get_by_uuid(self.context,
|
||||
action_plan_uuid)
|
||||
# todo(jed) remove direct access to dbapi need filter in object
|
||||
actions = Action.dbapi.get_action_list(self.context,
|
||||
filters={
|
||||
'action_plan_id':
|
||||
action_plan.id})
|
||||
return self.executor.execute(actions)
|
||||
filters = {'action_plan_id': action_plan.id}
|
||||
actions = objects.Action.dbapi.get_action_list(self.context, filters)
|
||||
return self.engine.execute(actions)
|
||||
|
||||
@@ -1,56 +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_log import log
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class DeployPhase(object):
|
||||
def __init__(self, executor):
|
||||
# todo(jed) oslo_conf 10 secondes
|
||||
self._max_timeout = 100000
|
||||
self._actions = []
|
||||
self._executor = executor
|
||||
|
||||
@property
|
||||
def actions(self):
|
||||
return self._actions
|
||||
|
||||
@property
|
||||
def max_timeout(self):
|
||||
return self._max_timeout
|
||||
|
||||
@max_timeout.setter
|
||||
def max_timeout(self, m):
|
||||
self._max_timeout = m
|
||||
|
||||
def populate(self, action):
|
||||
self._actions.append(action)
|
||||
|
||||
def execute_primitive(self, primitive):
|
||||
future = primitive.execute(primitive)
|
||||
return future.result(self.max_timeout)
|
||||
|
||||
def rollback(self):
|
||||
reverted = sorted(self.actions, reverse=True)
|
||||
for primitive in reverted:
|
||||
try:
|
||||
self.execute_primitive(primitive)
|
||||
except Exception as e:
|
||||
LOG.error(e)
|
||||
@@ -1,76 +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_log import log
|
||||
from watcher.applier.execution.deploy_phase import DeployPhase
|
||||
from watcher.applier.mapping.default import DefaultActionMapper
|
||||
from watcher.applier.messaging.events import Events
|
||||
from watcher.common.messaging.events.event import Event
|
||||
from watcher.objects import Action
|
||||
from watcher.objects.action_plan import Status
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class ActionPlanExecutor(object):
|
||||
def __init__(self, manager_applier, context):
|
||||
self.manager_applier = manager_applier
|
||||
self.context = context
|
||||
self.deploy = DeployPhase(self)
|
||||
self.mapper = DefaultActionMapper()
|
||||
|
||||
def get_primitive(self, action):
|
||||
return self.mapper.build_primitive_from_action(action)
|
||||
|
||||
def notify(self, action, state):
|
||||
db_action = Action.get_by_uuid(self.context, action.uuid)
|
||||
db_action.state = state
|
||||
db_action.save()
|
||||
event = Event()
|
||||
event.type = Events.LAUNCH_ACTION
|
||||
event.data = {}
|
||||
payload = {'action_uuid': action.uuid,
|
||||
'action_state': state}
|
||||
self.manager_applier.topic_status.publish_event(event.type.name,
|
||||
payload)
|
||||
|
||||
def execute(self, actions):
|
||||
for action in actions:
|
||||
try:
|
||||
self.notify(action, Status.ONGOING)
|
||||
primitive = self.get_primitive(action)
|
||||
result = self.deploy.execute_primitive(primitive)
|
||||
if result is False:
|
||||
self.notify(action, Status.FAILED)
|
||||
self.deploy.rollback()
|
||||
return False
|
||||
else:
|
||||
self.deploy.populate(primitive)
|
||||
self.notify(action, Status.SUCCEEDED)
|
||||
except Exception as e:
|
||||
LOG.debug(
|
||||
'The applier module failed to execute the action{0} with '
|
||||
'the exception {1} '.format(
|
||||
action,
|
||||
unicode(e)))
|
||||
|
||||
LOG.error("Trigger a rollback")
|
||||
self.notify(action, Status.FAILED)
|
||||
self.deploy.rollback()
|
||||
return False
|
||||
return True
|
||||
@@ -16,20 +16,24 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
|
||||
from watcher.applier.messaging.trigger import TriggerActionPlan
|
||||
from watcher.common.messaging.messaging_core import MessagingCore
|
||||
from watcher.applier.messaging import trigger
|
||||
from watcher.common.messaging import messaging_core
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
# Register options
|
||||
APPLIER_MANAGER_OPTS = [
|
||||
cfg.IntOpt('applier_worker', default='1', help='The number of worker'),
|
||||
cfg.IntOpt('workers',
|
||||
default='1',
|
||||
min=1,
|
||||
required=True,
|
||||
help='Number of workers for applier, default value is 1.'),
|
||||
cfg.StrOpt('topic_control',
|
||||
default='watcher.applier.control',
|
||||
help='The topic name used for'
|
||||
@@ -45,7 +49,11 @@ APPLIER_MANAGER_OPTS = [
|
||||
cfg.StrOpt('publisher_id',
|
||||
default='watcher.applier.api',
|
||||
help='The identifier used by watcher '
|
||||
'module on the message broker')
|
||||
'module on the message broker'),
|
||||
cfg.StrOpt('workflow_engine',
|
||||
default='taskflow',
|
||||
required=True,
|
||||
help='Select the engine to use to execute the workflow')
|
||||
]
|
||||
|
||||
opt_group = cfg.OptGroup(name='watcher_applier',
|
||||
@@ -55,7 +63,7 @@ CONF.register_group(opt_group)
|
||||
CONF.register_opts(APPLIER_MANAGER_OPTS, opt_group)
|
||||
|
||||
|
||||
class ApplierManager(MessagingCore):
|
||||
class ApplierManager(messaging_core.MessagingCore):
|
||||
def __init__(self):
|
||||
super(ApplierManager, self).__init__(
|
||||
CONF.watcher_applier.publisher_id,
|
||||
@@ -63,10 +71,7 @@ class ApplierManager(MessagingCore):
|
||||
CONF.watcher_applier.topic_status,
|
||||
api_version=self.API_VERSION,
|
||||
)
|
||||
# shared executor of the workflow
|
||||
self.executor = ThreadPoolExecutor(max_workers=1)
|
||||
# trigger action_plan
|
||||
self.topic_control.add_endpoint(TriggerActionPlan(self))
|
||||
self.topic_control.add_endpoint(trigger.TriggerActionPlan(self))
|
||||
|
||||
def join(self):
|
||||
self.topic_control.join()
|
||||
|
||||
@@ -1,33 +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.
|
||||
#
|
||||
|
||||
import abc
|
||||
import six
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class BaseActionMapper(object):
|
||||
@abc.abstractmethod
|
||||
def build_primitive_from_action(self, action):
|
||||
"""Transform an action to a primitive
|
||||
|
||||
:type action: watcher.decision_engine.action.BaseAction
|
||||
:return: the associated Primitive
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
@@ -1,47 +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.applier.mapping.base import BaseActionMapper
|
||||
from watcher.applier.primitives.change_nova_service_state import \
|
||||
ChangeNovaServiceState
|
||||
from watcher.applier.primitives.migration import Migrate
|
||||
from watcher.applier.primitives.nop import Nop
|
||||
from watcher.applier.primitives.power_state import ChangePowerState
|
||||
from watcher.common.exception import ActionNotFound
|
||||
from watcher.decision_engine.planner.default import Primitives
|
||||
|
||||
|
||||
class DefaultActionMapper(BaseActionMapper):
|
||||
def build_primitive_from_action(self, action):
|
||||
if action.action_type == Primitives.COLD_MIGRATE.value:
|
||||
return Migrate(action.applies_to, Primitives.COLD_MIGRATE,
|
||||
action.src,
|
||||
action.dst)
|
||||
elif action.action_type == Primitives.LIVE_MIGRATE.value:
|
||||
return Migrate(action.applies_to, Primitives.COLD_MIGRATE,
|
||||
action.src,
|
||||
action.dst)
|
||||
elif action.action_type == Primitives.HYPERVISOR_STATE.value:
|
||||
return ChangeNovaServiceState(action.applies_to, action.parameter)
|
||||
elif action.action_type == Primitives.POWER_STATE.value:
|
||||
return ChangePowerState()
|
||||
elif action.action_type == Primitives.NOP.value:
|
||||
return Nop()
|
||||
else:
|
||||
raise ActionNotFound()
|
||||
@@ -17,9 +17,9 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
from enum import Enum
|
||||
import enum
|
||||
|
||||
|
||||
class Events(Enum):
|
||||
class EventTypes(enum.Enum):
|
||||
LAUNCH_ACTION_PLAN = "launch_action_plan"
|
||||
LAUNCH_ACTION = "launch_action"
|
||||
@@ -16,30 +16,35 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
from concurrent import futures
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
|
||||
from watcher.applier.action_plan.default import DefaultActionPlanHandler
|
||||
from watcher.applier.action_plan import default
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class TriggerActionPlan(object):
|
||||
def __init__(self, manager_applier):
|
||||
self.manager_applier = manager_applier
|
||||
def __init__(self, applier_manager):
|
||||
self.applier_manager = applier_manager
|
||||
workers = CONF.watcher_applier.workers
|
||||
self.executor = futures.ThreadPoolExecutor(max_workers=workers)
|
||||
|
||||
def do_launch_action_plan(self, context, action_plan_uuid):
|
||||
try:
|
||||
cmd = DefaultActionPlanHandler(context,
|
||||
self.manager_applier,
|
||||
action_plan_uuid)
|
||||
cmd = default.DefaultActionPlanHandler(context,
|
||||
self.applier_manager,
|
||||
action_plan_uuid)
|
||||
cmd.execute()
|
||||
except Exception as e:
|
||||
LOG.exception(e)
|
||||
|
||||
def launch_action_plan(self, context, action_plan_uuid):
|
||||
LOG.debug("Trigger ActionPlan %s" % action_plan_uuid)
|
||||
LOG.debug("Trigger ActionPlan %s", action_plan_uuid)
|
||||
# submit
|
||||
self.manager_applier.executor.submit(self.do_launch_action_plan,
|
||||
context,
|
||||
action_plan_uuid)
|
||||
self.executor.submit(self.do_launch_action_plan, context,
|
||||
action_plan_uuid)
|
||||
return action_plan_uuid
|
||||
|
||||
@@ -1,88 +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 keystoneclient.auth.identity import v3
|
||||
from keystoneclient import session
|
||||
from oslo_config import cfg
|
||||
|
||||
from watcher.applier.primitives.base import BasePrimitive
|
||||
from watcher.applier.promise import Promise
|
||||
from watcher.common.keystone import KeystoneClient
|
||||
from watcher.common.nova import NovaClient
|
||||
from watcher.decision_engine.planner.default import Primitives
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class Migrate(BasePrimitive):
|
||||
def __init__(self, vm_uuid=None,
|
||||
migration_type=None,
|
||||
source_hypervisor=None,
|
||||
destination_hypervisor=None):
|
||||
super(BasePrimitive, self).__init__()
|
||||
self.instance_uuid = vm_uuid
|
||||
self.migration_type = migration_type
|
||||
self.source_hypervisor = source_hypervisor
|
||||
self.destination_hypervisor = destination_hypervisor
|
||||
|
||||
def migrate(self, destination):
|
||||
keystone = KeystoneClient()
|
||||
wrapper = NovaClient(keystone.get_credentials(),
|
||||
session=keystone.get_session())
|
||||
instance = wrapper.find_instance(self.instance_uuid)
|
||||
if instance:
|
||||
project_id = getattr(instance, "tenant_id")
|
||||
|
||||
creds2 = \
|
||||
{'auth_url': CONF.keystone_authtoken.auth_uri,
|
||||
'username': CONF.keystone_authtoken.admin_user,
|
||||
'password': CONF.keystone_authtoken.admin_password,
|
||||
'project_id': project_id,
|
||||
'user_domain_name': "default",
|
||||
'project_domain_name': "default"}
|
||||
auth2 = v3.Password(auth_url=creds2['auth_url'],
|
||||
username=creds2['username'],
|
||||
password=creds2['password'],
|
||||
project_id=creds2['project_id'],
|
||||
user_domain_name=creds2[
|
||||
'user_domain_name'],
|
||||
project_domain_name=creds2[
|
||||
'project_domain_name'])
|
||||
sess2 = session.Session(auth=auth2)
|
||||
wrapper2 = NovaClient(creds2, session=sess2)
|
||||
|
||||
# todo(jed) remove Primitves
|
||||
if self.migration_type is Primitives.COLD_MIGRATE:
|
||||
return wrapper2.live_migrate_instance(
|
||||
instance_id=self.instance_uuid,
|
||||
dest_hostname=destination,
|
||||
block_migration=True)
|
||||
elif self.migration_type is Primitives.LIVE_MIGRATE:
|
||||
return wrapper2.live_migrate_instance(
|
||||
instance_id=self.instance_uuid,
|
||||
dest_hostname=destination,
|
||||
block_migration=False)
|
||||
|
||||
@Promise
|
||||
def execute(self):
|
||||
return self.migrate(self.destination_hypervisor)
|
||||
|
||||
@Promise
|
||||
def undo(self):
|
||||
return self.migrate(self.source_hypervisor)
|
||||
@@ -1,50 +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 concurrent.futures import Future
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
|
||||
class Promise(object):
|
||||
executor = ThreadPoolExecutor(
|
||||
max_workers=10)
|
||||
|
||||
def __init__(self, func):
|
||||
self.func = func
|
||||
|
||||
def resolve(self, *args, **kwargs):
|
||||
resolved_args = []
|
||||
resolved_kwargs = {}
|
||||
|
||||
for i, arg in enumerate(args):
|
||||
if isinstance(arg, Future):
|
||||
resolved_args.append(arg.result())
|
||||
else:
|
||||
resolved_args.append(arg)
|
||||
|
||||
for kw, arg in kwargs.items():
|
||||
if isinstance(arg, Future):
|
||||
resolved_kwargs[kw] = arg.result()
|
||||
else:
|
||||
resolved_kwargs[kw] = arg
|
||||
|
||||
return self.func(*resolved_args, **resolved_kwargs)
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
return self.executor.submit(self.resolve, *args, **kwargs)
|
||||
@@ -23,8 +23,8 @@ import oslo_messaging as om
|
||||
from watcher.applier.manager import APPLIER_MANAGER_OPTS
|
||||
from watcher.applier.manager import opt_group
|
||||
from watcher.common import exception
|
||||
from watcher.common.messaging.messaging_core import MessagingCore
|
||||
from watcher.common.messaging.notification_handler import NotificationHandler
|
||||
from watcher.common.messaging import messaging_core
|
||||
from watcher.common.messaging import notification_handler as notification
|
||||
from watcher.common import utils
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ CONF.register_group(opt_group)
|
||||
CONF.register_opts(APPLIER_MANAGER_OPTS, opt_group)
|
||||
|
||||
|
||||
class ApplierAPI(MessagingCore):
|
||||
class ApplierAPI(messaging_core.MessagingCore):
|
||||
|
||||
def __init__(self):
|
||||
super(ApplierAPI, self).__init__(
|
||||
@@ -43,7 +43,7 @@ class ApplierAPI(MessagingCore):
|
||||
CONF.watcher_applier.topic_status,
|
||||
api_version=self.API_VERSION,
|
||||
)
|
||||
self.handler = NotificationHandler(self.publisher_id)
|
||||
self.handler = notification.NotificationHandler(self.publisher_id)
|
||||
self.handler.register_observer(self)
|
||||
self.topic_status.add_endpoint(self.handler)
|
||||
transport = om.get_transport(CONF)
|
||||
|
||||
70
watcher/applier/workflow_engine/base.py
Normal file
@@ -0,0 +1,70 @@
|
||||
# -*- 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.
|
||||
# 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
|
||||
|
||||
import six
|
||||
|
||||
from watcher.applier.actions import factory
|
||||
from watcher.applier.messaging import event_types
|
||||
from watcher.common.messaging.events import event
|
||||
from watcher import objects
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class BaseWorkFlowEngine(object):
|
||||
def __init__(self):
|
||||
self._applier_manager = None
|
||||
self._context = None
|
||||
self._action_factory = factory.ActionFactory()
|
||||
|
||||
@property
|
||||
def context(self):
|
||||
return self._context
|
||||
|
||||
@context.setter
|
||||
def context(self, c):
|
||||
self._context = c
|
||||
|
||||
@property
|
||||
def applier_manager(self):
|
||||
return self._applier_manager
|
||||
|
||||
@applier_manager.setter
|
||||
def applier_manager(self, a):
|
||||
self._applier_manager = a
|
||||
|
||||
@property
|
||||
def action_factory(self):
|
||||
return self._action_factory
|
||||
|
||||
def notify(self, action, state):
|
||||
db_action = objects.Action.get_by_uuid(self.context, action.uuid)
|
||||
db_action.state = state
|
||||
db_action.save()
|
||||
ev = event.Event()
|
||||
ev.type = event_types.EventTypes.LAUNCH_ACTION
|
||||
ev.data = {}
|
||||
payload = {'action_uuid': action.uuid,
|
||||
'action_state': state}
|
||||
self.applier_manager.topic_status.publish_event(ev.type.name,
|
||||
payload)
|
||||
|
||||
@abc.abstractmethod
|
||||
def execute(self, actions):
|
||||
raise NotImplementedError()
|
||||
159
watcher/applier/workflow_engine/default.py
Normal file
@@ -0,0 +1,159 @@
|
||||
# -*- 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.
|
||||
# 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 taskflow import engines
|
||||
from taskflow.patterns import graph_flow as gf
|
||||
from taskflow import task
|
||||
|
||||
from watcher._i18n import _LE, _LW, _LC
|
||||
from watcher.applier.workflow_engine import base
|
||||
from watcher.objects import action as obj_action
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class DefaultWorkFlowEngine(base.BaseWorkFlowEngine):
|
||||
def decider(self, history):
|
||||
# FIXME(jed) not possible with the current Watcher Planner
|
||||
#
|
||||
# decider – A callback function that will be expected to
|
||||
# decide at runtime whether v should be allowed to execute
|
||||
# (or whether the execution of v should be ignored,
|
||||
# and therefore not executed). It is expected to take as single
|
||||
# keyword argument history which will be the execution results of
|
||||
# all u decideable links that have v as a target. It is expected
|
||||
# to return a single boolean
|
||||
# (True to allow v execution or False to not).
|
||||
return True
|
||||
|
||||
def execute(self, actions):
|
||||
try:
|
||||
# NOTE(jed) We want to have a strong separation of concern
|
||||
# between the Watcher planner and the Watcher Applier in order
|
||||
# to us the possibility to support several workflow engine.
|
||||
# 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.
|
||||
# todo(jed) add olso conf for retry and name
|
||||
flow = gf.Flow("watcher_flow")
|
||||
previous = None
|
||||
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
|
||||
|
||||
e = engines.load(flow)
|
||||
e.run()
|
||||
return True
|
||||
except Exception as e:
|
||||
LOG.exception(e)
|
||||
return False
|
||||
|
||||
|
||||
class TaskFlowActionContainer(task.Task):
|
||||
def __init__(self, db_action, engine):
|
||||
name = "action_type:{0} uuid:{1}".format(db_action.action_type,
|
||||
db_action.uuid)
|
||||
super(TaskFlowActionContainer, self).__init__(name=name)
|
||||
self._db_action = db_action
|
||||
self._engine = engine
|
||||
self.loaded_action = None
|
||||
|
||||
@property
|
||||
def action(self):
|
||||
if self.loaded_action is None:
|
||||
action = self.engine.action_factory.make_action(self._db_action)
|
||||
self.loaded_action = action
|
||||
return self.loaded_action
|
||||
|
||||
@property
|
||||
def engine(self):
|
||||
return self._engine
|
||||
|
||||
def pre_execute(self):
|
||||
try:
|
||||
self.engine.notify(self._db_action,
|
||||
obj_action.State.ONGOING)
|
||||
LOG.debug("Precondition action %s", self.name)
|
||||
self.action.precondition()
|
||||
except Exception as e:
|
||||
LOG.exception(e)
|
||||
self.engine.notify(self._db_action,
|
||||
obj_action.State.FAILED)
|
||||
raise
|
||||
|
||||
def execute(self, *args, **kwargs):
|
||||
try:
|
||||
LOG.debug("Running action %s", self.name)
|
||||
|
||||
# todo(jed) remove return (true or false) raise an Exception
|
||||
result = self.action.execute()
|
||||
if result is not True:
|
||||
self.engine.notify(self._db_action,
|
||||
obj_action.State.FAILED)
|
||||
else:
|
||||
self.engine.notify(self._db_action,
|
||||
obj_action.State.SUCCEEDED)
|
||||
except Exception as e:
|
||||
LOG.exception(e)
|
||||
LOG.error(_LE('The WorkFlow Engine has failed '
|
||||
'to execute the action %s'), self.name)
|
||||
|
||||
self.engine.notify(self._db_action,
|
||||
obj_action.State.FAILED)
|
||||
raise
|
||||
|
||||
def post_execute(self):
|
||||
try:
|
||||
LOG.debug("postcondition action %s", self.name)
|
||||
self.action.postcondition()
|
||||
except Exception as e:
|
||||
LOG.exception(e)
|
||||
self.engine.notify(self._db_action,
|
||||
obj_action.State.FAILED)
|
||||
raise
|
||||
|
||||
def revert(self, *args, **kwargs):
|
||||
LOG.warning(_LW("Revert action %s"), self.name)
|
||||
try:
|
||||
# todo(jed) do we need to update the states in case of failure ?
|
||||
self.action.revert()
|
||||
except Exception as e:
|
||||
LOG.exception(e)
|
||||
LOG.critical(_LC("Oops! We need disaster recover plan"))
|
||||
|
||||
|
||||
class TaskFlowNop(task.Task):
|
||||
"""This class is use in case of the workflow have only one Action.
|
||||
|
||||
We need at least two atoms to create a link
|
||||
"""
|
||||
def execute(self):
|
||||
pass
|
||||
30
watcher/applier/workflow_engine/loading/default.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# -*- 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 __future__ import unicode_literals
|
||||
|
||||
from oslo_log import log
|
||||
|
||||
from watcher.common.loader import default
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class DefaultWorkFlowEngineLoader(default.DefaultLoader):
|
||||
def __init__(self):
|
||||
super(DefaultWorkFlowEngineLoader, self).__init__(
|
||||
namespace='watcher_workflow_engines')
|
||||
@@ -49,12 +49,12 @@ class CeilometerClient(object):
|
||||
This query can be then used for querying resources, meters and
|
||||
statistics.
|
||||
:Parameters:
|
||||
- `user_id`: user_id, has a priority over list of ids
|
||||
- `tenant_id`: tenant_id, has a priority over list of ids
|
||||
- `resource_id`: resource_id, has a priority over list of ids
|
||||
- `user_ids`: list of user_ids
|
||||
- `tenant_ids`: list of tenant_ids
|
||||
- `resource_ids`: list of resource_ids
|
||||
- `user_id`: user_id, has a priority over list of ids
|
||||
- `tenant_id`: tenant_id, has a priority over list of ids
|
||||
- `resource_id`: resource_id, has a priority over list of ids
|
||||
- `user_ids`: list of user_ids
|
||||
- `tenant_ids`: list of tenant_ids
|
||||
- `resource_ids`: list of resource_ids
|
||||
"""
|
||||
|
||||
user_ids = user_ids or []
|
||||
|
||||
@@ -40,20 +40,15 @@ CONF = cfg.CONF
|
||||
CONF.register_opts(exc_log_opts)
|
||||
|
||||
|
||||
def _cleanse_dict(original):
|
||||
"""Strip all admin_password, new_pass, rescue_pass keys from a dict"""
|
||||
return dict((k, v) for k, v in six.iteritems(original) if "_pass" not in k)
|
||||
|
||||
|
||||
class WatcherException(Exception):
|
||||
"""Base Watcher Exception
|
||||
|
||||
To correctly use this class, inherit from it and define
|
||||
a 'message' property. That message will get printf'd
|
||||
a 'msg_fmt' property. That msg_fmt will get printf'd
|
||||
with the keyword arguments provided to the constructor.
|
||||
|
||||
"""
|
||||
message = _("An unknown exception occurred")
|
||||
msg_fmt = _("An unknown exception occurred")
|
||||
code = 500
|
||||
headers = {}
|
||||
safe = False
|
||||
@@ -69,20 +64,19 @@ class WatcherException(Exception):
|
||||
|
||||
if not message:
|
||||
try:
|
||||
message = self.message % kwargs
|
||||
|
||||
message = self.msg_fmt % kwargs
|
||||
except Exception as e:
|
||||
# kwargs doesn't match a variable in the message
|
||||
# kwargs doesn't match a variable in msg_fmt
|
||||
# log the issue and the kwargs
|
||||
LOG.exception(_LE('Exception in string format operation'))
|
||||
for name, value in six.iteritems(kwargs):
|
||||
for name, value in kwargs.items():
|
||||
LOG.error("%s: %s", name, value)
|
||||
|
||||
if CONF.fatal_exception_format_errors:
|
||||
raise e
|
||||
else:
|
||||
# at least get the core message out if something happened
|
||||
message = self.message
|
||||
# at least get the core msg_fmt out if something happened
|
||||
message = self.msg_fmt
|
||||
|
||||
super(WatcherException, self).__init__(message)
|
||||
|
||||
@@ -94,7 +88,7 @@ class WatcherException(Exception):
|
||||
return self.args[0]
|
||||
|
||||
def __unicode__(self):
|
||||
return self.message
|
||||
return unicode(self.args[0])
|
||||
|
||||
def format_message(self):
|
||||
if self.__class__.__name__.endswith('_Remote'):
|
||||
@@ -104,114 +98,114 @@ class WatcherException(Exception):
|
||||
|
||||
|
||||
class NotAuthorized(WatcherException):
|
||||
message = _("Not authorized")
|
||||
msg_fmt = _("Not authorized")
|
||||
code = 403
|
||||
|
||||
|
||||
class OperationNotPermitted(NotAuthorized):
|
||||
message = _("Operation not permitted")
|
||||
msg_fmt = _("Operation not permitted")
|
||||
|
||||
|
||||
class Invalid(WatcherException):
|
||||
message = _("Unacceptable parameters")
|
||||
msg_fmt = _("Unacceptable parameters")
|
||||
code = 400
|
||||
|
||||
|
||||
class ObjectNotFound(WatcherException):
|
||||
message = _("The %(name)s %(id)s could not be found")
|
||||
msg_fmt = _("The %(name)s %(id)s could not be found")
|
||||
|
||||
|
||||
class Conflict(WatcherException):
|
||||
message = _('Conflict')
|
||||
msg_fmt = _('Conflict')
|
||||
code = 409
|
||||
|
||||
|
||||
class ResourceNotFound(ObjectNotFound):
|
||||
message = _("The %(name)s resource %(id)s could not be found")
|
||||
msg_fmt = _("The %(name)s resource %(id)s could not be found")
|
||||
code = 404
|
||||
|
||||
|
||||
class InvalidIdentity(Invalid):
|
||||
message = _("Expected an uuid or int but received %(identity)s")
|
||||
msg_fmt = _("Expected an uuid or int but received %(identity)s")
|
||||
|
||||
|
||||
class InvalidGoal(Invalid):
|
||||
message = _("Goal %(goal)s is not defined in Watcher configuration file")
|
||||
msg_fmt = _("Goal %(goal)s is not defined in Watcher configuration file")
|
||||
|
||||
|
||||
# Cannot be templated as the error syntax varies.
|
||||
# msg needs to be constructed when raised.
|
||||
class InvalidParameterValue(Invalid):
|
||||
message = _("%(err)s")
|
||||
msg_fmt = _("%(err)s")
|
||||
|
||||
|
||||
class InvalidUUID(Invalid):
|
||||
message = _("Expected a uuid but received %(uuid)s")
|
||||
msg_fmt = _("Expected a uuid but received %(uuid)s")
|
||||
|
||||
|
||||
class InvalidName(Invalid):
|
||||
message = _("Expected a logical name but received %(name)s")
|
||||
msg_fmt = _("Expected a logical name but received %(name)s")
|
||||
|
||||
|
||||
class InvalidUuidOrName(Invalid):
|
||||
message = _("Expected a logical name or uuid but received %(name)s")
|
||||
msg_fmt = _("Expected a logical name or uuid but received %(name)s")
|
||||
|
||||
|
||||
class AuditTemplateNotFound(ResourceNotFound):
|
||||
message = _("AuditTemplate %(audit_template)s could not be found")
|
||||
msg_fmt = _("AuditTemplate %(audit_template)s could not be found")
|
||||
|
||||
|
||||
class AuditTemplateAlreadyExists(Conflict):
|
||||
message = _("An audit_template with UUID %(uuid)s or name %(name)s "
|
||||
msg_fmt = _("An audit_template with UUID %(uuid)s or name %(name)s "
|
||||
"already exists")
|
||||
|
||||
|
||||
class AuditTemplateReferenced(Invalid):
|
||||
message = _("AuditTemplate %(audit_template)s is referenced by one or "
|
||||
msg_fmt = _("AuditTemplate %(audit_template)s is referenced by one or "
|
||||
"multiple audit")
|
||||
|
||||
|
||||
class AuditNotFound(ResourceNotFound):
|
||||
message = _("Audit %(audit)s could not be found")
|
||||
msg_fmt = _("Audit %(audit)s could not be found")
|
||||
|
||||
|
||||
class AuditAlreadyExists(Conflict):
|
||||
message = _("An audit with UUID %(uuid)s already exists")
|
||||
msg_fmt = _("An audit with UUID %(uuid)s already exists")
|
||||
|
||||
|
||||
class AuditReferenced(Invalid):
|
||||
message = _("Audit %(audit)s is referenced by one or multiple action "
|
||||
msg_fmt = _("Audit %(audit)s is referenced by one or multiple action "
|
||||
"plans")
|
||||
|
||||
|
||||
class ActionPlanNotFound(ResourceNotFound):
|
||||
message = _("ActionPlan %(action plan)s could not be found")
|
||||
msg_fmt = _("ActionPlan %(action plan)s could not be found")
|
||||
|
||||
|
||||
class ActionPlanAlreadyExists(Conflict):
|
||||
message = _("An action plan with UUID %(uuid)s already exists")
|
||||
msg_fmt = _("An action plan with UUID %(uuid)s already exists")
|
||||
|
||||
|
||||
class ActionPlanReferenced(Invalid):
|
||||
message = _("Action Plan %(action_plan)s is referenced by one or "
|
||||
msg_fmt = _("Action Plan %(action_plan)s is referenced by one or "
|
||||
"multiple actions")
|
||||
|
||||
|
||||
class ActionNotFound(ResourceNotFound):
|
||||
message = _("Action %(action)s could not be found")
|
||||
msg_fmt = _("Action %(action)s could not be found")
|
||||
|
||||
|
||||
class ActionAlreadyExists(Conflict):
|
||||
message = _("An action with UUID %(uuid)s already exists")
|
||||
msg_fmt = _("An action with UUID %(uuid)s already exists")
|
||||
|
||||
|
||||
class ActionReferenced(Invalid):
|
||||
message = _("Action plan %(action_plan)s is referenced by one or "
|
||||
msg_fmt = _("Action plan %(action_plan)s is referenced by one or "
|
||||
"multiple goals")
|
||||
|
||||
|
||||
class ActionFilterCombinationProhibited(Invalid):
|
||||
message = _("Filtering actions on both audit and action-plan is "
|
||||
msg_fmt = _("Filtering actions on both audit and action-plan is "
|
||||
"prohibited")
|
||||
|
||||
|
||||
@@ -220,84 +214,49 @@ class HTTPNotFound(ResourceNotFound):
|
||||
|
||||
|
||||
class PatchError(Invalid):
|
||||
message = _("Couldn't apply patch '%(patch)s'. Reason: %(reason)s")
|
||||
msg_fmt = _("Couldn't apply patch '%(patch)s'. Reason: %(reason)s")
|
||||
|
||||
|
||||
# decision engine
|
||||
|
||||
|
||||
class BaseException(Exception):
|
||||
|
||||
def __init__(self, desc=""):
|
||||
if (not isinstance(desc, six.string_types)):
|
||||
raise ValueError(_("Description must be an instance of str"))
|
||||
|
||||
desc = desc.strip()
|
||||
|
||||
self._desc = desc
|
||||
|
||||
def get_description(self):
|
||||
return self._desc
|
||||
|
||||
def get_message(self):
|
||||
return _("An exception occurred without a description")
|
||||
|
||||
def __str__(self):
|
||||
return self.get_message()
|
||||
class IllegalArgumentException(WatcherException):
|
||||
msg_fmt = _('Illegal argument')
|
||||
|
||||
|
||||
class IllegalArgumentException(BaseException):
|
||||
def __init__(self, desc):
|
||||
desc = desc or _("Description cannot be empty")
|
||||
super(IllegalArgumentException, self).__init__(desc)
|
||||
|
||||
def get_message(self):
|
||||
return self._desc
|
||||
class NoSuchMetric(WatcherException):
|
||||
msg_fmt = _('No such metric')
|
||||
|
||||
|
||||
class NoSuchMetric(BaseException):
|
||||
def __init__(self, desc):
|
||||
desc = desc or _("No such metric")
|
||||
super(NoSuchMetric, self).__init__(desc)
|
||||
|
||||
def get_message(self):
|
||||
return self._desc
|
||||
|
||||
|
||||
class NoDataFound(BaseException):
|
||||
def __init__(self, desc):
|
||||
desc = desc or _("No rows were returned")
|
||||
super(NoDataFound, self).__init__(desc)
|
||||
|
||||
def get_message(self):
|
||||
return self._desc
|
||||
class NoDataFound(WatcherException):
|
||||
msg_fmt = _('No rows were returned')
|
||||
|
||||
|
||||
class KeystoneFailure(WatcherException):
|
||||
message = _("'Keystone API endpoint is missing''")
|
||||
msg_fmt = _("'Keystone API endpoint is missing''")
|
||||
|
||||
|
||||
class ClusterEmpty(WatcherException):
|
||||
message = _("The list of hypervisor(s) in the cluster is empty")
|
||||
msg_fmt = _("The list of hypervisor(s) in the cluster is empty")
|
||||
|
||||
|
||||
class MetricCollectorNotDefined(WatcherException):
|
||||
message = _("The metrics resource collector is not defined")
|
||||
msg_fmt = _("The metrics resource collector is not defined")
|
||||
|
||||
|
||||
class ClusterStateNotDefined(WatcherException):
|
||||
message = _("the cluster state is not defined")
|
||||
msg_fmt = _("the cluster state is not defined")
|
||||
|
||||
|
||||
# Model
|
||||
|
||||
class VMNotFound(WatcherException):
|
||||
message = _("The VM could not be found")
|
||||
class InstanceNotFound(WatcherException):
|
||||
msg_fmt = _("The instance '%(name)s' is not found")
|
||||
|
||||
|
||||
class HypervisorNotFound(WatcherException):
|
||||
message = _("The hypervisor could not be found")
|
||||
msg_fmt = _("The hypervisor is not found")
|
||||
|
||||
|
||||
class MetaActionNotFound(WatcherException):
|
||||
message = _("The Meta-Action could not be found")
|
||||
class LoadingError(WatcherException):
|
||||
msg_fmt = _("Error loading plugin '%(name)s'")
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
# -*- 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
|
||||
# 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,
|
||||
@@ -15,20 +13,20 @@
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import abc
|
||||
import six
|
||||
from watcher.applier.promise import Promise
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class BasePrimitive(object):
|
||||
@Promise
|
||||
class BaseLoader(object):
|
||||
|
||||
@abc.abstractmethod
|
||||
def execute(self):
|
||||
def list_available(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
@Promise
|
||||
@abc.abstractmethod
|
||||
def undo(self):
|
||||
def load(self, name):
|
||||
raise NotImplementedError()
|
||||
48
watcher/common/loader/default.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# -*- 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 __future__ import unicode_literals
|
||||
|
||||
from oslo_log import log
|
||||
from stevedore.driver import DriverManager
|
||||
from stevedore import ExtensionManager
|
||||
|
||||
from watcher.common import exception
|
||||
from watcher.common.loader.base import BaseLoader
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class DefaultLoader(BaseLoader):
|
||||
def __init__(self, namespace):
|
||||
super(DefaultLoader, self).__init__()
|
||||
self.namespace = namespace
|
||||
|
||||
def load(self, name):
|
||||
try:
|
||||
LOG.debug("Loading in namespace %s => %s ", self.namespace, name)
|
||||
driver_manager = DriverManager(namespace=self.namespace,
|
||||
name=name)
|
||||
loaded = driver_manager.driver
|
||||
except Exception as exc:
|
||||
LOG.exception(exc)
|
||||
raise exception.LoadingError(name=name)
|
||||
|
||||
return loaded()
|
||||
|
||||
def list_available(self):
|
||||
extension_manager = ExtensionManager(namespace=self.namespace)
|
||||
return {ext.name: ext.plugin for ext in extension_manager.extensions}
|
||||
@@ -94,7 +94,7 @@ class MessagingHandler(threading.Thread):
|
||||
)
|
||||
self.__server = self.build_server(target)
|
||||
else:
|
||||
LOG.warn(
|
||||
LOG.warning(
|
||||
_LW("No endpoint defined; can only publish events"))
|
||||
except Exception as e:
|
||||
LOG.exception(e)
|
||||
|
||||
@@ -76,9 +76,9 @@ class NovaClient(object):
|
||||
|
||||
:param instance_id: the unique id of the instance to migrate.
|
||||
:param keep_original_image_name: flag indicating whether the
|
||||
image name from which the original instance was built must be
|
||||
used as the name of the intermediate image used for migration.
|
||||
If this flag is False, a temporary image name is built
|
||||
image name from which the original instance was built must be
|
||||
used as the name of the intermediate image used for migration.
|
||||
If this flag is False, a temporary image name is built
|
||||
"""
|
||||
|
||||
new_image_name = ""
|
||||
@@ -328,7 +328,7 @@ class NovaClient(object):
|
||||
return False
|
||||
|
||||
def live_migrate_instance(self, instance_id, dest_hostname,
|
||||
block_migration=True, retry=120):
|
||||
block_migration=False, retry=120):
|
||||
"""This method does a live migration of a given instance
|
||||
|
||||
This method uses the Nova built-in live_migrate()
|
||||
@@ -438,13 +438,13 @@ class NovaClient(object):
|
||||
|
||||
It waits for this image to be in 'active' state before returning.
|
||||
It returns the unique UUID of the created image if successful,
|
||||
None otherwise
|
||||
None otherwise.
|
||||
|
||||
:param instance_id: the uniqueid of
|
||||
the instance to backup as an image.
|
||||
the instance to backup as an image.
|
||||
:param image_name: the name of the image to create.
|
||||
:param metadata: a dictionary containing the list of
|
||||
key-value pairs to associate to the image as metadata.
|
||||
key-value pairs to associate to the image as metadata.
|
||||
"""
|
||||
if self.glance is None:
|
||||
glance_endpoint = self.keystone. \
|
||||
@@ -568,7 +568,7 @@ class NovaClient(object):
|
||||
|
||||
:param instance: instance object.
|
||||
:param status_list: tuple containing the list of
|
||||
status we are waiting for
|
||||
status we are waiting for
|
||||
:param retry: how many times to retry
|
||||
:param sleep: seconds to sleep between the retries
|
||||
"""
|
||||
|
||||
@@ -50,8 +50,9 @@ def safe_rstrip(value, chars=None):
|
||||
|
||||
"""
|
||||
if not isinstance(value, six.string_types):
|
||||
LOG.warn(_LW("Failed to remove trailing character. Returning original "
|
||||
"object. Supplied object is not a string: %s,"), value)
|
||||
LOG.warning(_LW(
|
||||
"Failed to remove trailing character. Returning original object."
|
||||
"Supplied object is not a string: %s,"), value)
|
||||
return value
|
||||
|
||||
return value.rstrip(chars) or value
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
..
|
||||
Except where otherwise noted, this document is licensed under Creative
|
||||
Commons Attribution 3.0 License. You can view the license at:
|
||||
|
||||
https://creativecommons.org/licenses/by/3.0/
|
||||
|
||||
Tempest Field Guide to Infrastructure Optimization API tests
|
||||
============================================================
|
||||
|
||||
|
||||
What are these tests?
|
||||
---------------------
|
||||
|
||||
These tests stress the OpenStack Infrastructure Optimization API provided by
|
||||
Watcher.
|
||||
|
||||
|
||||
Why are these tests in tempest?
|
||||
------------------------------
|
||||
|
||||
The purpose of these tests is to exercise the various APIs provided by Watcher
|
||||
for optimizing the infrastructure.
|
||||
|
||||
|
||||
Scope of these tests
|
||||
--------------------
|
||||
|
||||
The Infrastructure Optimization API test perform basic CRUD operations on the Watcher node
|
||||
inventory. They do not actually perform placement or migration of virtual resources. It is important
|
||||
to note that all Watcher API actions are admin operations meant to be used
|
||||
either by cloud operators.
|
||||
@@ -1,130 +0,0 @@
|
||||
# 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 functools
|
||||
|
||||
from tempest_lib.common.utils import data_utils
|
||||
from tempest_lib import exceptions as lib_exc
|
||||
|
||||
from tempest import clients_infra_optim as clients
|
||||
from tempest.common import credentials
|
||||
from tempest import config
|
||||
from tempest import test
|
||||
|
||||
CONF = config.CONF
|
||||
|
||||
|
||||
# Resources must be deleted in a specific order, this list
|
||||
# defines the resource types to clean up, and the correct order.
|
||||
RESOURCE_TYPES = ['audit_template']
|
||||
# RESOURCE_TYPES = ['action', 'action_plan', 'audit', 'audit_template']
|
||||
|
||||
|
||||
def creates(resource):
|
||||
"""Decorator that adds resources to the appropriate cleanup list."""
|
||||
|
||||
def decorator(f):
|
||||
@functools.wraps(f)
|
||||
def wrapper(cls, *args, **kwargs):
|
||||
resp, body = f(cls, *args, **kwargs)
|
||||
|
||||
if 'uuid' in body:
|
||||
cls.created_objects[resource].add(body['uuid'])
|
||||
|
||||
return resp, body
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
class BaseInfraOptimTest(test.BaseTestCase):
|
||||
"""Base class for Infrastructure Optimization API tests."""
|
||||
|
||||
@classmethod
|
||||
# def skip_checks(cls):
|
||||
# super(BaseInfraOptimTest, cls).skip_checks()
|
||||
# if not CONF.service_available.watcher:
|
||||
# skip_msg = \
|
||||
# ('%s skipped as Watcher is not available' % cls.__name__)
|
||||
# raise cls.skipException(skip_msg)
|
||||
@classmethod
|
||||
def setup_credentials(cls):
|
||||
super(BaseInfraOptimTest, cls).setup_credentials()
|
||||
if (not hasattr(cls, 'isolated_creds') or
|
||||
not cls.isolated_creds.name == cls.__name__):
|
||||
cls.isolated_creds = credentials.get_isolated_credentials(
|
||||
name=cls.__name__, network_resources=cls.network_resources)
|
||||
cls.mgr = clients.Manager(cls.isolated_creds.get_admin_creds())
|
||||
|
||||
@classmethod
|
||||
def setup_clients(cls):
|
||||
super(BaseInfraOptimTest, cls).setup_clients()
|
||||
cls.client = cls.mgr.io_client
|
||||
|
||||
@classmethod
|
||||
def resource_setup(cls):
|
||||
super(BaseInfraOptimTest, cls).resource_setup()
|
||||
|
||||
cls.created_objects = {}
|
||||
for resource in RESOURCE_TYPES:
|
||||
cls.created_objects[resource] = set()
|
||||
|
||||
@classmethod
|
||||
def resource_cleanup(cls):
|
||||
"""Ensure that all created objects get destroyed."""
|
||||
|
||||
try:
|
||||
for resource in RESOURCE_TYPES:
|
||||
uuids = cls.created_objects[resource]
|
||||
delete_method = getattr(cls.client, 'delete_%s' % resource)
|
||||
for u in uuids:
|
||||
delete_method(u, ignore_errors=lib_exc.NotFound)
|
||||
finally:
|
||||
super(BaseInfraOptimTest, cls).resource_cleanup()
|
||||
|
||||
@classmethod
|
||||
@creates('audit_template')
|
||||
def create_audit_template(cls, description=None, expect_errors=False):
|
||||
"""Wrapper utility for creating test audit_template.
|
||||
|
||||
:param description: A description of the audit template.
|
||||
if not supplied, a random value will be generated.
|
||||
:return: Created audit template.
|
||||
"""
|
||||
|
||||
description = description or data_utils.rand_name(
|
||||
'test-audit_template')
|
||||
resp, body = cls.client.create_audit_template(description=description)
|
||||
return resp, body
|
||||
|
||||
@classmethod
|
||||
def delete_audit_template(cls, audit_template_id):
|
||||
"""Deletes a audit_template having the specified UUID.
|
||||
|
||||
:param uuid: The unique identifier of the audit_template.
|
||||
:return: Server response.
|
||||
"""
|
||||
|
||||
resp, body = cls.client.delete_audit_template(audit_template_id)
|
||||
|
||||
if audit_template_id in cls.created_objects['audit_template']:
|
||||
cls.created_objects['audit_template'].remove(audit_template_id)
|
||||
|
||||
return resp
|
||||
|
||||
def validate_self_link(self, resource, uuid, link):
|
||||
"""Check whether the given self link formatted correctly."""
|
||||
expected_link = "{base}/{pref}/{res}/{uuid}".format(
|
||||
base=self.client.base_url,
|
||||
pref=self.client.uri_prefix,
|
||||
res=resource,
|
||||
uuid=uuid)
|
||||
self.assertEqual(expected_link, link)
|
||||
@@ -1,56 +0,0 @@
|
||||
..
|
||||
Except where otherwise noted, this document is licensed under Creative
|
||||
Commons Attribution 3.0 License. You can view the license at:
|
||||
|
||||
https://creativecommons.org/licenses/by/3.0/
|
||||
|
||||
.. _cli_field_guide:
|
||||
|
||||
Tempest Field Guide to CLI tests
|
||||
================================
|
||||
|
||||
|
||||
What are these tests?
|
||||
---------------------
|
||||
The cli tests test the various OpenStack command line interface tools
|
||||
to ensure that they minimally function. The current scope is read only
|
||||
operations on a cloud that are hard to test via unit tests.
|
||||
|
||||
|
||||
Why are these tests in tempest?
|
||||
-------------------------------
|
||||
These tests exist here because it is extremely difficult to build a
|
||||
functional enough environment in the python-\*client unit tests to
|
||||
provide this kind of testing. Because we already put up a cloud in the
|
||||
gate with devstack + tempest it was decided it was better to have
|
||||
these as a side tree in tempest instead of another QA effort which
|
||||
would split review time.
|
||||
|
||||
|
||||
Scope of these tests
|
||||
--------------------
|
||||
This should stay limited to the scope of testing the cli. Functional
|
||||
testing of the cloud should be elsewhere, this is about exercising the
|
||||
cli code.
|
||||
|
||||
|
||||
Example of a good test
|
||||
----------------------
|
||||
Tests should be isolated to a single command in one of the python
|
||||
clients.
|
||||
|
||||
Tests should not modify the cloud.
|
||||
|
||||
If a test is validating the cli for bad data, it should do it with
|
||||
assertRaises.
|
||||
|
||||
A reasonable example of an existing test is as follows::
|
||||
|
||||
def test_admin_list(self):
|
||||
self.nova('list')
|
||||
self.nova('list', params='--all-tenants 1')
|
||||
self.nova('list', params='--all-tenants 0')
|
||||
self.assertRaises(subprocess.CalledProcessError,
|
||||
self.nova,
|
||||
'list',
|
||||
params='--all-tenants bad')
|
||||
@@ -1,126 +0,0 @@
|
||||
# Copyright 2013 OpenStack Foundation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import functools
|
||||
|
||||
from tempest_lib.cli import base
|
||||
from tempest_lib.cli import output_parser
|
||||
import testtools
|
||||
|
||||
from tempest.common import credentials
|
||||
from tempest import config
|
||||
from tempest import exceptions
|
||||
from tempest.openstack.common import versionutils
|
||||
from tempest import test
|
||||
|
||||
|
||||
CONF = config.CONF
|
||||
|
||||
|
||||
def check_client_version(client, version):
|
||||
"""Checks if the client's version is compatible with the given version
|
||||
|
||||
@param client: The client to check.
|
||||
@param version: The version to compare against.
|
||||
@return: True if the client version is compatible with the given version
|
||||
parameter, False otherwise.
|
||||
"""
|
||||
current_version = base.execute(client, '', params='--version',
|
||||
merge_stderr=True, cli_dir=CONF.cli.cli_dir)
|
||||
|
||||
if not current_version.strip():
|
||||
raise exceptions.TempestException('"%s --version" output was empty' %
|
||||
client)
|
||||
|
||||
return versionutils.is_compatible(version, current_version,
|
||||
same_major=False)
|
||||
|
||||
|
||||
def min_client_version(*args, **kwargs):
|
||||
"""A decorator to skip tests if the client used isn't of the right version.
|
||||
|
||||
@param client: The client command to run. For python-novaclient, this is
|
||||
'nova', for python-cinderclient this is 'cinder', etc.
|
||||
@param version: The minimum version required to run the CLI test.
|
||||
"""
|
||||
def decorator(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(*func_args, **func_kwargs):
|
||||
if not check_client_version(kwargs['client'], kwargs['version']):
|
||||
msg = "requires %s client version >= %s" % (kwargs['client'],
|
||||
kwargs['version'])
|
||||
raise testtools.TestCase.skipException(msg)
|
||||
return func(*func_args, **func_kwargs)
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
class ClientTestBase(test.BaseTestCase):
|
||||
|
||||
@classmethod
|
||||
def skip_checks(cls):
|
||||
super(ClientTestBase, cls).skip_checks()
|
||||
if not CONF.identity_feature_enabled.api_v2:
|
||||
raise cls.skipException("CLI clients rely on identity v2 API, "
|
||||
"which is configured as not available")
|
||||
|
||||
@classmethod
|
||||
def resource_setup(cls):
|
||||
if not CONF.cli.enabled:
|
||||
msg = "cli testing disabled"
|
||||
raise cls.skipException(msg)
|
||||
super(ClientTestBase, cls).resource_setup()
|
||||
cls.isolated_creds = credentials.get_isolated_credentials(cls.__name__)
|
||||
cls.creds = cls.isolated_creds.get_admin_creds()
|
||||
|
||||
def _get_clients(self):
|
||||
clients = base.CLIClient(self.creds.username,
|
||||
self.creds.password,
|
||||
self.creds.tenant_name,
|
||||
CONF.identity.uri, CONF.cli.cli_dir)
|
||||
return clients
|
||||
|
||||
# TODO(mtreinish): The following code is basically copied from tempest-lib.
|
||||
# The base cli test class in tempest-lib 0.0.1 doesn't work as a mixin like
|
||||
# is needed here. The code below should be removed when tempest-lib
|
||||
# provides a way to provide this functionality
|
||||
def setUp(self):
|
||||
super(ClientTestBase, self).setUp()
|
||||
self.clients = self._get_clients()
|
||||
self.parser = output_parser
|
||||
|
||||
def assertTableStruct(self, items, field_names):
|
||||
"""Verify that all items has keys listed in field_names.
|
||||
|
||||
:param items: items to assert are field names in the output table
|
||||
:type items: list
|
||||
:param field_names: field names from the output table of the cmd
|
||||
:type field_names: list
|
||||
"""
|
||||
for item in items:
|
||||
for field in field_names:
|
||||
self.assertIn(field, item)
|
||||
|
||||
def assertFirstLineStartsWith(self, lines, beginning):
|
||||
"""Verify that the first line starts with a string
|
||||
|
||||
:param lines: strings for each line of output
|
||||
:type lines: list
|
||||
:param beginning: verify this is at the beginning of the first line
|
||||
:type beginning: string
|
||||
"""
|
||||
self.assertTrue(lines[0].startswith(beginning),
|
||||
msg=('Beginning of first line has invalid content: %s'
|
||||
% lines[:3]))
|
||||
@@ -1 +0,0 @@
|
||||
This directory consists of simple read only python client tests.
|
||||
@@ -1,220 +0,0 @@
|
||||
# Copyright 2013 OpenStack Foundation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import logging
|
||||
import re
|
||||
|
||||
from tempest_lib import exceptions
|
||||
import testtools
|
||||
|
||||
from tempest import cli
|
||||
from tempest import clients
|
||||
from tempest import config
|
||||
from tempest import test
|
||||
|
||||
|
||||
CONF = config.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SimpleReadOnlyCinderClientTest(cli.ClientTestBase):
|
||||
"""Basic, read-only tests for Cinder CLI client.
|
||||
|
||||
Checks return values and output of read-only commands.
|
||||
These tests do not presume any content, nor do they create
|
||||
their own. They only verify the structure of output if present.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def resource_setup(cls):
|
||||
# if not CONF.service_available.cinder:
|
||||
# msg = ("%s skipped as Cinder is not available" % cls.__name__)
|
||||
# raise cls.skipException(msg)
|
||||
super(SimpleReadOnlyCinderClientTest, cls).resource_setup()
|
||||
id_cl = clients.AdminManager().identity_client
|
||||
tenant = id_cl.get_tenant_by_name(CONF.identity.admin_tenant_name)
|
||||
cls.admin_tenant_id = tenant['id']
|
||||
|
||||
def cinder(self, *args, **kwargs):
|
||||
return self.clients.cinder(*args,
|
||||
endpoint_type=CONF.volume.endpoint_type,
|
||||
**kwargs)
|
||||
|
||||
@test.idempotent_id('229bc6dc-d804-4668-b753-b590caf63061')
|
||||
def test_cinder_fake_action(self):
|
||||
self.assertRaises(exceptions.CommandFailed,
|
||||
self.cinder,
|
||||
'this-does-not-exist')
|
||||
|
||||
@test.idempotent_id('77140216-14db-4fc5-a246-e2a587e9e99b')
|
||||
def test_cinder_absolute_limit_list(self):
|
||||
roles = self.parser.listing(self.cinder('absolute-limits'))
|
||||
self.assertTableStruct(roles, ['Name', 'Value'])
|
||||
|
||||
@test.idempotent_id('2206b9ce-1a36-4a0a-a129-e5afc7cee1dd')
|
||||
def test_cinder_backup_list(self):
|
||||
backup_list = self.parser.listing(self.cinder('backup-list'))
|
||||
self.assertTableStruct(backup_list, ['ID', 'Volume ID', 'Status',
|
||||
'Name', 'Size', 'Object Count',
|
||||
'Container'])
|
||||
|
||||
@test.idempotent_id('c7f50346-cd99-4e0b-953f-796ff5f47295')
|
||||
def test_cinder_extra_specs_list(self):
|
||||
extra_specs_list = self.parser.listing(self.cinder('extra-specs-list'))
|
||||
self.assertTableStruct(extra_specs_list, ['ID', 'Name', 'extra_specs'])
|
||||
|
||||
@test.idempotent_id('9de694cb-b40b-442c-a30c-5f9873e144f7')
|
||||
def test_cinder_volumes_list(self):
|
||||
list = self.parser.listing(self.cinder('list'))
|
||||
self.assertTableStruct(list, ['ID', 'Status', 'Name', 'Size',
|
||||
'Volume Type', 'Bootable',
|
||||
'Attached to'])
|
||||
self.cinder('list', params='--all-tenants 1')
|
||||
self.cinder('list', params='--all-tenants 0')
|
||||
self.assertRaises(exceptions.CommandFailed,
|
||||
self.cinder,
|
||||
'list',
|
||||
params='--all-tenants bad')
|
||||
|
||||
@test.idempotent_id('56f7c15c-ee82-4f23-bbe8-ce99b66da493')
|
||||
def test_cinder_quota_class_show(self):
|
||||
"""This CLI can accept and string as param."""
|
||||
roles = self.parser.listing(self.cinder('quota-class-show',
|
||||
params='abc'))
|
||||
self.assertTableStruct(roles, ['Property', 'Value'])
|
||||
|
||||
@test.idempotent_id('a919a811-b7f0-47a7-b4e5-f3eb674dd200')
|
||||
def test_cinder_quota_defaults(self):
|
||||
"""This CLI can accept and string as param."""
|
||||
roles = self.parser.listing(self.cinder('quota-defaults',
|
||||
params=self.admin_tenant_id))
|
||||
self.assertTableStruct(roles, ['Property', 'Value'])
|
||||
|
||||
@test.idempotent_id('18166673-ffa8-4df3-b60c-6375532288bc')
|
||||
def test_cinder_quota_show(self):
|
||||
"""This CLI can accept and string as param."""
|
||||
roles = self.parser.listing(self.cinder('quota-show',
|
||||
params=self.admin_tenant_id))
|
||||
self.assertTableStruct(roles, ['Property', 'Value'])
|
||||
|
||||
@test.idempotent_id('b2c66ed9-ca96-4dc4-94cc-8083e664e516')
|
||||
def test_cinder_rate_limits(self):
|
||||
rate_limits = self.parser.listing(self.cinder('rate-limits'))
|
||||
self.assertTableStruct(rate_limits, ['Verb', 'URI', 'Value', 'Remain',
|
||||
'Unit', 'Next_Available'])
|
||||
|
||||
@test.idempotent_id('7a19955b-807c-481a-a2ee-9d76733eac28')
|
||||
@testtools.skipUnless(CONF.volume_feature_enabled.snapshot,
|
||||
'Volume snapshot not available.')
|
||||
def test_cinder_snapshot_list(self):
|
||||
snapshot_list = self.parser.listing(self.cinder('snapshot-list'))
|
||||
self.assertTableStruct(snapshot_list, ['ID', 'Volume ID', 'Status',
|
||||
'Name', 'Size'])
|
||||
|
||||
@test.idempotent_id('6e54ecd9-7ba9-490d-8e3b-294b67139e73')
|
||||
def test_cinder_type_list(self):
|
||||
type_list = self.parser.listing(self.cinder('type-list'))
|
||||
self.assertTableStruct(type_list, ['ID', 'Name'])
|
||||
|
||||
@test.idempotent_id('2c363583-24a0-4980-b9cb-b50c0d241e82')
|
||||
def test_cinder_list_extensions(self):
|
||||
roles = self.parser.listing(self.cinder('list-extensions'))
|
||||
self.assertTableStruct(roles, ['Name', 'Summary', 'Alias', 'Updated'])
|
||||
|
||||
@test.idempotent_id('691bd6df-30ad-4be7-927b-a02d62aaa38a')
|
||||
def test_cinder_credentials(self):
|
||||
credentials = self.parser.listing(self.cinder('credentials'))
|
||||
self.assertTableStruct(credentials, ['User Credentials', 'Value'])
|
||||
|
||||
@test.idempotent_id('5c6d71a3-4904-4a3a-aec9-7fd4aa830e95')
|
||||
def test_cinder_availability_zone_list(self):
|
||||
zone_list = self.parser.listing(self.cinder('availability-zone-list'))
|
||||
self.assertTableStruct(zone_list, ['Name', 'Status'])
|
||||
|
||||
@test.idempotent_id('9b0fd5a6-f955-42b9-a42f-6f542a80b9a3')
|
||||
def test_cinder_endpoints(self):
|
||||
out = self.cinder('endpoints')
|
||||
tables = self.parser.tables(out)
|
||||
for table in tables:
|
||||
headers = table['headers']
|
||||
self.assertTrue(2 >= len(headers))
|
||||
self.assertEqual('Value', headers[1])
|
||||
|
||||
@test.idempotent_id('301b5ae1-9591-4e9f-999c-d525a9bdf822')
|
||||
def test_cinder_service_list(self):
|
||||
service_list = self.parser.listing(self.cinder('service-list'))
|
||||
self.assertTableStruct(service_list, ['Binary', 'Host', 'Zone',
|
||||
'Status', 'State', 'Updated_at'])
|
||||
|
||||
@test.idempotent_id('7260ae52-b462-461e-9048-36d0bccf92c6')
|
||||
def test_cinder_transfer_list(self):
|
||||
transfer_list = self.parser.listing(self.cinder('transfer-list'))
|
||||
self.assertTableStruct(transfer_list, ['ID', 'Volume ID', 'Name'])
|
||||
|
||||
@test.idempotent_id('0976dea8-14f3-45a9-8495-3617fc4fbb13')
|
||||
def test_cinder_bash_completion(self):
|
||||
self.cinder('bash-completion')
|
||||
|
||||
@test.idempotent_id('b7c00361-be80-4512-8735-5f98fc54f2a9')
|
||||
def test_cinder_qos_list(self):
|
||||
qos_list = self.parser.listing(self.cinder('qos-list'))
|
||||
self.assertTableStruct(qos_list, ['ID', 'Name', 'Consumer', 'specs'])
|
||||
|
||||
@test.idempotent_id('2e92dc6e-22b5-4d94-abfc-b543b0c50a89')
|
||||
def test_cinder_encryption_type_list(self):
|
||||
encrypt_list = self.parser.listing(self.cinder('encryption-type-list'))
|
||||
self.assertTableStruct(encrypt_list, ['Volume Type ID', 'Provider',
|
||||
'Cipher', 'Key Size',
|
||||
'Control Location'])
|
||||
|
||||
@test.idempotent_id('0ee6cb4c-8de6-4811-a7be-7f4bb75b80cc')
|
||||
def test_admin_help(self):
|
||||
help_text = self.cinder('help')
|
||||
lines = help_text.split('\n')
|
||||
self.assertFirstLineStartsWith(lines, 'usage: cinder')
|
||||
|
||||
commands = []
|
||||
cmds_start = lines.index('Positional arguments:')
|
||||
cmds_end = lines.index('Optional arguments:')
|
||||
command_pattern = re.compile('^ {4}([a-z0-9\-\_]+)')
|
||||
for line in lines[cmds_start:cmds_end]:
|
||||
match = command_pattern.match(line)
|
||||
if match:
|
||||
commands.append(match.group(1))
|
||||
commands = set(commands)
|
||||
wanted_commands = set(('absolute-limits', 'list', 'help',
|
||||
'quota-show', 'type-list', 'snapshot-list'))
|
||||
self.assertFalse(wanted_commands - commands)
|
||||
|
||||
# Optional arguments:
|
||||
|
||||
@test.idempotent_id('2fd6f530-183c-4bda-8918-1e59e36c26b9')
|
||||
def test_cinder_version(self):
|
||||
self.cinder('', flags='--version')
|
||||
|
||||
@test.idempotent_id('306bac51-c443-4426-a6cf-583a953fcd68')
|
||||
def test_cinder_debug_list(self):
|
||||
self.cinder('list', flags='--debug')
|
||||
|
||||
@test.idempotent_id('6d97fcd2-5dd1-429d-af70-030c949d86cd')
|
||||
def test_cinder_retries_list(self):
|
||||
self.cinder('list', flags='--retries 3')
|
||||
|
||||
@test.idempotent_id('95a2850c-35b4-4159-bb93-51647a5ad232')
|
||||
def test_cinder_region_list(self):
|
||||
region = CONF.volume.region
|
||||
if not region:
|
||||
region = CONF.identity.region
|
||||
self.cinder('list', flags='--os-region-name ' + region)
|
||||
@@ -1,42 +0,0 @@
|
||||
# Copyright 2014 Mirantis Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from tempest import clients
|
||||
from tempest.common import cred_provider
|
||||
from tempest import config
|
||||
from tempest.services.infra_optim.v1.json import infra_optim_client as ioc
|
||||
|
||||
CONF = config.CONF
|
||||
|
||||
|
||||
class Manager(clients.Manager):
|
||||
def __init__(self, credentials=None, service=None):
|
||||
super(Manager, self).__init__(credentials, service)
|
||||
self.io_client = ioc.InfraOptimClientJSON(self.auth_provider,
|
||||
'infra-optim',
|
||||
CONF.identity.region)
|
||||
|
||||
|
||||
class AltManager(Manager):
|
||||
def __init__(self, service=None):
|
||||
super(AltManager, self).__init__(
|
||||
cred_provider.get_configured_credentials('alt_user'), service)
|
||||
|
||||
|
||||
class AdminManager(Manager):
|
||||
def __init__(self, service=None):
|
||||
super(AdminManager, self).__init__(
|
||||
cred_provider.get_configured_credentials('identity_admin'),
|
||||
service)
|
||||
@@ -1,45 +0,0 @@
|
||||
# Copyright 2014 Mirantis Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
from oslo_config import cfg
|
||||
|
||||
from tempest import config # noqa
|
||||
|
||||
service_available_group = cfg.OptGroup(name="service_available",
|
||||
title="Available OpenStack Services")
|
||||
|
||||
ServiceAvailableGroup = [
|
||||
cfg.BoolOpt("watcher",
|
||||
default=True,
|
||||
help="Whether or not watcher is expected to be available"),
|
||||
]
|
||||
|
||||
|
||||
class TempestConfigProxyWatcher(object):
|
||||
"""Wrapper over standard Tempest config that sets Watcher opts."""
|
||||
|
||||
def __init__(self):
|
||||
self._config = config.CONF
|
||||
config.register_opt_group(
|
||||
cfg.CONF, service_available_group, ServiceAvailableGroup)
|
||||
self._config.share = cfg.CONF.share
|
||||
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self._config, attr)
|
||||
|
||||
|
||||
CONF = TempestConfigProxyWatcher()
|
||||