Compare commits
80 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
982410dd3e | ||
|
|
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
|
# Sphinx
|
||||||
doc/build
|
doc/build
|
||||||
|
doc/source/api
|
||||||
|
|
||||||
# pbr generates these
|
# pbr generates these
|
||||||
AUTHORS
|
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
|
AMQP Bus
|
||||||
--------
|
--------
|
||||||
|
|
||||||
The AMQP message bus handles asynchronous communications between the different
|
The AMQP message bus handles internal asynchronous communications between the
|
||||||
Watcher components.
|
different Watcher components.
|
||||||
|
|
||||||
.. _cluster_history_db_definition:
|
.. _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
|
which are optimized for handling time series data, which are arrays of numbers
|
||||||
indexed by time (a datetime or a datetime range).
|
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
|
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
|
:ref:`Cluster <cluster_definition>` to control and monitor the Watcher system
|
||||||
via any interaction mechanism connected to this API:
|
via any interaction mechanism connected to this API:
|
||||||
|
|
||||||
- :ref:`CLI <watcher_cli_definition>`
|
- :ref:`CLI <archi_watcher_cli_definition>`
|
||||||
- Horizon plugin
|
- Horizon plugin
|
||||||
- Python SDK
|
- Python SDK
|
||||||
|
|
||||||
You can also read the detailed description of `Watcher API`_.
|
You can also read the detailed description of `Watcher API`_.
|
||||||
|
|
||||||
.. _watcher_applier_definition:
|
.. _archi_watcher_applier_definition:
|
||||||
|
|
||||||
Watcher Applier
|
Watcher Applier
|
||||||
---------------
|
---------------
|
||||||
@@ -111,7 +119,7 @@ If the :ref:`Action <action_definition>` fails, the
|
|||||||
previous state of the :ref:`Managed resource <managed_resource_definition>`
|
previous state of the :ref:`Managed resource <managed_resource_definition>`
|
||||||
(i.e. before the command was sent to the underlying OpenStack service).
|
(i.e. before the command was sent to the underlying OpenStack service).
|
||||||
|
|
||||||
.. _watcher_cli_definition:
|
.. _archi_watcher_cli_definition:
|
||||||
|
|
||||||
Watcher CLI
|
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/>`_
|
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
|
Watcher Database
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
This database stores all the Watcher domain objects which can be requested
|
This database stores all the Watcher domain objects which can be requested
|
||||||
by the :ref:`Watcher API <watcher_api_definition>` or the
|
by the :ref:`Watcher API <archi_watcher_api_definition>` or the
|
||||||
:ref:`Watcher CLI <watcher_cli_definition>`:
|
:ref:`Watcher CLI <archi_watcher_cli_definition>`:
|
||||||
|
|
||||||
- :ref:`Audit templates <audit_template_definition>`
|
- :ref:`Audit templates <audit_template_definition>`
|
||||||
- :ref:`Audits <audit_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
|
The Watcher domain being here "*optimization of some resources provided by an
|
||||||
OpenStack system*".
|
OpenStack system*".
|
||||||
|
|
||||||
.. _watcher_decision_engine_definition:
|
.. _archi_watcher_decision_engine_definition:
|
||||||
|
|
||||||
Watcher Decision Engine
|
Watcher Decision Engine
|
||||||
-----------------------
|
-----------------------
|
||||||
@@ -159,7 +167,7 @@ The :ref:`Strategy <strategy_definition>` is then dynamically loaded (via
|
|||||||
`stevedore <https://github.com/openstack/stevedore/>`_). The
|
`stevedore <https://github.com/openstack/stevedore/>`_). The
|
||||||
:ref:`Watcher Decision Engine <watcher_decision_engine_definition>` calls the
|
:ref:`Watcher Decision Engine <watcher_decision_engine_definition>` calls the
|
||||||
**execute()** method of the :ref:`Strategy <strategy_definition>` class which
|
**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
|
These :ref:`Actions <action_definition>` are scheduled in time by the
|
||||||
:ref:`Watcher Planner <watcher_planner_definition>` (i.e., it generates an
|
: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:
|
Audit, the :ref:`Strategy <strategy_definition>` relies on two sets of data:
|
||||||
|
|
||||||
- the current state of the
|
- the current state of the
|
||||||
:ref:`Managed resources <managed_resource_definition>`
|
:ref:`Managed resources <managed_resource_definition>`
|
||||||
(e.g., the data stored in the Nova database)
|
(e.g., the data stored in the Nova database)
|
||||||
- the data stored in the
|
- 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
|
which provides information about the past of the
|
||||||
:ref:`Cluster <cluster_definition>`
|
:ref:`Cluster <cluster_definition>`
|
||||||
|
|
||||||
So far, only one :ref:`Strategy <strategy_definition>` can be associated to a
|
So far, only one :ref:`Strategy <strategy_definition>` can be associated to a
|
||||||
given :ref:`Goal <goal_definition>` via the main Watcher configuration file.
|
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
|
.. _Watcher API: webapi/v1.html
|
||||||
|
|||||||
@@ -11,19 +11,14 @@
|
|||||||
# implied.
|
# implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from watcher import version as watcher_version
|
from watcher import version as watcher_version
|
||||||
|
|
||||||
sys.path.insert(0, os.path.abspath('../..'))
|
|
||||||
# -- General configuration ----------------------------------------------------
|
# -- General configuration ----------------------------------------------------
|
||||||
|
|
||||||
# Add any Sphinx extension module names here, as strings. They can be
|
# Add any Sphinx extension module names here, as strings. They can be
|
||||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||||
extensions = [
|
extensions = [
|
||||||
'sphinx.ext.autodoc',
|
'sphinx.ext.autodoc',
|
||||||
# 'sphinx.ext.intersphinx',
|
|
||||||
'sphinx.ext.viewcode',
|
'sphinx.ext.viewcode',
|
||||||
'sphinxcontrib.httpdomain',
|
'sphinxcontrib.httpdomain',
|
||||||
'sphinxcontrib.pecanwsme.rest',
|
'sphinxcontrib.pecanwsme.rest',
|
||||||
@@ -46,8 +41,8 @@ source_suffix = '.rst'
|
|||||||
master_doc = 'index'
|
master_doc = 'index'
|
||||||
|
|
||||||
# General information about the project.
|
# General information about the project.
|
||||||
project = u'watcher'
|
project = u'Watcher'
|
||||||
copyright = u'2015, OpenStack Foundation'
|
copyright = u'OpenStack Foundation'
|
||||||
|
|
||||||
# The version info for the project you're documenting, acts as replacement for
|
# The version info for the project you're documenting, acts as replacement for
|
||||||
# |version| and |release|, also used in various other places throughout the
|
# |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.
|
# A list of ignored prefixes for module index sorting.
|
||||||
modindex_common_prefix = ['watcher.']
|
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.
|
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||||
add_function_parentheses = True
|
add_function_parentheses = True
|
||||||
@@ -73,6 +76,22 @@ add_module_names = True
|
|||||||
# The name of the Pygments (syntax highlighting) style to use.
|
# The name of the Pygments (syntax highlighting) style to use.
|
||||||
pygments_style = 'sphinx'
|
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 --------------------------------------------------
|
# -- Options for HTML output --------------------------------------------------
|
||||||
|
|
||||||
# The theme to use for HTML and HTML Help pages. Major themes that come with
|
# 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
|
The original implementation has been based on MongoDB but you can create your
|
||||||
own storage driver using whatever technology you want.
|
own storage driver using whatever technology you want.
|
||||||
For more information : https://wiki.openstack.org/wiki/Gnocchi
|
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:
|
.. _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
|
In this guide we're going to take you through the fundamentals of using
|
||||||
Watcher.
|
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
|
Getting started with Watcher
|
||||||
----------------------------
|
----------------------------
|
||||||
This guide assumes you have a working installation of Watcher. If you get
|
This guide assumes you have a working installation of Watcher. If you get
|
||||||
"*watcher: command not found*" you may have to verify your installation.
|
"*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
|
In order to use Watcher, you have to configure your credentials suitable for
|
||||||
watcher command-line tools.
|
watcher command-line tools.
|
||||||
If you need help on a specific command, you can use:
|
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
|
$ watcher help COMMAND
|
||||||
|
|
||||||
|
.. _`installation guide`: https://factory.b-com.com/www/watcher/doc/python-watcherclient
|
||||||
|
|
||||||
Seeing what the Watcher CLI can do ?
|
Seeing what the Watcher CLI can do ?
|
||||||
------------------------------------
|
------------------------------------
|
||||||
We can see all of the commands available with Watcher CLI by running the
|
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>
|
$ watcher action-plan-list --audit <the_audit_uuid>
|
||||||
|
|
||||||
- Have a look on the list of optimization :ref:`actions <action_definition>`
|
- 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
|
.. code:: bash
|
||||||
|
|
||||||
@@ -119,3 +130,4 @@ You can also obtain more detailed information about a specific action:
|
|||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
$ watcher action-show <the_action_uuid>
|
$ watcher action-show <the_action_uuid>
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
|
|
||||||
.. _contributing:
|
.. _contributing:
|
||||||
|
|
||||||
======================
|
=======================
|
||||||
Contributing to Watcher
|
Contributing to Watcher
|
||||||
======================
|
=======================
|
||||||
|
|
||||||
If you're interested in contributing to the Watcher project,
|
If you're interested in contributing to the Watcher project,
|
||||||
the following will help get you started.
|
the following will help get you started.
|
||||||
@@ -43,7 +43,7 @@ notifications of important events.
|
|||||||
|
|
||||||
|
|
||||||
Project Hosting Details
|
Project Hosting Details
|
||||||
-------------------------
|
-----------------------
|
||||||
|
|
||||||
Bug tracker
|
Bug tracker
|
||||||
http://launchpad.net/watcher
|
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/
|
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`_
|
This document describes getting the source from watcher `Git repository`_
|
||||||
for development purposes.
|
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
|
prior to using `pip`, and the installation method may vary depending on
|
||||||
your platform.
|
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+:
|
$ sudo yum install openssl-devel libffi-devel mysql-devel
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ sudo yum install openssl-devel libffi-devel mysql-devel
|
|
||||||
|
|
||||||
|
|
||||||
PyPi Packages and VirtualEnv
|
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, ...).
|
isolated environment (Identity, Message Bus, SQL database, Horizon, ...).
|
||||||
|
|
||||||
.. _`watcher-tools`: https://github.com/b-com/watcher-tools
|
.. _`watcher-tools`: https://github.com/b-com/watcher-tools
|
||||||
|
|
||||||
|
|||||||
@@ -4,22 +4,20 @@
|
|||||||
|
|
||||||
https://creativecommons.org/licenses/by/3.0/
|
https://creativecommons.org/licenses/by/3.0/
|
||||||
|
|
||||||
===============
|
=================================
|
||||||
Watcher plugins
|
Build a new optimization strategy
|
||||||
===============
|
=================================
|
||||||
|
|
||||||
Writing a Watcher Decision Engine plugin
|
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
|
||||||
Watcher has an external :ref:`strategy <strategy_definition>` plugin interface
|
algorithms.
|
||||||
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
|
This section gives some guidelines on how to implement and integrate custom
|
||||||
Stategies with Watcher.
|
Stategies with Watcher.
|
||||||
|
|
||||||
Pre-requisites
|
Pre-requisites
|
||||||
--------------
|
==============
|
||||||
|
|
||||||
Before using any strategy, you should make sure you have your Telemetry service
|
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
|
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
|
Creating a new plugin
|
||||||
---------------------
|
=====================
|
||||||
|
|
||||||
First of all you have to:
|
First of all you have to:
|
||||||
|
|
||||||
@@ -65,20 +63,21 @@ your ``__init__`` method.
|
|||||||
|
|
||||||
|
|
||||||
Abstract Plugin Class
|
Abstract Plugin Class
|
||||||
---------------------
|
=====================
|
||||||
|
|
||||||
Here below is the abstract ``BaseStrategy`` class that every single strategy
|
Here below is the abstract ``BaseStrategy`` class that every single strategy
|
||||||
should implement:
|
should implement:
|
||||||
|
|
||||||
.. automodule:: watcher.decision_engine.strategy.base
|
.. automodule:: watcher.decision_engine.strategy.strategies.base
|
||||||
:noindex:
|
:noindex:
|
||||||
|
|
||||||
.. autoclass:: BaseStrategy
|
.. autoclass:: BaseStrategy
|
||||||
:members:
|
:members:
|
||||||
|
:noindex:
|
||||||
|
|
||||||
|
|
||||||
Add a new entry point
|
Add a new entry point
|
||||||
---------------------
|
=====================
|
||||||
|
|
||||||
In order for the Watcher Decision Engine to load your new strategy, the
|
In order for the Watcher Decision Engine to load your new strategy, the
|
||||||
strategy must be registered as a named entry point under 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/
|
.. _pbr: http://docs.openstack.org/developer/pbr/
|
||||||
|
|
||||||
Using strategy plugins
|
Using strategy plugins
|
||||||
----------------------
|
======================
|
||||||
|
|
||||||
The Watcher Decision Engine service will automatically discover any installed
|
The Watcher Decision Engine service will automatically discover any installed
|
||||||
plugins when it is run. If a Python package containing a custom plugin is
|
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.
|
check/configure the latter so your new strategy can be fully functional.
|
||||||
|
|
||||||
Querying metrics
|
Querying metrics
|
||||||
~~~~~~~~~~~~~~~~
|
----------------
|
||||||
|
|
||||||
The metrics available depend on the hypervisors that OpenStack manages on
|
A large set of metrics, generated by OpenStack modules, can be used in your
|
||||||
the specific implementation. You can find the metrics available per hypervisor
|
strategy implementation. To collect these metrics, Watcher provides a
|
||||||
and OpenStack release on the OpenStack site.
|
`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
|
If you want to use your own metrics database backend, please refer to the
|
||||||
use the default Ceilometer API or our Helper.
|
`Ceilometer developer guide`_. Indeed, Ceilometer's pluggable model allows
|
||||||
The Helper attempted to make the Ceilometer API more reusable and easy to use.
|
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
|
Read usage metrics using the Python binding
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
-------------------------------------------
|
||||||
|
|
||||||
You can find the information about the Ceilometer Python binding on the
|
You can find the information about the Ceilometer Python binding on the
|
||||||
OpenStack `ceilometer client python API documentation
|
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)
|
value_cpu = cclient.samples.list(meter_name='cpu_util', limit=10, q=query)
|
||||||
|
|
||||||
Read usage metrics using the Watcher Cluster History Helper
|
Read usage metrics using the Watcher Cluster History Helper
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
-----------------------------------------------------------
|
||||||
|
|
||||||
Here below is the abstract ``BaseClusterHistory`` class of the 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
|
.. autoclass:: BaseClusterHistory
|
||||||
:members:
|
:members:
|
||||||
|
:noindex:
|
||||||
|
|
||||||
|
|
||||||
The following snippet code shows how to create a Cluster History class:
|
The following snippet code shows how to create a Cluster History class:
|
||||||
@@ -4,9 +4,9 @@
|
|||||||
|
|
||||||
https://creativecommons.org/licenses/by/3.0/
|
https://creativecommons.org/licenses/by/3.0/
|
||||||
|
|
||||||
==========
|
========
|
||||||
Glossary
|
Glossary
|
||||||
==========
|
========
|
||||||
|
|
||||||
.. glossary::
|
.. glossary::
|
||||||
:sorted:
|
:sorted:
|
||||||
@@ -20,97 +20,14 @@ They are sorted in alphabetical order.
|
|||||||
Action
|
Action
|
||||||
======
|
======
|
||||||
|
|
||||||
An :ref:`Action <action_definition>` is what enables Watcher to transform the
|
.. watcher-term:: watcher.api.controllers.v1.action
|
||||||
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>`
|
|
||||||
|
|
||||||
.. _action_plan_definition:
|
.. _action_plan_definition:
|
||||||
|
|
||||||
Action Plan
|
Action Plan
|
||||||
===========
|
===========
|
||||||
|
|
||||||
An :ref:`Action Plan <action_plan_definition>` is a flow of
|
.. watcher-term:: watcher.api.controllers.v1.action_plan
|
||||||
: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>`
|
|
||||||
|
|
||||||
.. _administrator_definition:
|
.. _administrator_definition:
|
||||||
|
|
||||||
@@ -144,73 +61,14 @@ any Watcher configuration files and to restart Watcher services.
|
|||||||
Audit
|
Audit
|
||||||
=====
|
=====
|
||||||
|
|
||||||
In the Watcher system, an :ref:`Audit <audit_definition>` is a request for
|
.. watcher-term:: watcher.api.controllers.v1.audit
|
||||||
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>`
|
|
||||||
|
|
||||||
.. _audit_template_definition:
|
.. _audit_template_definition:
|
||||||
|
|
||||||
Audit Template
|
Audit Template
|
||||||
==============
|
==============
|
||||||
|
|
||||||
An :ref:`Audit <audit_definition>` may be launched several times with the same
|
.. watcher-term:: watcher.api.controllers.v1.audit_template
|
||||||
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.
|
|
||||||
|
|
||||||
.. _availability_zone_definition:
|
.. _availability_zone_definition:
|
||||||
|
|
||||||
@@ -241,136 +99,14 @@ The :ref:`Cluster <cluster_definition>` may be divided in one or several
|
|||||||
Cluster Data Model
|
Cluster Data Model
|
||||||
==================
|
==================
|
||||||
|
|
||||||
A :ref:`Cluster Data Model <cluster_data_model_definition>` is a logical
|
.. watcher-term:: watcher.metrics_engine.cluster_model_collector.api
|
||||||
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, ...).
|
|
||||||
|
|
||||||
.. _cluster_history_definition:
|
.. _cluster_history_definition:
|
||||||
|
|
||||||
Cluster History
|
Cluster History
|
||||||
===============
|
===============
|
||||||
|
|
||||||
The :ref:`Cluster History <cluster_history_definition>` contains all the
|
.. watcher-term:: watcher.metrics_engine.cluster_history.api
|
||||||
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,...).
|
|
||||||
|
|
||||||
.. _controller_node_definition:
|
.. _controller_node_definition:
|
||||||
|
|
||||||
@@ -419,20 +155,7 @@ them, or at least reported to them.
|
|||||||
Goal
|
Goal
|
||||||
====
|
====
|
||||||
|
|
||||||
A :ref:`Goal <goal_definition>` is a human readable, observable and measurable
|
.. watcher-term:: watcher.api.controllers.v1.goal
|
||||||
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, ...
|
|
||||||
|
|
||||||
|
|
||||||
.. _host_aggregates_definition:
|
.. _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>`_.
|
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_definition:
|
||||||
|
|
||||||
SLA
|
SLA
|
||||||
@@ -610,67 +316,21 @@ which provides a good definition.
|
|||||||
Solution
|
Solution
|
||||||
========
|
========
|
||||||
|
|
||||||
A :ref:`Solution <solution_definition>` is a set of
|
.. watcher-term:: watcher.decision_engine.solution.base
|
||||||
: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.
|
|
||||||
|
|
||||||
.. _strategy_definition:
|
.. _strategy_definition:
|
||||||
|
|
||||||
Strategy
|
Strategy
|
||||||
========
|
========
|
||||||
|
|
||||||
A :ref:`Strategy <strategy_definition>` is an algorithm implementation which is
|
.. watcher-term:: watcher.decision_engine.strategy.strategies.base
|
||||||
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_applier_definition:
|
.. _watcher_applier_definition:
|
||||||
|
|
||||||
Watcher Applier
|
Watcher Applier
|
||||||
===============
|
===============
|
||||||
|
|
||||||
This component is in charge of executing the
|
.. watcher-term:: watcher.applier.base
|
||||||
: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_database_definition:
|
.. _watcher_database_definition:
|
||||||
|
|
||||||
@@ -696,47 +356,12 @@ See :doc:`architecture` for more details on this component.
|
|||||||
Watcher Decision Engine
|
Watcher Decision Engine
|
||||||
=======================
|
=======================
|
||||||
|
|
||||||
This component is responsible for computing a set of potential optimization
|
.. watcher-term:: watcher.decision_engine.manager
|
||||||
: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_planner_definition:
|
.. _watcher_planner_definition:
|
||||||
|
|
||||||
Watcher Planner
|
Watcher Planner
|
||||||
===============
|
===============
|
||||||
|
|
||||||
The :ref:`Watcher Planner <watcher_planner_definition>` is part of the
|
.. watcher-term:: watcher.decision_engine.planner.base
|
||||||
: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.
|
|
||||||
|
|
||||||
|
|||||||
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>
|
</text>
|
||||||
</g>
|
</g>
|
||||||
<g>
|
<g>
|
||||||
<rect style="fill: #ffb400" x="407" y="-0.832" width="162.3" height="89"/>
|
<rect style="fill: #ffb400" x="407" y="-38.25" width="325.386" height="126.418"/>
|
||||||
<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 407,-38.25 A 10,10 0 0 0 397,-28.25 L 407,-28.25 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"/>
|
<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="9.168" width="182.3" height="69"/>
|
<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 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"/>
|
<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="-0.832" x2="569.3" y2="-0.832"/>
|
<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="569.3" y2="88.168"/>
|
<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,-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 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 579.3,9.168 A 10,10 0 0 0 569.3,-0.832"/>
|
<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="9.168" x2="397" y2="78.168"/>
|
<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="579.3" y1="9.168" x2="579.3" 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 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"/>
|
<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="488.15" y="39.1291">
|
<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="488.15" y="39.1291">Watcher</tspan>
|
<tspan x="569.693" y="20.4201">Watcher</tspan>
|
||||||
<tspan x="488.15" y="56.768">Decision Engine</tspan>
|
<tspan x="569.693" y="38.059">Decision Engine</tspan>
|
||||||
</text>
|
</text>
|
||||||
</g>
|
</g>
|
||||||
<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"/>
|
<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="394.764,21.4013 384.727,26.3262 387.264,21.3451 384.802,16.3265 "/>
|
<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="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="338.622,37.0208 348.668,32.1139 346.122,37.0905 348.575,42.1135 "/>
|
||||||
</g>
|
<polygon style="fill: #000000" points="394.764,37.5426 384.718,42.4495 387.264,37.4729 384.811,32.4499 "/>
|
||||||
<g>
|
<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 "/>
|
||||||
<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 "/>
|
|
||||||
</g>
|
</g>
|
||||||
<g>
|
<g>
|
||||||
<rect style="fill: #ffb400" x="407" y="-159" width="160" height="89"/>
|
<rect style="fill: #ffb400" x="407" y="-159" width="160" height="89"/>
|
||||||
@@ -160,10 +155,10 @@
|
|||||||
</text>
|
</text>
|
||||||
</g>
|
</g>
|
||||||
<g>
|
<g>
|
||||||
<rect style="fill: #ffffff" x="639.986" y="-66.4" width="117" height="56"/>
|
<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="639.986" y="-66.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="698.486" y="-34.5">
|
<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="698.486" y="-34.5">Nova</tspan>
|
<tspan x="703.486" y="-89.5">Nova</tspan>
|
||||||
</text>
|
</text>
|
||||||
</g>
|
</g>
|
||||||
<g>
|
<g>
|
||||||
@@ -174,14 +169,9 @@
|
|||||||
</text>
|
</text>
|
||||||
</g>
|
</g>
|
||||||
<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"/>
|
<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="224.15,-56.0284 214.086,-51.1588 216.651,-56.1259 214.216,-61.1579 "/>
|
<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="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="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: #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 "/>
|
|
||||||
</g>
|
</g>
|
||||||
<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"/>
|
<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 "/>
|
<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>
|
||||||
<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"/>
|
<rect style="fill: #ffffff" x="522.61" y="138.001" width="137.55" height="38"/>
|
||||||
<polygon style="fill: #000000" points="638.394,-53.9699 627.762,-57.4302 633.053,-59.2355 634.783,-64.5512 "/>
|
<rect style="fill: none; fill-opacity:0; stroke-width: 2; stroke: #000000" x="522.61" y="138.001" width="137.55" height="38"/>
|
||||||
<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 "/>
|
<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">
|
||||||
</g>
|
<tspan x="591.385" y="160.901">Ceilometer API</tspan>
|
||||||
<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>
|
|
||||||
</text>
|
</text>
|
||||||
</g>
|
</g>
|
||||||
<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"/>
|
<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="841.15,54.4914 831.131,59.4527 833.65,54.4624 831.169,49.4528 "/>
|
<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="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="369.044,143.25 373.105,132.833 374.606,138.218 379.814,140.248 "/>
|
||||||
</g>
|
</g>
|
||||||
<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"/>
|
<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 "/>
|
<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>
|
||||||
<g>
|
<g>
|
||||||
<rect style="fill: #ffa200" x="506.36" y="72.1008" width="92.025" height="49.9"/>
|
<rect style="fill: #ffa200" x="422.36" y="60.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 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 608.385,82.1008 A 10,10 0 0 0 598.385,72.1008 L 598.385,82.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="496.36" y="82.1008" width="112.025" height="29.9"/>
|
<rect style="fill: #ffa200" x="412.36" y="70.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 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 598.385,122.001 A 10,10 0 0 0 608.385,112.001 L 598.385,112.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="506.36" y1="72.1008" x2="598.385" y2="72.1008"/>
|
<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="506.36" y1="122.001" x2="598.385" y2="122.001"/>
|
<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 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 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 608.385,82.1008 A 10,10 0 0 0 598.385,72.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="496.36" y1="82.1008" x2="496.36" y2="112.001"/>
|
<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="608.385" y1="82.1008" x2="608.385" y2="112.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 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 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 598.385,122.001 A 10,10 0 0 0 608.385,112.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="552.372" y="101.331">
|
<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="552.372" y="101.331">Strategy</tspan>
|
<tspan x="468.372" y="89.3314">Strategy</tspan>
|
||||||
</text>
|
</text>
|
||||||
</g>
|
</g>
|
||||||
<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"/>
|
<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="652.482,140.829 641.345,139.843 646.095,136.896 646.588,131.328 "/>
|
<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="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="556.641,131.602 545.712,129.248 550.79,126.911 551.967,121.446 "/>
|
||||||
</g>
|
</g>
|
||||||
<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 "/>
|
<rect style="fill: #ffa200" x="625.488" y="60.1008" width="80.5125" height="49.9"/>
|
||||||
<polygon style="fill: #000000" points="874.386,11.2639 869.386,1.26393 874.386,3.76393 879.386,1.26393 "/>
|
<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"/>
|
||||||
<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 "/>
|
<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>
|
||||||
<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"/>
|
<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="638.201,-23.0527 633.233,-13.0367 632.216,-18.5335 627.208,-21.0175 "/>
|
<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="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="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>
|
||||||
<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"/>
|
<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>
|
</text>
|
||||||
</g>
|
</g>
|
||||||
<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: #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 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 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 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"/>
|
<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="724.78" y="227.767">
|
<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="724.78" y="227.767">Cluster History DB</tspan>
|
<tspan x="354.72" y="227.767">Cluster Model DB</tspan>
|
||||||
</text>
|
</text>
|
||||||
</g>
|
</g>
|
||||||
<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 "/>
|
<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"/>
|
||||||
<polygon style="fill: #000000" points="899.386,12.7649 894.386,2.76493 899.386,5.26493 904.386,2.76493 "/>
|
<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"/>
|
||||||
<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: 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>
|
</g>
|
||||||
</svg>
|
</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/
|
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
|
Watcher project consists of several source code repositories:
|
||||||
on-the-go.
|
|
||||||
|
|
||||||
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
|
on the latest code, and may not represent the state of the project at any
|
||||||
specific prior release.
|
specific prior release.
|
||||||
|
|
||||||
|
.. _watcher: https://git.openstack.org/cgit/openstack/watcher/
|
||||||
|
.. _python-watcherclient: https://git.openstack.org/cgit/openstack/python-watcherclient/
|
||||||
|
|
||||||
Developer Guide
|
Developer Guide
|
||||||
===============
|
===============
|
||||||
|
|
||||||
@@ -29,9 +41,18 @@ Introduction
|
|||||||
|
|
||||||
glossary
|
glossary
|
||||||
architecture
|
architecture
|
||||||
dev/environment
|
|
||||||
dev/contributing
|
dev/contributing
|
||||||
dev/plugins
|
|
||||||
|
|
||||||
|
Getting Started
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 1
|
||||||
|
|
||||||
|
dev/environment
|
||||||
|
dev/devstack
|
||||||
|
deploy/configuration
|
||||||
|
|
||||||
|
|
||||||
API References
|
API References
|
||||||
@@ -42,25 +63,44 @@ API References
|
|||||||
|
|
||||||
webapi/v1
|
webapi/v1
|
||||||
|
|
||||||
|
Plugins
|
||||||
|
-------
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 1
|
||||||
|
|
||||||
|
dev/strategy-plugin
|
||||||
|
|
||||||
|
|
||||||
Admin Guide
|
Admin Guide
|
||||||
===========
|
===========
|
||||||
|
|
||||||
Overview
|
Introduction
|
||||||
--------
|
------------
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 1
|
:maxdepth: 1
|
||||||
|
|
||||||
deploy/user-guide
|
|
||||||
deploy/installation
|
deploy/installation
|
||||||
|
deploy/user-guide
|
||||||
|
|
||||||
Commands
|
Watcher Manual Pages
|
||||||
--------
|
====================
|
||||||
|
|
||||||
.. toctree::
|
.. 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
|
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:
|
||||||
|
|
||||||
=============
|
=================
|
||||||
watcher-db-manage
|
watcher-db-manage
|
||||||
=============
|
=================
|
||||||
|
|
||||||
The :command:`watcher-db-manage` utility is used to create the database schema
|
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
|
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/
|
https://creativecommons.org/licenses/by/3.0/
|
||||||
|
|
||||||
=====================
|
====================
|
||||||
RESTful Web API (v1)
|
RESTful Web API (v1)
|
||||||
=====================
|
====================
|
||||||
|
|
||||||
Audit Templates
|
Audit Templates
|
||||||
===============
|
===============
|
||||||
|
|
||||||
.. rest-controller:: watcher.api.controllers.v1.audit_template:AuditTemplatesController
|
.. 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
|
.. autotype:: watcher.api.controllers.v1.audit_template.AuditTemplateCollection
|
||||||
:members:
|
:members:
|
||||||
@@ -20,7 +20,6 @@ Audit Templates
|
|||||||
.. autotype:: watcher.api.controllers.v1.audit_template.AuditTemplate
|
.. autotype:: watcher.api.controllers.v1.audit_template.AuditTemplate
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
|
||||||
Audits
|
Audits
|
||||||
======
|
======
|
||||||
|
|
||||||
@@ -33,24 +32,22 @@ Audits
|
|||||||
.. autotype:: watcher.api.controllers.v1.audit.Audit
|
.. autotype:: watcher.api.controllers.v1.audit.Audit
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
|
||||||
Links
|
Links
|
||||||
=====
|
=====
|
||||||
|
|
||||||
.. autotype:: watcher.api.controllers.link.Link
|
.. autotype:: watcher.api.controllers.link.Link
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
Action Plans
|
||||||
ActionPlans
|
============
|
||||||
===========
|
|
||||||
|
|
||||||
.. rest-controller:: watcher.api.controllers.v1.action_plan:ActionPlansController
|
.. rest-controller:: watcher.api.controllers.v1.action_plan:ActionPlansController
|
||||||
:webprefix: /v1/action_plans
|
:webprefix: /v1/action_plans
|
||||||
|
|
||||||
.. autotype:: watcher.api.controllers.v1.action_plan.ActionPlan
|
.. autotype:: watcher.api.controllers.v1.action_plan.ActionPlanCollection
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
.. autotype:: watcher.api.controllers.v1.action_plan.ActionPlanCollection
|
.. autotype:: watcher.api.controllers.v1.action_plan.ActionPlan
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -26,4 +26,5 @@ python-openstackclient>=1.5.0
|
|||||||
six>=1.9.0
|
six>=1.9.0
|
||||||
SQLAlchemy>=0.9.9,<1.1.0
|
SQLAlchemy>=0.9.9,<1.1.0
|
||||||
stevedore>=1.5.0 # Apache-2.0
|
stevedore>=1.5.0 # Apache-2.0
|
||||||
|
taskflow>=1.25.0 # Apache-2.0
|
||||||
WSME>=0.7
|
WSME>=0.7
|
||||||
|
|||||||
28
setup.cfg
@@ -21,6 +21,7 @@ classifier =
|
|||||||
[files]
|
[files]
|
||||||
packages =
|
packages =
|
||||||
watcher
|
watcher
|
||||||
|
watcher_tempest_plugin
|
||||||
data_files =
|
data_files =
|
||||||
etc/ = etc/*
|
etc/ = etc/*
|
||||||
|
|
||||||
@@ -38,6 +39,9 @@ console_scripts =
|
|||||||
watcher-decision-engine = watcher.cmd.decisionengine:main
|
watcher-decision-engine = watcher.cmd.decisionengine:main
|
||||||
watcher-applier = watcher.cmd.applier:main
|
watcher-applier = watcher.cmd.applier:main
|
||||||
|
|
||||||
|
tempest.test_plugins =
|
||||||
|
watcher_tests = watcher_tempest_plugin.plugin:WatcherTempestPlugin
|
||||||
|
|
||||||
watcher.database.migration_backend =
|
watcher.database.migration_backend =
|
||||||
sqlalchemy = watcher.db.sqlalchemy.migration
|
sqlalchemy = watcher.db.sqlalchemy.migration
|
||||||
|
|
||||||
@@ -46,6 +50,28 @@ watcher_strategies =
|
|||||||
basic = watcher.decision_engine.strategy.strategies.basic_consolidation:BasicConsolidation
|
basic = watcher.decision_engine.strategy.strategies.basic_consolidation:BasicConsolidation
|
||||||
outlet_temp_control = watcher.decision_engine.strategy.strategies.outlet_temp_control:OutletTempControl
|
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]
|
[build_sphinx]
|
||||||
source-dir = doc/source
|
source-dir = doc/source
|
||||||
build-dir = doc/build
|
build-dir = doc/build
|
||||||
@@ -66,6 +92,6 @@ output_dir = watcher/locale
|
|||||||
input_file = watcher/locale/watcher.pot
|
input_file = watcher/locale/watcher.pot
|
||||||
|
|
||||||
[extract_messages]
|
[extract_messages]
|
||||||
keywords = _ gettext ngettext l_ lazy_gettext
|
keywords = _ gettext ngettext l_ lazy_gettext _LI _LW _LE _LC
|
||||||
mapping_file = babel.cfg
|
mapping_file = babel.cfg
|
||||||
output_file = watcher/locale/watcher.pot
|
output_file = watcher/locale/watcher.pot
|
||||||
|
|||||||
@@ -4,9 +4,11 @@
|
|||||||
|
|
||||||
coverage>=3.6
|
coverage>=3.6
|
||||||
discover
|
discover
|
||||||
|
doc8 # Apache-2.0
|
||||||
hacking>=0.10.2,<0.11
|
hacking>=0.10.2,<0.11
|
||||||
mock>=1.2
|
mock>=1.2
|
||||||
oslotest>=1.10.0 # Apache-2.0
|
oslotest>=1.10.0 # Apache-2.0
|
||||||
|
os-testr>=0.1.0
|
||||||
python-subunit>=0.0.18
|
python-subunit>=0.0.18
|
||||||
testrepository>=0.0.18
|
testrepository>=0.0.18
|
||||||
testscenarios>=0.4
|
testscenarios>=0.4
|
||||||
@@ -19,3 +21,4 @@ sphinxcontrib-pecanwsme>=0.8
|
|||||||
|
|
||||||
# For PyPI distribution
|
# For PyPI distribution
|
||||||
twine
|
twine
|
||||||
|
|
||||||
|
|||||||
21
tox.ini
@@ -14,19 +14,25 @@ deps = -r{toxinidir}/requirements.txt
|
|||||||
commands =
|
commands =
|
||||||
find . -type f -name "*.pyc" -delete
|
find . -type f -name "*.pyc" -delete
|
||||||
find . -type d -name "__pycache__" -delete
|
find . -type d -name "__pycache__" -delete
|
||||||
python setup.py testr --slowest --testr-args='{posargs}'
|
ostestr --concurrency=6 {posargs}
|
||||||
|
|
||||||
[testenv:pep8]
|
[testenv:pep8]
|
||||||
commands = flake8
|
commands =
|
||||||
|
doc8 doc/source/ CONTRIBUTING.rst HACKING.rst README.rst
|
||||||
|
flake8
|
||||||
|
|
||||||
[testenv:venv]
|
[testenv:venv]
|
||||||
|
setenv = PYTHONHASHSEED=0
|
||||||
commands = {posargs}
|
commands = {posargs}
|
||||||
|
|
||||||
[testenv:cover]
|
[testenv:cover]
|
||||||
commands = python setup.py testr --coverage --omit="watcher/tests/*" --testr-args='{posargs}'
|
commands = python setup.py testr --coverage --omit="watcher/tests/*" --testr-args='{posargs}'
|
||||||
|
|
||||||
[testenv:docs]
|
[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]
|
[testenv:debug]
|
||||||
commands = oslo_debug_helper {posargs}
|
commands = oslo_debug_helper {posargs}
|
||||||
@@ -42,10 +48,8 @@ commands =
|
|||||||
--output-file etc/watcher/watcher.conf.sample
|
--output-file etc/watcher/watcher.conf.sample
|
||||||
|
|
||||||
[flake8]
|
[flake8]
|
||||||
# E123, E125 skipped as they are invalid PEP-8.
|
|
||||||
|
|
||||||
show-source=True
|
show-source=True
|
||||||
ignore=E123,E125
|
ignore=
|
||||||
builtins= _
|
builtins= _
|
||||||
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,*sqlalchemy/alembic/versions/*,demo/
|
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]
|
[hacking]
|
||||||
import_exceptions = watcher._i18n
|
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
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# 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 datetime
|
||||||
|
|
||||||
import pecan
|
import pecan
|
||||||
@@ -87,9 +123,6 @@ class Action(base.APIBase):
|
|||||||
mandatory=True)
|
mandatory=True)
|
||||||
"""The action plan this action belongs to """
|
"""The action plan this action belongs to """
|
||||||
|
|
||||||
description = wtypes.text
|
|
||||||
"""Description of this action"""
|
|
||||||
|
|
||||||
state = wtypes.text
|
state = wtypes.text
|
||||||
"""This audit state"""
|
"""This audit state"""
|
||||||
|
|
||||||
@@ -99,17 +132,11 @@ class Action(base.APIBase):
|
|||||||
applies_to = wtypes.text
|
applies_to = wtypes.text
|
||||||
"""Applies to"""
|
"""Applies to"""
|
||||||
|
|
||||||
src = wtypes.text
|
|
||||||
"""Hypervisor source"""
|
|
||||||
|
|
||||||
dst = wtypes.text
|
|
||||||
"""Hypervisor source"""
|
|
||||||
|
|
||||||
action_type = wtypes.text
|
action_type = wtypes.text
|
||||||
"""Action type"""
|
"""Action type"""
|
||||||
|
|
||||||
parameter = wtypes.text
|
input_parameters = wtypes.DictType(wtypes.text, wtypes.text)
|
||||||
"""Additionnal parameter"""
|
"""One or more key/value pairs """
|
||||||
|
|
||||||
next_uuid = wsme.wsproperty(types.uuid, _get_next_uuid,
|
next_uuid = wsme.wsproperty(types.uuid, _get_next_uuid,
|
||||||
_set_next_uuid,
|
_set_next_uuid,
|
||||||
|
|||||||
@@ -15,6 +15,60 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# 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 datetime
|
||||||
|
|
||||||
import pecan
|
import pecan
|
||||||
@@ -23,21 +77,45 @@ import wsme
|
|||||||
from wsme import types as wtypes
|
from wsme import types as wtypes
|
||||||
import wsmeext.pecan as wsme_pecan
|
import wsmeext.pecan as wsme_pecan
|
||||||
|
|
||||||
|
from watcher._i18n import _
|
||||||
from watcher.api.controllers import base
|
from watcher.api.controllers import base
|
||||||
from watcher.api.controllers import link
|
from watcher.api.controllers import link
|
||||||
from watcher.api.controllers.v1 import collection
|
from watcher.api.controllers.v1 import collection
|
||||||
from watcher.api.controllers.v1 import types
|
from watcher.api.controllers.v1 import types
|
||||||
from watcher.api.controllers.v1 import utils as api_utils
|
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.common import exception
|
||||||
from watcher import objects
|
from watcher import objects
|
||||||
|
from watcher.objects import action_plan as ap_objects
|
||||||
|
|
||||||
|
|
||||||
class ActionPlanPatchType(types.JsonPatchType):
|
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
|
@staticmethod
|
||||||
def mandatory_attrs():
|
def mandatory_attrs():
|
||||||
return []
|
return ["audit_id", "state", "first_action_id"]
|
||||||
|
|
||||||
|
|
||||||
class ActionPlan(base.APIBase):
|
class ActionPlan(base.APIBase):
|
||||||
@@ -230,9 +308,9 @@ class ActionPlansController(rest.RestController):
|
|||||||
sort_key=sort_key,
|
sort_key=sort_key,
|
||||||
sort_dir=sort_dir)
|
sort_dir=sort_dir)
|
||||||
|
|
||||||
@wsme_pecan.wsexpose(ActionPlanCollection, types.uuid, types.uuid,
|
@wsme_pecan.wsexpose(ActionPlanCollection, types.uuid, int, wtypes.text,
|
||||||
int, wtypes.text, wtypes.text, types.uuid)
|
wtypes.text, types.uuid)
|
||||||
def get_all(self, action_plan_uuid=None, marker=None, limit=None,
|
def get_all(self, marker=None, limit=None,
|
||||||
sort_key='id', sort_dir='asc', audit_uuid=None):
|
sort_key='id', sort_dir='asc', audit_uuid=None):
|
||||||
"""Retrieve a list of action plans.
|
"""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_key: column to sort results by. Default: id.
|
||||||
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
|
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
|
||||||
:param audit_uuid: Optional UUID of an audit, to get only actions
|
:param audit_uuid: Optional UUID of an audit, to get only actions
|
||||||
for that audit.
|
for that audit.
|
||||||
"""
|
"""
|
||||||
return self._get_action_plans_collection(
|
return self._get_action_plans_collection(
|
||||||
marker, limit, sort_key, sort_dir, audit_uuid=audit_uuid)
|
marker, limit, sort_key, sort_dir, audit_uuid=audit_uuid)
|
||||||
|
|
||||||
@wsme_pecan.wsexpose(ActionPlanCollection, types.uuid, types.uuid,
|
@wsme_pecan.wsexpose(ActionPlanCollection, types.uuid, int, wtypes.text,
|
||||||
int, wtypes.text, wtypes.text, types.uuid)
|
wtypes.text, types.uuid)
|
||||||
def detail(self, action_plan_uuid=None, marker=None, limit=None,
|
def detail(self, marker=None, limit=None,
|
||||||
sort_key='id', sort_dir='asc', audit_uuid=None):
|
sort_key='id', sort_dir='asc', audit_uuid=None):
|
||||||
"""Retrieve a list of action_plans with detail.
|
"""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 marker: pagination marker for large data sets.
|
||||||
:param limit: maximum number of resources to return in a single result.
|
:param limit: maximum number of resources to return in a single result.
|
||||||
:param sort_key: column to sort results by. Default: id.
|
:param sort_key: column to sort results by. Default: id.
|
||||||
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
|
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
|
||||||
:param audit_uuid: Optional UUID of an audit, to get only actions
|
: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
|
# NOTE(lucasagomes): /detail should only work agaist collections
|
||||||
parent = pecan.request.path.split('/')[:-1][-1]
|
parent = pecan.request.path.split('/')[:-1][-1]
|
||||||
@@ -302,10 +378,10 @@ class ActionPlansController(rest.RestController):
|
|||||||
@wsme_pecan.wsexpose(ActionPlan, types.uuid,
|
@wsme_pecan.wsexpose(ActionPlan, types.uuid,
|
||||||
body=[ActionPlanPatchType])
|
body=[ActionPlanPatchType])
|
||||||
def patch(self, action_plan_uuid, patch):
|
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 action_plan_uuid: UUID of a action plan.
|
||||||
:param patch: a json PATCH document to apply to this audit template.
|
:param patch: a json PATCH document to apply to this action plan.
|
||||||
"""
|
"""
|
||||||
launch_action_plan = True
|
launch_action_plan = True
|
||||||
if self.from_actionsPlans:
|
if self.from_actionsPlans:
|
||||||
@@ -322,6 +398,34 @@ class ActionPlansController(rest.RestController):
|
|||||||
raise exception.PatchError(patch=patch, reason=e)
|
raise exception.PatchError(patch=patch, reason=e)
|
||||||
|
|
||||||
launch_action_plan = False
|
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
|
# Update only the fields that have changed
|
||||||
for field in objects.ActionPlan.fields:
|
for field in objects.ActionPlan.fields:
|
||||||
try:
|
try:
|
||||||
@@ -334,13 +438,14 @@ class ActionPlansController(rest.RestController):
|
|||||||
if action_plan_to_update[field] != patch_val:
|
if action_plan_to_update[field] != patch_val:
|
||||||
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
|
launch_action_plan = True
|
||||||
|
|
||||||
action_plan_to_update.save()
|
action_plan_to_update.save()
|
||||||
|
|
||||||
if launch_action_plan:
|
if launch_action_plan:
|
||||||
applier_client = ApplierAPI()
|
applier_client = rpcapi.ApplierAPI()
|
||||||
applier_client.launch_action_plan(pecan.request.context,
|
applier_client.launch_action_plan(pecan.request.context,
|
||||||
action_plan.uuid)
|
action_plan.uuid)
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,40 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# 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 datetime
|
||||||
|
|
||||||
import pecan
|
import pecan
|
||||||
@@ -23,6 +57,7 @@ import wsme
|
|||||||
from wsme import types as wtypes
|
from wsme import types as wtypes
|
||||||
import wsmeext.pecan as wsme_pecan
|
import wsmeext.pecan as wsme_pecan
|
||||||
|
|
||||||
|
from watcher._i18n import _
|
||||||
from watcher.api.controllers import base
|
from watcher.api.controllers import base
|
||||||
from watcher.api.controllers import link
|
from watcher.api.controllers import link
|
||||||
from watcher.api.controllers.v1 import collection
|
from watcher.api.controllers.v1 import collection
|
||||||
@@ -181,6 +216,7 @@ class AuditCollection(collection.Collection):
|
|||||||
"""A list containing audits objects"""
|
"""A list containing audits objects"""
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
|
super(AuditCollection, self).__init__()
|
||||||
self._type = 'audits'
|
self._type = 'audits'
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -257,10 +293,9 @@ class AuditsController(rest.RestController):
|
|||||||
sort_key=sort_key,
|
sort_key=sort_key,
|
||||||
sort_dir=sort_dir)
|
sort_dir=sort_dir)
|
||||||
|
|
||||||
@wsme_pecan.wsexpose(AuditCollection, types.uuid,
|
@wsme_pecan.wsexpose(AuditCollection, types.uuid, int, wtypes.text,
|
||||||
types.uuid, int, wtypes.text,
|
|
||||||
wtypes.text, 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):
|
sort_key='id', sort_dir='asc', audit_template=None):
|
||||||
"""Retrieve a list of audits.
|
"""Retrieve a list of audits.
|
||||||
|
|
||||||
@@ -275,13 +310,12 @@ class AuditsController(rest.RestController):
|
|||||||
sort_dir,
|
sort_dir,
|
||||||
audit_template=audit_template)
|
audit_template=audit_template)
|
||||||
|
|
||||||
@wsme_pecan.wsexpose(AuditCollection, types.uuid,
|
@wsme_pecan.wsexpose(AuditCollection, types.uuid, int, wtypes.text,
|
||||||
types.uuid, int, wtypes.text, wtypes.text)
|
wtypes.text)
|
||||||
def detail(self, audit_uuid=None, marker=None, limit=None,
|
def detail(self, marker=None, limit=None,
|
||||||
sort_key='id', sort_dir='asc'):
|
sort_key='id', sort_dir='asc'):
|
||||||
"""Retrieve a list of audits with detail.
|
"""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 marker: pagination marker for large data sets.
|
||||||
:param limit: maximum number of resources to return in a single result.
|
:param limit: maximum number of resources to return in a single result.
|
||||||
:param sort_key: column to sort results by. Default: id.
|
:param sort_key: column to sort results by. Default: id.
|
||||||
@@ -320,6 +354,11 @@ class AuditsController(rest.RestController):
|
|||||||
if self.from_audits:
|
if self.from_audits:
|
||||||
raise exception.OperationNotPermitted
|
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()
|
audit_dict = audit.as_dict()
|
||||||
context = pecan.request.context
|
context = pecan.request.context
|
||||||
new_audit = objects.Audit(context, **audit_dict)
|
new_audit = objects.Audit(context, **audit_dict)
|
||||||
|
|||||||
@@ -15,6 +15,39 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# 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 datetime
|
||||||
|
|
||||||
import pecan
|
import pecan
|
||||||
@@ -131,6 +164,7 @@ class AuditTemplateCollection(collection.Collection):
|
|||||||
"""A list containing audit templates objects"""
|
"""A list containing audit templates objects"""
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
|
super(AuditTemplateCollection, self).__init__()
|
||||||
self._type = 'audit_templates'
|
self._type = 'audit_templates'
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -15,6 +15,23 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# 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
|
from oslo_config import cfg
|
||||||
|
|
||||||
import pecan
|
import pecan
|
||||||
@@ -94,6 +111,7 @@ class GoalCollection(collection.Collection):
|
|||||||
"""A list containing goals objects"""
|
"""A list containing goals objects"""
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
|
super(GoalCollection, self).__init__()
|
||||||
self._type = 'goals'
|
self._type = 'goals'
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -69,8 +69,7 @@ class ParsableErrorMiddleware(object):
|
|||||||
if (state['status_code'] // 100) not in (2, 3):
|
if (state['status_code'] // 100) not in (2, 3):
|
||||||
req = webob.Request(environ)
|
req = webob.Request(environ)
|
||||||
if (req.accept.best_match(['application/json', 'application/xml']
|
if (req.accept.best_match(['application/json', 'application/xml']
|
||||||
) == 'application/xml'
|
) == 'application/xml'):
|
||||||
):
|
|
||||||
try:
|
try:
|
||||||
# simple check xml is valid
|
# simple check xml is valid
|
||||||
body = [et.ElementTree.tostring(
|
body = [et.ElementTree.tostring(
|
||||||
|
|||||||
@@ -18,51 +18,51 @@
|
|||||||
#
|
#
|
||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
|
|
||||||
from watcher.applier.action_plan.base import BaseActionPlanHandler
|
from watcher.applier.action_plan import base
|
||||||
from watcher.applier.default import DefaultApplier
|
from watcher.applier import default
|
||||||
from watcher.applier.messaging.events import Events
|
from watcher.applier.messaging import event_types
|
||||||
from watcher.common.messaging.events.event import Event
|
from watcher.common.messaging.events import event
|
||||||
from watcher.objects.action_plan import ActionPlan
|
from watcher.objects import action_plan as ap_objects
|
||||||
from watcher.objects.action_plan import Status
|
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class DefaultActionPlanHandler(BaseActionPlanHandler):
|
class DefaultActionPlanHandler(base.BaseActionPlanHandler):
|
||||||
def __init__(self, context, manager_applier, action_plan_uuid):
|
def __init__(self, context, applier_manager, action_plan_uuid):
|
||||||
super(DefaultActionPlanHandler, self).__init__()
|
super(DefaultActionPlanHandler, self).__init__()
|
||||||
self.ctx = context
|
self.ctx = context
|
||||||
self.action_plan_uuid = action_plan_uuid
|
self.action_plan_uuid = action_plan_uuid
|
||||||
self.manager_applier = manager_applier
|
self.applier_manager = applier_manager
|
||||||
|
|
||||||
def notify(self, uuid, event_type, state):
|
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.state = state
|
||||||
action_plan.save()
|
action_plan.save()
|
||||||
event = Event()
|
ev = event.Event()
|
||||||
event.type = event_type
|
ev.type = event_type
|
||||||
event.data = {}
|
ev.data = {}
|
||||||
payload = {'action_plan__uuid': uuid,
|
payload = {'action_plan__uuid': uuid,
|
||||||
'action_plan_state': state}
|
'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)
|
payload)
|
||||||
|
|
||||||
def execute(self):
|
def execute(self):
|
||||||
try:
|
try:
|
||||||
# update state
|
# update state
|
||||||
self.notify(self.action_plan_uuid,
|
self.notify(self.action_plan_uuid,
|
||||||
Events.LAUNCH_ACTION_PLAN,
|
event_types.EventTypes.LAUNCH_ACTION_PLAN,
|
||||||
Status.ONGOING)
|
ap_objects.State.ONGOING)
|
||||||
applier = DefaultApplier(self.manager_applier, self.ctx)
|
applier = default.DefaultApplier(self.applier_manager, self.ctx)
|
||||||
result = applier.execute(self.action_plan_uuid)
|
result = applier.execute(self.action_plan_uuid)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
LOG.exception(e)
|
||||||
result = False
|
result = False
|
||||||
LOG.error("Launch Action Plan " + unicode(e))
|
|
||||||
finally:
|
finally:
|
||||||
if result is True:
|
if result is True:
|
||||||
status = Status.SUCCEEDED
|
status = ap_objects.State.SUCCEEDED
|
||||||
else:
|
else:
|
||||||
status = Status.FAILED
|
status = ap_objects.State.FAILED
|
||||||
# update state
|
# 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)
|
status)
|
||||||
|
|||||||
@@ -16,30 +16,47 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
|
|
||||||
import six
|
import six
|
||||||
from watcher.decision_engine.strategy.common.level import StrategyLevel
|
|
||||||
|
|
||||||
|
|
||||||
@six.add_metaclass(abc.ABCMeta)
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
class BaseAction(object):
|
class BaseAction(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._level = StrategyLevel.conservative
|
self._input_parameters = {}
|
||||||
self._priority = 0
|
self._applies_to = ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def level(self):
|
def input_parameters(self):
|
||||||
return self._level
|
return self._input_parameters
|
||||||
|
|
||||||
@level.setter
|
@input_parameters.setter
|
||||||
def level(self, l):
|
def input_parameters(self, p):
|
||||||
self._level = l
|
self._input_parameters = p
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def priority(self):
|
def applies_to(self):
|
||||||
return self._priority
|
return self._applies_to
|
||||||
|
|
||||||
@priority.setter
|
@applies_to.setter
|
||||||
def priority(self, p):
|
def applies_to(self, a):
|
||||||
self._priority = p
|
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._i18n import _
|
||||||
from watcher.applier.primitives.base import BasePrimitive
|
from watcher.applier.actions import base
|
||||||
from watcher.applier.promise import Promise
|
from watcher.common import exception
|
||||||
from watcher.common.exception import IllegalArgumentException
|
from watcher.common import keystone as kclient
|
||||||
from watcher.common.keystone import KeystoneClient
|
from watcher.common import nova as nclient
|
||||||
from watcher.common.nova import NovaClient
|
from watcher.decision_engine.model import hypervisor_state as hstate
|
||||||
from watcher.decision_engine.model.hypervisor_state import HypervisorState
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
|
||||||
|
|
||||||
|
|
||||||
class ChangeNovaServiceState(BasePrimitive):
|
class ChangeNovaServiceState(base.BaseAction):
|
||||||
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
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def host(self):
|
def host(self):
|
||||||
return self._host
|
return self.applies_to
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
return self._state
|
return self.input_parameters.get('state')
|
||||||
|
|
||||||
@Promise
|
|
||||||
def execute(self):
|
def execute(self):
|
||||||
target_state = None
|
target_state = None
|
||||||
if self.state == HypervisorState.OFFLINE.value:
|
if self.state == hstate.HypervisorState.OFFLINE.value:
|
||||||
target_state = False
|
target_state = False
|
||||||
elif self.status == HypervisorState.ONLINE.value:
|
elif self.status == hstate.HypervisorState.ONLINE.value:
|
||||||
target_state = True
|
target_state = True
|
||||||
return self.nova_manage_service(target_state)
|
return self.nova_manage_service(target_state)
|
||||||
|
|
||||||
@Promise
|
def revert(self):
|
||||||
def undo(self):
|
|
||||||
target_state = None
|
target_state = None
|
||||||
if self.state == HypervisorState.OFFLINE.value:
|
if self.state == hstate.HypervisorState.OFFLINE.value:
|
||||||
target_state = True
|
target_state = True
|
||||||
elif self.state == HypervisorState.ONLINE.value:
|
elif self.state == hstate.HypervisorState.ONLINE.value:
|
||||||
target_state = False
|
target_state = False
|
||||||
return self.nova_manage_service(target_state)
|
return self.nova_manage_service(target_state)
|
||||||
|
|
||||||
def nova_manage_service(self, state):
|
def nova_manage_service(self, state):
|
||||||
if state is None:
|
if state is None:
|
||||||
raise IllegalArgumentException(
|
raise exception.IllegalArgumentException(
|
||||||
_("The target state is not defined"))
|
message=_("The target state is not defined"))
|
||||||
|
|
||||||
keystone = KeystoneClient()
|
keystone = kclient.KeystoneClient()
|
||||||
wrapper = NovaClient(keystone.get_credentials(),
|
wrapper = nclient.NovaClient(keystone.get_credentials(),
|
||||||
session=keystone.get_session())
|
session=keystone.get_session())
|
||||||
if state is True:
|
if state is True:
|
||||||
return wrapper.enable_service_nova_compute(self.host)
|
return wrapper.enable_service_nova_compute(self.host)
|
||||||
else:
|
else:
|
||||||
return wrapper.disable_service_nova_compute(self.host)
|
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.
|
# implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# 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):
|
class DefaultActionLoader(default.DefaultLoader):
|
||||||
def test_default_solution(self):
|
def __init__(self):
|
||||||
solution = DefaultSolution()
|
super(DefaultActionLoader, self).__init__(namespace='watcher_actions')
|
||||||
solution.add_change_request("BLA")
|
|
||||||
self.assertEqual(solution.actions[0], "BLA")
|
|
||||||
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 oslo_log import log
|
||||||
|
|
||||||
|
from watcher.applier.actions import base
|
||||||
from watcher.applier.primitives.base import BasePrimitive
|
|
||||||
from watcher.applier.promise import Promise
|
|
||||||
|
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
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):
|
def execute(self):
|
||||||
LOG.debug("executing NOP command")
|
LOG.debug("executing action NOP message:%s ", self.message)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@Promise
|
def revert(self):
|
||||||
def undo(self):
|
LOG.debug("revert action NOP")
|
||||||
LOG.debug("undo NOP command")
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def precondition(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def postcondition(self):
|
||||||
|
pass
|
||||||
@@ -16,17 +16,33 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
|
import time
|
||||||
|
|
||||||
from watcher.applier.primitives.base import BasePrimitive
|
from oslo_log import log
|
||||||
from watcher.applier.promise import Promise
|
|
||||||
|
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):
|
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 revert(self):
|
||||||
def undo(self):
|
LOG.debug("revert action Sleep")
|
||||||
raise NotImplementedError # pragma:no cover
|
return True
|
||||||
|
|
||||||
|
def precondition(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def postcondition(self):
|
||||||
|
pass
|
||||||
@@ -17,6 +17,14 @@
|
|||||||
# limitations under the License.
|
# 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 abc
|
||||||
import six
|
import six
|
||||||
|
|
||||||
|
|||||||
@@ -16,25 +16,48 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
from watcher.applier.base import BaseApplier
|
from watcher.applier import base
|
||||||
from watcher.applier.execution.executor import ActionPlanExecutor
|
from watcher.applier.workflow_engine.loading import default
|
||||||
from watcher.objects import Action
|
from watcher import objects
|
||||||
from watcher.objects import ActionPlan
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
class DefaultApplier(BaseApplier):
|
class DefaultApplier(base.BaseApplier):
|
||||||
def __init__(self, manager_applier, context):
|
def __init__(self, applier_manager, context):
|
||||||
super(DefaultApplier, self).__init__()
|
super(DefaultApplier, self).__init__()
|
||||||
self.manager_applier = manager_applier
|
self._applier_manager = applier_manager
|
||||||
self.context = context
|
self._loader = default.DefaultWorkFlowEngineLoader()
|
||||||
self.executor = ActionPlanExecutor(manager_applier, context)
|
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):
|
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
|
# 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}
|
||||||
filters={
|
actions = objects.Action.dbapi.get_action_list(self.context, filters)
|
||||||
'action_plan_id':
|
return self.engine.execute(actions)
|
||||||
action_plan.id})
|
|
||||||
return self.executor.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
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
|
||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
|
|
||||||
from watcher.applier.messaging.trigger import TriggerActionPlan
|
from watcher.applier.messaging import trigger
|
||||||
from watcher.common.messaging.messaging_core import MessagingCore
|
from watcher.common.messaging import messaging_core
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
LOG = log.getLogger(__name__)
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
# Register options
|
# Register options
|
||||||
APPLIER_MANAGER_OPTS = [
|
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',
|
cfg.StrOpt('topic_control',
|
||||||
default='watcher.applier.control',
|
default='watcher.applier.control',
|
||||||
help='The topic name used for'
|
help='The topic name used for'
|
||||||
@@ -45,7 +49,11 @@ APPLIER_MANAGER_OPTS = [
|
|||||||
cfg.StrOpt('publisher_id',
|
cfg.StrOpt('publisher_id',
|
||||||
default='watcher.applier.api',
|
default='watcher.applier.api',
|
||||||
help='The identifier used by watcher '
|
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',
|
opt_group = cfg.OptGroup(name='watcher_applier',
|
||||||
@@ -55,7 +63,7 @@ CONF.register_group(opt_group)
|
|||||||
CONF.register_opts(APPLIER_MANAGER_OPTS, opt_group)
|
CONF.register_opts(APPLIER_MANAGER_OPTS, opt_group)
|
||||||
|
|
||||||
|
|
||||||
class ApplierManager(MessagingCore):
|
class ApplierManager(messaging_core.MessagingCore):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(ApplierManager, self).__init__(
|
super(ApplierManager, self).__init__(
|
||||||
CONF.watcher_applier.publisher_id,
|
CONF.watcher_applier.publisher_id,
|
||||||
@@ -63,10 +71,7 @@ class ApplierManager(MessagingCore):
|
|||||||
CONF.watcher_applier.topic_status,
|
CONF.watcher_applier.topic_status,
|
||||||
api_version=self.API_VERSION,
|
api_version=self.API_VERSION,
|
||||||
)
|
)
|
||||||
# shared executor of the workflow
|
self.topic_control.add_endpoint(trigger.TriggerActionPlan(self))
|
||||||
self.executor = ThreadPoolExecutor(max_workers=1)
|
|
||||||
# trigger action_plan
|
|
||||||
self.topic_control.add_endpoint(TriggerActionPlan(self))
|
|
||||||
|
|
||||||
def join(self):
|
def join(self):
|
||||||
self.topic_control.join()
|
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.
|
# 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_PLAN = "launch_action_plan"
|
||||||
LAUNCH_ACTION = "launch_action"
|
LAUNCH_ACTION = "launch_action"
|
||||||
@@ -16,30 +16,35 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
|
from concurrent import futures
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
|
|
||||||
from watcher.applier.action_plan.default import DefaultActionPlanHandler
|
from watcher.applier.action_plan import default
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
LOG = log.getLogger(__name__)
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
class TriggerActionPlan(object):
|
class TriggerActionPlan(object):
|
||||||
def __init__(self, manager_applier):
|
def __init__(self, applier_manager):
|
||||||
self.manager_applier = manager_applier
|
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):
|
def do_launch_action_plan(self, context, action_plan_uuid):
|
||||||
try:
|
try:
|
||||||
cmd = DefaultActionPlanHandler(context,
|
cmd = default.DefaultActionPlanHandler(context,
|
||||||
self.manager_applier,
|
self.applier_manager,
|
||||||
action_plan_uuid)
|
action_plan_uuid)
|
||||||
cmd.execute()
|
cmd.execute()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.exception(e)
|
LOG.exception(e)
|
||||||
|
|
||||||
def launch_action_plan(self, context, action_plan_uuid):
|
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
|
# submit
|
||||||
self.manager_applier.executor.submit(self.do_launch_action_plan,
|
self.executor.submit(self.do_launch_action_plan, context,
|
||||||
context,
|
action_plan_uuid)
|
||||||
action_plan_uuid)
|
|
||||||
return 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 APPLIER_MANAGER_OPTS
|
||||||
from watcher.applier.manager import opt_group
|
from watcher.applier.manager import opt_group
|
||||||
from watcher.common import exception
|
from watcher.common import exception
|
||||||
from watcher.common.messaging.messaging_core import MessagingCore
|
from watcher.common.messaging import messaging_core
|
||||||
from watcher.common.messaging.notification_handler import NotificationHandler
|
from watcher.common.messaging import notification_handler as notification
|
||||||
from watcher.common import utils
|
from watcher.common import utils
|
||||||
|
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ CONF.register_group(opt_group)
|
|||||||
CONF.register_opts(APPLIER_MANAGER_OPTS, opt_group)
|
CONF.register_opts(APPLIER_MANAGER_OPTS, opt_group)
|
||||||
|
|
||||||
|
|
||||||
class ApplierAPI(MessagingCore):
|
class ApplierAPI(messaging_core.MessagingCore):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(ApplierAPI, self).__init__(
|
super(ApplierAPI, self).__init__(
|
||||||
@@ -43,7 +43,7 @@ class ApplierAPI(MessagingCore):
|
|||||||
CONF.watcher_applier.topic_status,
|
CONF.watcher_applier.topic_status,
|
||||||
api_version=self.API_VERSION,
|
api_version=self.API_VERSION,
|
||||||
)
|
)
|
||||||
self.handler = NotificationHandler(self.publisher_id)
|
self.handler = notification.NotificationHandler(self.publisher_id)
|
||||||
self.handler.register_observer(self)
|
self.handler.register_observer(self)
|
||||||
self.topic_status.add_endpoint(self.handler)
|
self.topic_status.add_endpoint(self.handler)
|
||||||
transport = om.get_transport(CONF)
|
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
|
This query can be then used for querying resources, meters and
|
||||||
statistics.
|
statistics.
|
||||||
:Parameters:
|
:Parameters:
|
||||||
- `user_id`: user_id, has a priority over list of ids
|
- `user_id`: user_id, has a priority over list of ids
|
||||||
- `tenant_id`: tenant_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
|
- `resource_id`: resource_id, has a priority over list of ids
|
||||||
- `user_ids`: list of user_ids
|
- `user_ids`: list of user_ids
|
||||||
- `tenant_ids`: list of tenant_ids
|
- `tenant_ids`: list of tenant_ids
|
||||||
- `resource_ids`: list of resource_ids
|
- `resource_ids`: list of resource_ids
|
||||||
"""
|
"""
|
||||||
|
|
||||||
user_ids = user_ids or []
|
user_ids = user_ids or []
|
||||||
|
|||||||
@@ -40,20 +40,15 @@ CONF = cfg.CONF
|
|||||||
CONF.register_opts(exc_log_opts)
|
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):
|
class WatcherException(Exception):
|
||||||
"""Base Watcher Exception
|
"""Base Watcher Exception
|
||||||
|
|
||||||
To correctly use this class, inherit from it and define
|
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.
|
with the keyword arguments provided to the constructor.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
message = _("An unknown exception occurred")
|
msg_fmt = _("An unknown exception occurred")
|
||||||
code = 500
|
code = 500
|
||||||
headers = {}
|
headers = {}
|
||||||
safe = False
|
safe = False
|
||||||
@@ -69,20 +64,19 @@ class WatcherException(Exception):
|
|||||||
|
|
||||||
if not message:
|
if not message:
|
||||||
try:
|
try:
|
||||||
message = self.message % kwargs
|
message = self.msg_fmt % kwargs
|
||||||
|
|
||||||
except Exception as e:
|
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 the issue and the kwargs
|
||||||
LOG.exception(_LE('Exception in string format operation'))
|
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)
|
LOG.error("%s: %s", name, value)
|
||||||
|
|
||||||
if CONF.fatal_exception_format_errors:
|
if CONF.fatal_exception_format_errors:
|
||||||
raise e
|
raise e
|
||||||
else:
|
else:
|
||||||
# at least get the core message out if something happened
|
# at least get the core msg_fmt out if something happened
|
||||||
message = self.message
|
message = self.msg_fmt
|
||||||
|
|
||||||
super(WatcherException, self).__init__(message)
|
super(WatcherException, self).__init__(message)
|
||||||
|
|
||||||
@@ -94,7 +88,7 @@ class WatcherException(Exception):
|
|||||||
return self.args[0]
|
return self.args[0]
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return self.message
|
return unicode(self.args[0])
|
||||||
|
|
||||||
def format_message(self):
|
def format_message(self):
|
||||||
if self.__class__.__name__.endswith('_Remote'):
|
if self.__class__.__name__.endswith('_Remote'):
|
||||||
@@ -104,114 +98,114 @@ class WatcherException(Exception):
|
|||||||
|
|
||||||
|
|
||||||
class NotAuthorized(WatcherException):
|
class NotAuthorized(WatcherException):
|
||||||
message = _("Not authorized")
|
msg_fmt = _("Not authorized")
|
||||||
code = 403
|
code = 403
|
||||||
|
|
||||||
|
|
||||||
class OperationNotPermitted(NotAuthorized):
|
class OperationNotPermitted(NotAuthorized):
|
||||||
message = _("Operation not permitted")
|
msg_fmt = _("Operation not permitted")
|
||||||
|
|
||||||
|
|
||||||
class Invalid(WatcherException):
|
class Invalid(WatcherException):
|
||||||
message = _("Unacceptable parameters")
|
msg_fmt = _("Unacceptable parameters")
|
||||||
code = 400
|
code = 400
|
||||||
|
|
||||||
|
|
||||||
class ObjectNotFound(WatcherException):
|
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):
|
class Conflict(WatcherException):
|
||||||
message = _('Conflict')
|
msg_fmt = _('Conflict')
|
||||||
code = 409
|
code = 409
|
||||||
|
|
||||||
|
|
||||||
class ResourceNotFound(ObjectNotFound):
|
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
|
code = 404
|
||||||
|
|
||||||
|
|
||||||
class InvalidIdentity(Invalid):
|
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):
|
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.
|
# Cannot be templated as the error syntax varies.
|
||||||
# msg needs to be constructed when raised.
|
# msg needs to be constructed when raised.
|
||||||
class InvalidParameterValue(Invalid):
|
class InvalidParameterValue(Invalid):
|
||||||
message = _("%(err)s")
|
msg_fmt = _("%(err)s")
|
||||||
|
|
||||||
|
|
||||||
class InvalidUUID(Invalid):
|
class InvalidUUID(Invalid):
|
||||||
message = _("Expected a uuid but received %(uuid)s")
|
msg_fmt = _("Expected a uuid but received %(uuid)s")
|
||||||
|
|
||||||
|
|
||||||
class InvalidName(Invalid):
|
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):
|
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):
|
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):
|
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")
|
"already exists")
|
||||||
|
|
||||||
|
|
||||||
class AuditTemplateReferenced(Invalid):
|
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")
|
"multiple audit")
|
||||||
|
|
||||||
|
|
||||||
class AuditNotFound(ResourceNotFound):
|
class AuditNotFound(ResourceNotFound):
|
||||||
message = _("Audit %(audit)s could not be found")
|
msg_fmt = _("Audit %(audit)s could not be found")
|
||||||
|
|
||||||
|
|
||||||
class AuditAlreadyExists(Conflict):
|
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):
|
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")
|
"plans")
|
||||||
|
|
||||||
|
|
||||||
class ActionPlanNotFound(ResourceNotFound):
|
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):
|
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):
|
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")
|
"multiple actions")
|
||||||
|
|
||||||
|
|
||||||
class ActionNotFound(ResourceNotFound):
|
class ActionNotFound(ResourceNotFound):
|
||||||
message = _("Action %(action)s could not be found")
|
msg_fmt = _("Action %(action)s could not be found")
|
||||||
|
|
||||||
|
|
||||||
class ActionAlreadyExists(Conflict):
|
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):
|
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")
|
"multiple goals")
|
||||||
|
|
||||||
|
|
||||||
class ActionFilterCombinationProhibited(Invalid):
|
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")
|
"prohibited")
|
||||||
|
|
||||||
|
|
||||||
@@ -220,84 +214,49 @@ class HTTPNotFound(ResourceNotFound):
|
|||||||
|
|
||||||
|
|
||||||
class PatchError(Invalid):
|
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
|
# decision engine
|
||||||
|
|
||||||
|
|
||||||
class BaseException(Exception):
|
class IllegalArgumentException(WatcherException):
|
||||||
|
msg_fmt = _('Illegal argument')
|
||||||
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(BaseException):
|
class NoSuchMetric(WatcherException):
|
||||||
def __init__(self, desc):
|
msg_fmt = _('No such metric')
|
||||||
desc = desc or _("Description cannot be empty")
|
|
||||||
super(IllegalArgumentException, self).__init__(desc)
|
|
||||||
|
|
||||||
def get_message(self):
|
|
||||||
return self._desc
|
|
||||||
|
|
||||||
|
|
||||||
class NoSuchMetric(BaseException):
|
class NoDataFound(WatcherException):
|
||||||
def __init__(self, desc):
|
msg_fmt = _('No rows were returned')
|
||||||
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 KeystoneFailure(WatcherException):
|
class KeystoneFailure(WatcherException):
|
||||||
message = _("'Keystone API endpoint is missing''")
|
msg_fmt = _("'Keystone API endpoint is missing''")
|
||||||
|
|
||||||
|
|
||||||
class ClusterEmpty(WatcherException):
|
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):
|
class MetricCollectorNotDefined(WatcherException):
|
||||||
message = _("The metrics resource collector is not defined")
|
msg_fmt = _("The metrics resource collector is not defined")
|
||||||
|
|
||||||
|
|
||||||
class ClusterStateNotDefined(WatcherException):
|
class ClusterStateNotDefined(WatcherException):
|
||||||
message = _("the cluster state is not defined")
|
msg_fmt = _("the cluster state is not defined")
|
||||||
|
|
||||||
|
|
||||||
# Model
|
# Model
|
||||||
|
|
||||||
class VMNotFound(WatcherException):
|
class InstanceNotFound(WatcherException):
|
||||||
message = _("The VM could not be found")
|
msg_fmt = _("The instance '%(name)s' is not found")
|
||||||
|
|
||||||
|
|
||||||
class HypervisorNotFound(WatcherException):
|
class HypervisorNotFound(WatcherException):
|
||||||
message = _("The hypervisor could not be found")
|
msg_fmt = _("The hypervisor is not found")
|
||||||
|
|
||||||
|
|
||||||
class MetaActionNotFound(WatcherException):
|
class LoadingError(WatcherException):
|
||||||
message = _("The Meta-Action could not be found")
|
msg_fmt = _("Error loading plugin '%(name)s'")
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
# -*- encoding: utf-8 -*-
|
||||||
# Copyright (c) 2015 b<>com
|
# 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");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
# You may obtain a copy of the License at
|
# 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
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
@@ -15,20 +13,20 @@
|
|||||||
# implied.
|
# implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
import six
|
import six
|
||||||
from watcher.applier.promise import Promise
|
|
||||||
|
|
||||||
|
|
||||||
@six.add_metaclass(abc.ABCMeta)
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
class BasePrimitive(object):
|
class BaseLoader(object):
|
||||||
@Promise
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def execute(self):
|
def list_available(self):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
@Promise
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def undo(self):
|
def load(self, name):
|
||||||
raise NotImplementedError()
|
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)
|
self.__server = self.build_server(target)
|
||||||
else:
|
else:
|
||||||
LOG.warn(
|
LOG.warning(
|
||||||
_LW("No endpoint defined; can only publish events"))
|
_LW("No endpoint defined; can only publish events"))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.exception(e)
|
LOG.exception(e)
|
||||||
|
|||||||
@@ -76,9 +76,9 @@ class NovaClient(object):
|
|||||||
|
|
||||||
:param instance_id: the unique id of the instance to migrate.
|
:param instance_id: the unique id of the instance to migrate.
|
||||||
:param keep_original_image_name: flag indicating whether the
|
:param keep_original_image_name: flag indicating whether the
|
||||||
image name from which the original instance was built must be
|
image name from which the original instance was built must be
|
||||||
used as the name of the intermediate image used for migration.
|
used as the name of the intermediate image used for migration.
|
||||||
If this flag is False, a temporary image name is built
|
If this flag is False, a temporary image name is built
|
||||||
"""
|
"""
|
||||||
|
|
||||||
new_image_name = ""
|
new_image_name = ""
|
||||||
@@ -328,7 +328,7 @@ class NovaClient(object):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def live_migrate_instance(self, instance_id, dest_hostname,
|
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 does a live migration of a given instance
|
||||||
|
|
||||||
This method uses the Nova built-in live_migrate()
|
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 waits for this image to be in 'active' state before returning.
|
||||||
It returns the unique UUID of the created image if successful,
|
It returns the unique UUID of the created image if successful,
|
||||||
None otherwise
|
None otherwise.
|
||||||
|
|
||||||
:param instance_id: the uniqueid of
|
: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 image_name: the name of the image to create.
|
||||||
:param metadata: a dictionary containing the list of
|
: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:
|
if self.glance is None:
|
||||||
glance_endpoint = self.keystone. \
|
glance_endpoint = self.keystone. \
|
||||||
@@ -568,7 +568,7 @@ class NovaClient(object):
|
|||||||
|
|
||||||
:param instance: instance object.
|
:param instance: instance object.
|
||||||
:param status_list: tuple containing the list of
|
: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 retry: how many times to retry
|
||||||
:param sleep: seconds to sleep between the retries
|
: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):
|
if not isinstance(value, six.string_types):
|
||||||
LOG.warn(_LW("Failed to remove trailing character. Returning original "
|
LOG.warning(_LW(
|
||||||
"object. Supplied object is not a string: %s,"), value)
|
"Failed to remove trailing character. Returning original object."
|
||||||
|
"Supplied object is not a string: %s,"), value)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
return value.rstrip(chars) or 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()
|
|
||||||