[WIP] Add hosts APIs and compute rpc APIs
Change-Id: I5893bf066fc876e795f020214080fb92b79a00a5
This commit is contained in:
parent
2f791de40e
commit
a40f556027
9
devstack/README.rst
Normal file
9
devstack/README.rst
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
====================
|
||||||
|
DevStack Integration
|
||||||
|
====================
|
||||||
|
|
||||||
|
This directory contains the files necessary to integrate gyan with devstack.
|
||||||
|
|
||||||
|
Refer the quickstart guide at
|
||||||
|
https://docs.openstack.org/gyan/latest/contributor/quickstart.html
|
||||||
|
for more information on using devstack and gyan.
|
325
devstack/lib/gyan
Normal file
325
devstack/lib/gyan
Normal file
@ -0,0 +1,325 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# lib/gyan
|
||||||
|
# Functions to control the configuration and operation of the **gyan** service
|
||||||
|
|
||||||
|
# Dependencies:
|
||||||
|
#
|
||||||
|
# - ``functions`` file
|
||||||
|
# - ``DEST``, ``DATA_DIR``, ``STACK_USER`` must be defined
|
||||||
|
# - ``SERVICE_{TENANT_NAME|PASSWORD}`` must be defined
|
||||||
|
|
||||||
|
# ``stack.sh`` calls the entry points in this order:
|
||||||
|
#
|
||||||
|
# - install_gyan
|
||||||
|
# - configure_gyan
|
||||||
|
# - create_gyan_conf
|
||||||
|
# - create_gyan_accounts
|
||||||
|
# - init_gyan
|
||||||
|
# - start_gyan
|
||||||
|
# - stop_gyan
|
||||||
|
# - cleanup_gyan
|
||||||
|
|
||||||
|
# Save trace setting
|
||||||
|
XTRACE=$(set +o | grep xtrace)
|
||||||
|
set +o xtrace
|
||||||
|
|
||||||
|
|
||||||
|
# Defaults
|
||||||
|
# --------
|
||||||
|
|
||||||
|
# Set up default directories
|
||||||
|
GYAN_REPO=${GYAN_REPO:-${GIT_BASE}/openstack/gyan.git}
|
||||||
|
GYAN_BRANCH=${GYAN_BRANCH:-master}
|
||||||
|
GYAN_DIR=$DEST/gyan
|
||||||
|
|
||||||
|
GITREPO["python-gyanclient"]=${GYANCLIENT_REPO:-${GIT_BASE}/openstack/python-gyanclient.git}
|
||||||
|
GITBRANCH["python-gyanclient"]=${GYANCLIENT_BRANCH:-master}
|
||||||
|
GITDIR["python-gyanclient"]=$DEST/python-gyanclient
|
||||||
|
|
||||||
|
GYAN_STATE_PATH=${GYAN_STATE_PATH:=$DATA_DIR/gyan}
|
||||||
|
GYAN_AUTH_CACHE_DIR=${GYAN_AUTH_CACHE_DIR:-/var/cache/gyan}
|
||||||
|
|
||||||
|
GYAN_CONF_DIR=/etc/gyan
|
||||||
|
GYAN_CONF=$GYAN_CONF_DIR/gyan.conf
|
||||||
|
GYAN_API_PASTE=$GYAN_CONF_DIR/api-paste.ini
|
||||||
|
|
||||||
|
if is_ssl_enabled_service "gyan" || is_service_enabled tls-proxy; then
|
||||||
|
GYAN_SERVICE_PROTOCOL="https"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Toggle for deploying GYAN-API under a wsgi server
|
||||||
|
GYAN_USE_UWSGI=${GYAN_USE_UWSGI:-True}
|
||||||
|
|
||||||
|
|
||||||
|
# Public facing bits
|
||||||
|
GYAN_SERVICE_HOST=${GYAN_SERVICE_HOST:-$SERVICE_HOST}
|
||||||
|
GYAN_SERVICE_PORT=${GYAN_SERVICE_PORT:-8517}
|
||||||
|
GYAN_SERVICE_PORT_INT=${GYAN_SERVICE_PORT_INT:-18517}
|
||||||
|
GYAN_SERVICE_PROTOCOL=${GYAN_SERVICE_PROTOCOL:-$SERVICE_PROTOCOL}
|
||||||
|
|
||||||
|
GYAN_TRUSTEE_DOMAIN_ADMIN_PASSWORD=${GYAN_TRUSTEE_DOMAIN_ADMIN_PASSWORD:-secret}
|
||||||
|
|
||||||
|
# Support entry points installation of console scripts
|
||||||
|
if [[ -d $GYAN_DIR/bin ]]; then
|
||||||
|
GYAN_BIN_DIR=$GYAN_DIR/bin
|
||||||
|
else
|
||||||
|
GYAN_BIN_DIR=$(get_python_exec_prefix)
|
||||||
|
fi
|
||||||
|
|
||||||
|
GYAN_UWSGI=$GYAN_BIN_DIR/gyan-api-wsgi
|
||||||
|
GYAN_UWSGI_CONF=$GYAN_CONF_DIR/gyan-api-uwsgi.ini
|
||||||
|
|
||||||
|
GYAN_DB_TYPE=${GYAN_DB_TYPE:-sql}
|
||||||
|
|
||||||
|
if is_ubuntu; then
|
||||||
|
UBUNTU_RELEASE_BASE_NUM=`lsb_release -r | awk '{print $2}' | cut -d '.' -f 1`
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Functions
|
||||||
|
# ---------
|
||||||
|
|
||||||
|
# cleanup_gyan() - Remove residual data files, anything left over from previous
|
||||||
|
# runs that a clean run would need to clean up
|
||||||
|
function cleanup_gyan {
|
||||||
|
sudo rm -rf $GYAN_STATE_PATH $GYAN_AUTH_CACHE_DIR
|
||||||
|
|
||||||
|
remove_uwsgi_config "$GYAN_UWSGI_CONF" "$GYAN_UWSGI"
|
||||||
|
}
|
||||||
|
|
||||||
|
# configure_gyan() - Set config files, create data dirs, etc
|
||||||
|
function configure_gyan {
|
||||||
|
# Put config files in ``/etc/gyan`` for everyone to find
|
||||||
|
if [[ ! -d $GYAN_CONF_DIR ]]; then
|
||||||
|
sudo mkdir -p $GYAN_CONF_DIR
|
||||||
|
sudo chown $STACK_USER $GYAN_CONF_DIR
|
||||||
|
fi
|
||||||
|
|
||||||
|
configure_rootwrap gyan
|
||||||
|
|
||||||
|
# Rebuild the config file from scratch
|
||||||
|
create_gyan_conf
|
||||||
|
|
||||||
|
create_api_paste_conf
|
||||||
|
|
||||||
|
write_uwsgi_config "$GYAN_UWSGI_CONF" "$GYAN_UWSGI" "/ml-infra"
|
||||||
|
|
||||||
|
if [[ "$USE_PYTHON3" = "True" ]]; then
|
||||||
|
# Switch off glance->swift communication as swift fails under py3.x
|
||||||
|
iniset /etc/glance/glance-api.conf glance_store default_store file
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# create_gyan_accounts() - Set up common required GYAN accounts
|
||||||
|
#
|
||||||
|
# Project User Roles
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# SERVICE_PROJECT_NAME gyan service
|
||||||
|
function create_gyan_accounts {
|
||||||
|
|
||||||
|
create_service_user "gyan" "admin"
|
||||||
|
|
||||||
|
if is_service_enabled gyan-api; then
|
||||||
|
|
||||||
|
local gyan_api_url
|
||||||
|
if [[ "$GYAN_USE_UWSGI" == "True" ]]; then
|
||||||
|
gyan_api_url="$GYAN_SERVICE_PROTOCOL://$GYAN_SERVICE_HOST/ml-infra"
|
||||||
|
else
|
||||||
|
gyan_api_url="$GYAN_SERVICE_PROTOCOL://$GYAN_SERVICE_HOST:$GYAN_SERVICE_PORT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local gyan_service=$(get_or_create_service "gyan" \
|
||||||
|
"ml-infra" "ML Infra As Service")
|
||||||
|
get_or_create_endpoint $gyan_service \
|
||||||
|
"$REGION_NAME" \
|
||||||
|
"$gyan_api_url/v1" \
|
||||||
|
"$gyan_api_url/v1" \
|
||||||
|
"$gyan_api_url/v1"
|
||||||
|
fi
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
# create_gyan_conf() - Create a new gyan.conf file
|
||||||
|
function create_gyan_conf {
|
||||||
|
|
||||||
|
# (Re)create ``gyan.conf``
|
||||||
|
rm -f $GYAN_CONF
|
||||||
|
if [[ ${GYAN_DRIVER} == "tensorflow" ]]; then
|
||||||
|
iniset $GYAN_CONF DEFAULT ml_model_driver "ml_model.driver.TensorflowDriver"
|
||||||
|
fi
|
||||||
|
if [[ ${GYAN_DB_TYPE} == "sql" ]]; then
|
||||||
|
iniset $GYAN_CONF DEFAULT db_type sql
|
||||||
|
fi
|
||||||
|
iniset $GYAN_CONF DEFAULT debug "$ENABLE_DEBUG_LOG_LEVEL"
|
||||||
|
iniset $GYAN_CONF DEFAULT my_ip "$HOST_IP"
|
||||||
|
iniset $GYAN_CONF DEFAULT host "$HOST_IP"
|
||||||
|
iniset $GYAN_CONF oslo_messaging_rabbit rabbit_userid $RABBIT_USERID
|
||||||
|
iniset $GYAN_CONF oslo_messaging_rabbit rabbit_password $RABBIT_PASSWORD
|
||||||
|
iniset $GYAN_CONF oslo_messaging_rabbit rabbit_host $RABBIT_HOST
|
||||||
|
iniset $GYAN_CONF database connection `database_connection_url gyan`
|
||||||
|
iniset $GYAN_CONF api host_ip "$GYAN_SERVICE_HOST"
|
||||||
|
iniset $GYAN_CONF api port "$GYAN_SERVICE_PORT"
|
||||||
|
|
||||||
|
iniset $GYAN_CONF keystone_auth auth_type password
|
||||||
|
iniset $GYAN_CONF keystone_auth username gyan
|
||||||
|
iniset $GYAN_CONF keystone_auth password $SERVICE_PASSWORD
|
||||||
|
iniset $GYAN_CONF keystone_auth project_name $SERVICE_PROJECT_NAME
|
||||||
|
iniset $GYAN_CONF keystone_auth project_domain_id default
|
||||||
|
iniset $GYAN_CONF keystone_auth user_domain_id default
|
||||||
|
|
||||||
|
# FIXME(pauloewerton): keystone_authtoken section is deprecated. Remove it
|
||||||
|
# after deprecation period.
|
||||||
|
iniset $GYAN_CONF keystone_authtoken admin_user gyan
|
||||||
|
iniset $GYAN_CONF keystone_authtoken admin_password $SERVICE_PASSWORD
|
||||||
|
iniset $GYAN_CONF keystone_authtoken admin_tenant_name $SERVICE_PROJECT_NAME
|
||||||
|
|
||||||
|
configure_auth_token_middleware $GYAN_CONF gyan $GYAN_AUTH_CACHE_DIR
|
||||||
|
|
||||||
|
iniset $GYAN_CONF keystone_auth auth_url $KEYSTONE_AUTH_URI_V3
|
||||||
|
iniset $GYAN_CONF keystone_authtoken www_authenticate_uri $KEYSTONE_SERVICE_URI_V3
|
||||||
|
iniset $GYAN_CONF keystone_authtoken auth_url $KEYSTONE_AUTH_URI_V3
|
||||||
|
iniset $GYAN_CONF keystone_authtoken auth_version v3
|
||||||
|
|
||||||
|
|
||||||
|
if is_fedora || is_suse; then
|
||||||
|
# gyan defaults to /usr/local/bin, but fedora and suse pip like to
|
||||||
|
# install things in /usr/bin
|
||||||
|
iniset $GYAN_CONF DEFAULT bindir "/usr/bin"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$GYAN_STATE_PATH" ]; then
|
||||||
|
iniset $GYAN_CONF DEFAULT state_path "$GYAN_STATE_PATH"
|
||||||
|
iniset $GYAN_CONF oslo_concurrency lock_path "$GYAN_STATE_PATH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$SYSLOG" != "False" ]; then
|
||||||
|
iniset $GYAN_CONF DEFAULT use_syslog "True"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Format logging
|
||||||
|
if [ "$LOG_COLOR" == "True" ] && [ "$SYSLOG" == "False" ]; then
|
||||||
|
setup_colorized_logging $GYAN_CONF DEFAULT
|
||||||
|
else
|
||||||
|
# Show user_name and project_name instead of user_id and project_id
|
||||||
|
iniset $GYAN_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 gyan; then
|
||||||
|
ensure_certificates gyan
|
||||||
|
|
||||||
|
iniset $GYAN_CONF DEFAULT ssl_cert_file "$GYAN_SSL_CERT"
|
||||||
|
iniset $GYAN_CONF DEFAULT ssl_key_file "$GYAN_SSL_KEY"
|
||||||
|
|
||||||
|
iniset $GYAN_CONF DEFAULT enabled_ssl_apis "$GYAN_ENABLED_APIS"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function create_api_paste_conf {
|
||||||
|
# copy api_paste.ini
|
||||||
|
cp $GYAN_DIR/etc/gyan/api-paste.ini $GYAN_API_PASTE
|
||||||
|
}
|
||||||
|
|
||||||
|
# create_gyan_cache_dir() - Part of the init_GYAN() process
|
||||||
|
function create_gyan_cache_dir {
|
||||||
|
# Create cache dir
|
||||||
|
sudo mkdir -p $GYAN_AUTH_CACHE_DIR
|
||||||
|
sudo chown $STACK_USER $GYAN_AUTH_CACHE_DIR
|
||||||
|
rm -f $GYAN_AUTH_CACHE_DIR/*
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# init_gyan() - Initialize databases, etc.
|
||||||
|
function init_gyan {
|
||||||
|
# Only do this step once on the API node for an entire cluster.
|
||||||
|
if is_service_enabled gyan-api; then
|
||||||
|
if is_service_enabled $DATABASE_BACKENDS; then
|
||||||
|
# (Re)create gyan database
|
||||||
|
recreate_database gyan
|
||||||
|
|
||||||
|
# Migrate gyan database
|
||||||
|
$GYAN_BIN_DIR/gyan-db-manage upgrade
|
||||||
|
fi
|
||||||
|
|
||||||
|
if is_service_enabled gyan-etcd; then
|
||||||
|
install_etcd_server
|
||||||
|
fi
|
||||||
|
create_gyan_cache_dir
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# install_gyanclient() - Collect source and prepare
|
||||||
|
function install_gyanclient {
|
||||||
|
if use_library_from_git "python-gyanclient"; then
|
||||||
|
git_clone_by_name "python-gyanclient"
|
||||||
|
setup_dev_lib "python-gyanclient"
|
||||||
|
sudo install -D -m 0644 -o $STACK_USER {${GITDIR["python-gyanclient"]}/tools/,/etc/bash_completion.d/}gyan.bash_completion
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# install_gyan() - Collect source and prepare
|
||||||
|
function install_gyan {
|
||||||
|
git_clone $GYAN_REPO $GYAN_DIR $GYAN_BRANCH
|
||||||
|
setup_develop $GYAN_DIR
|
||||||
|
}
|
||||||
|
|
||||||
|
# start_gyan_api() - Start the API process ahead of other things
|
||||||
|
function start_gyan_api {
|
||||||
|
# Get right service port for testing
|
||||||
|
local service_port=$GYAN_SERVICE_PORT
|
||||||
|
local service_protocol=$GYAN_SERVICE_PROTOCOL
|
||||||
|
if is_service_enabled tls-proxy; then
|
||||||
|
service_port=$GYAN_SERVICE_PORT_INT
|
||||||
|
service_protocol="http"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local gyan_url
|
||||||
|
if [ "$GYAN_USE_UWSGI" == "True" ]; then
|
||||||
|
run_process gyan-api "$GYAN_BIN_DIR/uwsgi --procname-prefix gyan-api --ini $GYAN_UWSGI_CONF"
|
||||||
|
gyan_url=$service_protocol://$GYAN_SERVICE_HOST/ml-infra
|
||||||
|
else
|
||||||
|
run_process gyan-api "$GYAN_BIN_DIR/gyan-api"
|
||||||
|
gyan_url=$service_protocol://$GYAN_SERVICE_HOST:$service_port
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Waiting for gyan-api to start..."
|
||||||
|
if ! wait_for_service $SERVICE_TIMEOUT $gyan_url; then
|
||||||
|
die $LINENO "gyan-api did not start"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start proxies if enabled
|
||||||
|
if is_service_enabled tls-proxy; then
|
||||||
|
start_tls_proxy '*' $GYAN_SERVICE_PORT $GYAN_SERVICE_HOST $GYAN_SERVICE_PORT_INT &
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# start_gyan_compute() - Start Gyan compute agent
|
||||||
|
function start_gyan_compute {
|
||||||
|
echo "Start gyan compute..."
|
||||||
|
run_process gyan-compute "$GYAN_BIN_DIR/gyan-compute"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# start_gyan() - Start running processes, including screen
|
||||||
|
function start_gyan {
|
||||||
|
|
||||||
|
# ``run_process`` checks ``is_service_enabled``, it is not needed here
|
||||||
|
start_gyan_api
|
||||||
|
start_gyan_compute
|
||||||
|
}
|
||||||
|
|
||||||
|
# stop_gyan() - Stop running processes (non-screen)
|
||||||
|
function stop_gyan {
|
||||||
|
|
||||||
|
if [ "$GYAN_USE_UWSGI" == "True" ]; then
|
||||||
|
disable_apache_site gyan
|
||||||
|
restart_apache_server
|
||||||
|
else
|
||||||
|
stop_process gyan-api
|
||||||
|
fi
|
||||||
|
stop_process gyan-compute
|
||||||
|
}
|
||||||
|
|
||||||
|
# Restore xtrace
|
||||||
|
$XTRACE
|
13
devstack/local.conf.sample
Normal file
13
devstack/local.conf.sample
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
[[local|localrc]]
|
||||||
|
HOST_IP=10.0.0.11 # change this to your IP address
|
||||||
|
DATABASE_PASSWORD=password
|
||||||
|
RABBIT_PASSWORD=password
|
||||||
|
SERVICE_TOKEN=password
|
||||||
|
SERVICE_PASSWORD=password
|
||||||
|
ADMIN_PASSWORD=password
|
||||||
|
enable_plugin gyan https://git.openstack.org/openstack/gyan
|
||||||
|
|
||||||
|
# install python-gyanclient from git
|
||||||
|
LIBS_FROM_GIT="python-gyanclient"
|
||||||
|
|
||||||
|
|
18
devstack/local.conf.subnode.sample
Normal file
18
devstack/local.conf.subnode.sample
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
[[local|localrc]]
|
||||||
|
HOST_IP=10.0.0.31 # change this to your IP address
|
||||||
|
DATABASE_PASSWORD=password
|
||||||
|
RABBIT_PASSWORD=password
|
||||||
|
SERVICE_TOKEN=password
|
||||||
|
SERVICE_PASSWORD=password
|
||||||
|
ADMIN_PASSWORD=password
|
||||||
|
enable_plugin gyan https://git.openstack.org/openstack/gyan
|
||||||
|
|
||||||
|
|
||||||
|
# Following is for multi host settings
|
||||||
|
MULTI_HOST=True
|
||||||
|
SERVICE_HOST=10.0.0.11 # change this to controller's IP address
|
||||||
|
DATABASE_TYPE=mysql
|
||||||
|
MYSQL_HOST=$SERVICE_HOST
|
||||||
|
RABBIT_HOST=$SERVICE_HOST
|
||||||
|
|
||||||
|
ENABLED_SERVICES=gyan-compute
|
47
devstack/plugin.sh
Executable file
47
devstack/plugin.sh
Executable file
@ -0,0 +1,47 @@
|
|||||||
|
# gyan - Devstack extras script to install gyan
|
||||||
|
|
||||||
|
# Save trace setting
|
||||||
|
XTRACE=$(set +o | grep xtrace)
|
||||||
|
set -o xtrace
|
||||||
|
|
||||||
|
echo_summary "gyan's plugin.sh was called..."
|
||||||
|
source $DEST/gyan/devstack/lib/gyan
|
||||||
|
(set -o posix; set)
|
||||||
|
|
||||||
|
if is_service_enabled gyan-api gyan-compute; then
|
||||||
|
if [[ "$1" == "stack" && "$2" == "install" ]]; then
|
||||||
|
echo_summary "Installing gyan"
|
||||||
|
install_gyan
|
||||||
|
|
||||||
|
install_gyanclient
|
||||||
|
cleanup_gyan
|
||||||
|
|
||||||
|
elif [[ "$1" == "stack" && "$2" == "post-config" ]]; then
|
||||||
|
echo_summary "Configuring gyan"
|
||||||
|
configure_gyan
|
||||||
|
|
||||||
|
if is_service_enabled key; then
|
||||||
|
create_gyan_accounts
|
||||||
|
fi
|
||||||
|
|
||||||
|
elif [[ "$1" == "stack" && "$2" == "extra" ]]; then
|
||||||
|
# Initialize gyan
|
||||||
|
init_gyan
|
||||||
|
|
||||||
|
# Start the gyan API and gyan compute
|
||||||
|
echo_summary "Starting gyan"
|
||||||
|
start_gyan
|
||||||
|
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$1" == "unstack" ]]; then
|
||||||
|
stop_gyan
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$1" == "clean" ]]; then
|
||||||
|
cleanup_gyan
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Restore xtrace
|
||||||
|
$XTRACE
|
24
devstack/settings
Normal file
24
devstack/settings
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Devstack settings
|
||||||
|
|
||||||
|
## Modify to your environment
|
||||||
|
# FLOATING_RANGE=192.168.1.224/27
|
||||||
|
# PUBLIC_NETWORK_GATEWAY=192.168.1.225
|
||||||
|
# PUBLIC_INTERFACE=em1
|
||||||
|
# FIXED_RANGE=10.0.0.0/24
|
||||||
|
## Log all output to files
|
||||||
|
# LOGFILE=$HOME/devstack.log
|
||||||
|
## Neutron settings
|
||||||
|
# Q_USE_SECGROUP=True
|
||||||
|
# ENABLE_TENANT_VLANS=True
|
||||||
|
# TENANT_VLAN_RANGE=
|
||||||
|
# PHYSICAL_NETWORK=public
|
||||||
|
# OVS_PHYSICAL_BRIDGE=br-ex
|
||||||
|
|
||||||
|
# Enable Gyan services
|
||||||
|
if [[ ${HOST_IP} == ${SERVICE_HOST} ]]; then
|
||||||
|
enable_service gyan-api
|
||||||
|
enable_service gyan-compute
|
||||||
|
else
|
||||||
|
enable_service gyan-compute
|
||||||
|
fi
|
||||||
|
|
41
etc/apache2/gyan.conf.template
Normal file
41
etc/apache2/gyan.conf.template
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
# This is an example Apache2 configuration file for using the
|
||||||
|
# gyan API through mod_wsgi.
|
||||||
|
|
||||||
|
# Note: If you are using a Debian-based system then the paths
|
||||||
|
# "/var/log/httpd" and "/var/run/httpd" will use "apache2" instead
|
||||||
|
# of "httpd".
|
||||||
|
#
|
||||||
|
# The number of processes and threads is an example only and should
|
||||||
|
# be adjusted according to local requirements.
|
||||||
|
|
||||||
|
Listen %PUBLICPORT%
|
||||||
|
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\" %D(us)" gyan_combined
|
||||||
|
|
||||||
|
<VirtualHost *:%PUBLICPORT%>
|
||||||
|
WSGIDaemonProcess gyan-api user=%USER% processes=5 threads=1 display-name=%{GROUP}
|
||||||
|
WSGIScriptAlias / %PUBLICWSGI%
|
||||||
|
WSGIProcessGroup gyan-api
|
||||||
|
ErrorLogFormat "%M"
|
||||||
|
ErrorLog /var/log/%APACHE_NAME%/gyan_api.log
|
||||||
|
LogLevel info
|
||||||
|
CustomLog /var/log/%APACHE_NAME%/gyan_access.log gyan_combined
|
||||||
|
|
||||||
|
<Directory /opt/stack/gyan/gyan/api>
|
||||||
|
WSGIProcessGroup gyan-api
|
||||||
|
WSGIApplicationGroup %{GLOBAL}
|
||||||
|
AllowOverride All
|
||||||
|
Require all granted
|
||||||
|
</Directory>
|
||||||
|
</VirtualHost>
|
19
etc/gyan/api-paste.ini
Normal file
19
etc/gyan/api-paste.ini
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
[pipeline:main]
|
||||||
|
pipeline = cors request_id osprofiler authtoken api_v1
|
||||||
|
|
||||||
|
[app:api_v1]
|
||||||
|
paste.app_factory = gyan.api.app:app_factory
|
||||||
|
|
||||||
|
[filter:authtoken]
|
||||||
|
acl_public_routes = /, /v1
|
||||||
|
paste.filter_factory = gyan.api.middleware.auth_token:AuthTokenMiddleware.factory
|
||||||
|
|
||||||
|
[filter:osprofiler]
|
||||||
|
paste.filter_factory = gyan.common.profiler:WsgiMiddleware.factory
|
||||||
|
|
||||||
|
[filter:request_id]
|
||||||
|
paste.filter_factory = oslo_middleware:RequestId.factory
|
||||||
|
|
||||||
|
[filter:cors]
|
||||||
|
paste.filter_factory = oslo_middleware.cors:filter_factory
|
||||||
|
oslo_config_project = gyan
|
14
etc/gyan/gyan-config-generator.conf
Normal file
14
etc/gyan/gyan-config-generator.conf
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
[DEFAULT]
|
||||||
|
output_file = etc/gyan/gyan.conf.sample
|
||||||
|
wrap_width = 79
|
||||||
|
|
||||||
|
namespace = gyan.conf
|
||||||
|
namespace = keystonemiddleware.auth_token
|
||||||
|
namespace = oslo.concurrency
|
||||||
|
namespace = oslo.db
|
||||||
|
namespace = oslo.log
|
||||||
|
namespace = oslo.messaging
|
||||||
|
namespace = oslo.middleware.cors
|
||||||
|
namespace = oslo.policy
|
||||||
|
namespace = oslo.service.periodic_task
|
||||||
|
namespace = oslo.service.service
|
3
etc/gyan/gyan-policy-generator.conf
Normal file
3
etc/gyan/gyan-policy-generator.conf
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[DEFAULT]
|
||||||
|
output_file = etc/gyan/policy.yaml.sample
|
||||||
|
namespace = gyan
|
27
etc/gyan/rootwrap.conf
Normal file
27
etc/gyan/rootwrap.conf
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# Configuration for gyan-rootwrap
|
||||||
|
# This file should be owned by (and only-writable by) the root user
|
||||||
|
|
||||||
|
[DEFAULT]
|
||||||
|
# List of directories to load filter definitions from (separated by ',').
|
||||||
|
# These directories MUST all be only writable by root !
|
||||||
|
filters_path=/etc/gyan/rootwrap.d
|
||||||
|
|
||||||
|
# List of directories to search executables in, in case filters do not
|
||||||
|
# explicitely specify a full path (separated by ',')
|
||||||
|
# If not specified, defaults to system PATH environment variable.
|
||||||
|
# These directories MUST all be only writable by root !
|
||||||
|
exec_dirs=/sbin,/usr/sbin,/bin,/usr/bin,/usr/local/bin,/usr/local/sbin
|
||||||
|
|
||||||
|
# Enable logging to syslog
|
||||||
|
# Default value is False
|
||||||
|
use_syslog=False
|
||||||
|
|
||||||
|
# Which syslog facility to use.
|
||||||
|
# Valid values include auth, authpriv, syslog, local0, local1...
|
||||||
|
# Default value is 'syslog'
|
||||||
|
syslog_log_facility=syslog
|
||||||
|
|
||||||
|
# Which messages to log.
|
||||||
|
# INFO means log all usage
|
||||||
|
# ERROR means only log unsuccessful attempts
|
||||||
|
syslog_log_level=ERROR
|
8
etc/gyan/rootwrap.d/gyan.filters
Normal file
8
etc/gyan/rootwrap.d/gyan.filters
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# gyan command filters
|
||||||
|
# This file should be owned by (and only-writeable by) the root user
|
||||||
|
|
||||||
|
[Filters]
|
||||||
|
# privileged/__init__.py: priv_context.PrivContext(default)
|
||||||
|
# This line ties the superuser privs with the config files, context name,
|
||||||
|
# and (implicitly) the actual python code invoked.
|
||||||
|
privsep-rootwrap: RegExpFilter, privsep-helper, root, privsep-helper, --config-file, /etc/(?!\.\.).*, --privsep_context, os_brick.privileged.default, --privsep_sock_path, /tmp/.*
|
1
gyan/MANIFEST.in
Normal file
1
gyan/MANIFEST.in
Normal file
@ -0,0 +1 @@
|
|||||||
|
recursive-include public *
|
0
gyan/api/__init__.py
Normal file
0
gyan/api/__init__.py
Normal file
67
gyan/api/app.py
Normal file
67
gyan/api/app.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
# 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 os
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log
|
||||||
|
from paste import deploy
|
||||||
|
import pecan
|
||||||
|
|
||||||
|
from gyan.api import config as api_config
|
||||||
|
from gyan.api import middleware
|
||||||
|
from gyan.common import config as common_config
|
||||||
|
import gyan.conf
|
||||||
|
|
||||||
|
CONF = gyan.conf.CONF
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def get_pecan_config():
|
||||||
|
# Set up the pecan configuration
|
||||||
|
filename = api_config.__file__.replace('.pyc', '.py')
|
||||||
|
return pecan.configuration.conf_from_file(filename)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_app(config=None):
|
||||||
|
if not config:
|
||||||
|
config = get_pecan_config()
|
||||||
|
|
||||||
|
app_conf = dict(config.app)
|
||||||
|
common_config.set_config_defaults()
|
||||||
|
|
||||||
|
app = pecan.make_app(
|
||||||
|
app_conf.pop('root'),
|
||||||
|
logging=getattr(config, 'logging', {}),
|
||||||
|
wrap_app=middleware.ParsableErrorMiddleware,
|
||||||
|
**app_conf
|
||||||
|
)
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
def load_app():
|
||||||
|
cfg_file = None
|
||||||
|
cfg_path = CONF.api.api_paste_config
|
||||||
|
if not os.path.isabs(cfg_path):
|
||||||
|
cfg_file = CONF.find_file(cfg_path)
|
||||||
|
elif os.path.exists(cfg_path):
|
||||||
|
cfg_file = cfg_path
|
||||||
|
|
||||||
|
if not cfg_file:
|
||||||
|
raise cfg.ConfigFilesNotFoundError([CONF.api.api_paste_config])
|
||||||
|
LOG.info("Full WSGI config used: %s", cfg_file)
|
||||||
|
return deploy.loadapp("config:" + cfg_file)
|
||||||
|
|
||||||
|
|
||||||
|
def app_factory(global_config, **local_conf):
|
||||||
|
return setup_app()
|
19
gyan/api/app.wsgi
Normal file
19
gyan/api/app.wsgi
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
"""Use this file for deploying the API under mod_wsgi.
|
||||||
|
See https://pecan.readthedocs.org/en/latest/deployment.html for details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from gyan.api import wsgi
|
||||||
|
|
||||||
|
application = wsgi.init_application()
|
25
gyan/api/config.py
Normal file
25
gyan/api/config.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# 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 gyan.api import hooks
|
||||||
|
|
||||||
|
# Pecan Application Configurations
|
||||||
|
app = {
|
||||||
|
'root': 'gyan.api.controllers.root.RootController',
|
||||||
|
'modules': ['gyan'],
|
||||||
|
'hooks': [
|
||||||
|
hooks.ContextHook(),
|
||||||
|
hooks.NoExceptionTracebackHook(),
|
||||||
|
hooks.RPCHook(),
|
||||||
|
],
|
||||||
|
'debug': True,
|
||||||
|
}
|
0
gyan/api/controllers/__init__.py
Normal file
0
gyan/api/controllers/__init__.py
Normal file
233
gyan/api/controllers/base.py
Normal file
233
gyan/api/controllers/base.py
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
# 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 operator
|
||||||
|
import six
|
||||||
|
|
||||||
|
import pecan
|
||||||
|
from pecan import rest
|
||||||
|
from webob import exc
|
||||||
|
from gyan.api.controllers import versions
|
||||||
|
from gyan.api import versioned_method
|
||||||
|
from gyan.common import exception
|
||||||
|
from gyan.common.i18n import _
|
||||||
|
|
||||||
|
|
||||||
|
# name of attribute to keep version method information
|
||||||
|
VER_METHOD_ATTR = 'versioned_methods'
|
||||||
|
|
||||||
|
|
||||||
|
class APIBase(object):
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
for field in self.fields:
|
||||||
|
if field in kwargs:
|
||||||
|
value = kwargs[field]
|
||||||
|
setattr(self, field, value)
|
||||||
|
|
||||||
|
def __setattr__(self, field, value):
|
||||||
|
super(APIBase, self).__setattr__(field, value)
|
||||||
|
|
||||||
|
def as_dict(self):
|
||||||
|
"""Render this object as a dict of its fields."""
|
||||||
|
return {f: getattr(self, f)
|
||||||
|
for f in self.fields
|
||||||
|
if hasattr(self, f)}
|
||||||
|
|
||||||
|
def __json__(self):
|
||||||
|
return self.as_dict()
|
||||||
|
|
||||||
|
def unset_fields_except(self, except_list=None):
|
||||||
|
"""Unset fields so they don't appear in the message body.
|
||||||
|
|
||||||
|
:param except_list: A list of fields that won't be touched.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if except_list is None:
|
||||||
|
except_list = []
|
||||||
|
|
||||||
|
for k in self.as_dict():
|
||||||
|
if k not in except_list:
|
||||||
|
setattr(self, k, None)
|
||||||
|
|
||||||
|
|
||||||
|
class ControllerMetaclass(type):
|
||||||
|
"""Controller metaclass.
|
||||||
|
|
||||||
|
This metaclass automates the task of assembling a dictionary
|
||||||
|
mapping action keys to method names.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __new__(mcs, name, bases, cls_dict):
|
||||||
|
"""Adds version function dictionary to the class."""
|
||||||
|
|
||||||
|
versioned_methods = None
|
||||||
|
|
||||||
|
for base in bases:
|
||||||
|
if base.__name__ == "Controller":
|
||||||
|
# NOTE(cyeoh): This resets the VER_METHOD_ATTR attribute
|
||||||
|
# between API controller class creations. This allows us
|
||||||
|
# to use a class decorator on the API methods that doesn't
|
||||||
|
# require naming explicitly what method is being versioned as
|
||||||
|
# it can be implicit based on the method decorated. It is a bit
|
||||||
|
# ugly.
|
||||||
|
if VER_METHOD_ATTR in base.__dict__:
|
||||||
|
versioned_methods = getattr(base, VER_METHOD_ATTR)
|
||||||
|
delattr(base, VER_METHOD_ATTR)
|
||||||
|
|
||||||
|
if versioned_methods:
|
||||||
|
cls_dict[VER_METHOD_ATTR] = versioned_methods
|
||||||
|
|
||||||
|
return super(ControllerMetaclass, mcs).__new__(mcs, name, bases,
|
||||||
|
cls_dict)
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(ControllerMetaclass)
|
||||||
|
class Controller(rest.RestController):
|
||||||
|
"""Base Rest Controller"""
|
||||||
|
|
||||||
|
@pecan.expose('json')
|
||||||
|
def _no_version_match(self, *args, **kwargs):
|
||||||
|
from pecan import request
|
||||||
|
|
||||||
|
raise exc.HTTPNotAcceptable(_(
|
||||||
|
"Version %(ver)s was requested but the requested API is not "
|
||||||
|
"supported for this version.") % {'ver': request.version})
|
||||||
|
|
||||||
|
def __getattribute__(self, key):
|
||||||
|
|
||||||
|
def version_select():
|
||||||
|
"""Select the correct method based on version
|
||||||
|
|
||||||
|
@return: Returns the correct versioned method
|
||||||
|
@raises: HTTPNotAcceptable if there is no method which
|
||||||
|
matches the name and version constraints
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pecan import request
|
||||||
|
ver = request.version
|
||||||
|
|
||||||
|
func_list = self.versioned_methods[key]
|
||||||
|
for func in func_list:
|
||||||
|
if ver.matches(func.start_version, func.end_version):
|
||||||
|
return func.func
|
||||||
|
|
||||||
|
return self._no_version_match
|
||||||
|
|
||||||
|
try:
|
||||||
|
version_meth_dict = object.__getattribute__(self, VER_METHOD_ATTR)
|
||||||
|
except AttributeError:
|
||||||
|
# No versioning on this class
|
||||||
|
return object.__getattribute__(self, key)
|
||||||
|
if version_meth_dict and key in version_meth_dict:
|
||||||
|
return version_select().__get__(self, self.__class__)
|
||||||
|
|
||||||
|
return object.__getattribute__(self, key)
|
||||||
|
|
||||||
|
# NOTE: This decorator MUST appear first (the outermost
|
||||||
|
# decorator) on an API method for it to work correctly
|
||||||
|
@classmethod
|
||||||
|
def api_version(cls, min_ver, max_ver=None):
|
||||||
|
"""Decorator for versioning api methods.
|
||||||
|
|
||||||
|
Add the decorator to any pecan method that has been exposed.
|
||||||
|
This decorator will store the method, min version, and max
|
||||||
|
version in a list for each api. It will check that there is no
|
||||||
|
overlap between versions and methods. When the api is called the
|
||||||
|
controller will use the list for each api to determine which
|
||||||
|
method to call.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
@base.Controller.api_version("1.1", "1.2")
|
||||||
|
def get_one(self, ml_model_id):
|
||||||
|
{...code for versions 1.1 to 1.2...}
|
||||||
|
|
||||||
|
@base.Controller.api_version("1.3")
|
||||||
|
def get_one(self, ml_model_id):
|
||||||
|
{...code for versions 1.3 to latest}
|
||||||
|
|
||||||
|
@min_ver: string representing minimum version
|
||||||
|
@max_ver: optional string representing maximum version
|
||||||
|
@raises: ApiVersionsIntersect if an version overlap is found between
|
||||||
|
method versions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(f):
|
||||||
|
obj_min_ver = versions.Version('', '', '', min_ver)
|
||||||
|
if max_ver:
|
||||||
|
obj_max_ver = versions.Version('', '', '', max_ver)
|
||||||
|
else:
|
||||||
|
obj_max_ver = versions.Version('', '', '',
|
||||||
|
versions.CURRENT_MAX_VER)
|
||||||
|
|
||||||
|
# Add to list of versioned methods registered
|
||||||
|
func_name = f.__name__
|
||||||
|
new_func = versioned_method.VersionedMethod(
|
||||||
|
func_name, obj_min_ver, obj_max_ver, f)
|
||||||
|
|
||||||
|
func_dict = getattr(cls, VER_METHOD_ATTR, {})
|
||||||
|
if not func_dict:
|
||||||
|
setattr(cls, VER_METHOD_ATTR, func_dict)
|
||||||
|
|
||||||
|
func_list = func_dict.get(func_name, [])
|
||||||
|
if not func_list:
|
||||||
|
func_dict[func_name] = func_list
|
||||||
|
func_list.append(new_func)
|
||||||
|
|
||||||
|
is_intersect = Controller.check_for_versions_intersection(
|
||||||
|
func_list)
|
||||||
|
|
||||||
|
if is_intersect:
|
||||||
|
raise exception.ApiVersionsIntersect(
|
||||||
|
name=new_func.name,
|
||||||
|
min_ver=new_func.start_version,
|
||||||
|
max_ver=new_func.end_version
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure the list is sorted by minimum version (reversed)
|
||||||
|
# so later when we work through the list in order we find
|
||||||
|
# the method which has the latest version which supports
|
||||||
|
# the version requested.
|
||||||
|
func_list.sort(key=lambda f: f.start_version, reverse=True)
|
||||||
|
|
||||||
|
return f
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def check_for_versions_intersection(func_list):
|
||||||
|
"""Determines whether function list intersections
|
||||||
|
|
||||||
|
General algorithm:
|
||||||
|
https://en.wikipedia.org/wiki/Intersection_algorithm
|
||||||
|
|
||||||
|
:param func_list: list of VersionedMethod objects
|
||||||
|
:return: boolean
|
||||||
|
"""
|
||||||
|
|
||||||
|
pairs = []
|
||||||
|
counter = 0
|
||||||
|
|
||||||
|
for f in func_list:
|
||||||
|
pairs.append((f.start_version, 1))
|
||||||
|
pairs.append((f.end_version, -1))
|
||||||
|
|
||||||
|
pairs.sort(key=operator.itemgetter(1), reverse=True)
|
||||||
|
pairs.sort(key=operator.itemgetter(0))
|
||||||
|
|
||||||
|
for p in pairs:
|
||||||
|
counter += p[1]
|
||||||
|
|
||||||
|
if counter > 1:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
35
gyan/api/controllers/link.py
Normal file
35
gyan/api/controllers/link.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# 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 pecan
|
||||||
|
|
||||||
|
|
||||||
|
def build_url(resource, resource_args, bookmark=False, base_url=None):
|
||||||
|
if base_url is None:
|
||||||
|
base_url = pecan.request.host_url
|
||||||
|
|
||||||
|
template = '%(url)s/%(res)s' if bookmark else '%(url)s/v1/%(res)s'
|
||||||
|
# FIXME(tbh): I'm getting a 404 when doing a GET on
|
||||||
|
# a nested resource that the URL ends with a '/'.
|
||||||
|
# https://groups.google.com/forum/#!topic/pecan-dev/QfSeviLg5qs
|
||||||
|
template += '%(args)s' if resource_args.startswith('?') else '/%(args)s'
|
||||||
|
return template % {'url': base_url, 'res': resource, 'args': resource_args}
|
||||||
|
|
||||||
|
|
||||||
|
def make_link(rel_name, url, resource, resource_args,
|
||||||
|
bookmark=False, type=None):
|
||||||
|
href = build_url(resource, resource_args,
|
||||||
|
bookmark=bookmark, base_url=url)
|
||||||
|
if type is None:
|
||||||
|
return {'href': href, 'rel': rel_name}
|
||||||
|
else:
|
||||||
|
return {'href': href, 'rel': rel_name, 'type': type}
|
97
gyan/api/controllers/root.py
Normal file
97
gyan/api/controllers/root.py
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
# 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 pecan
|
||||||
|
from pecan import rest
|
||||||
|
|
||||||
|
from gyan.api.controllers import base
|
||||||
|
from gyan.api.controllers import link
|
||||||
|
from gyan.api.controllers import v1
|
||||||
|
from gyan.api.controllers import versions
|
||||||
|
|
||||||
|
|
||||||
|
class Version(base.APIBase):
|
||||||
|
"""An API version representation."""
|
||||||
|
|
||||||
|
fields = (
|
||||||
|
'id',
|
||||||
|
'links',
|
||||||
|
'status',
|
||||||
|
'max_version',
|
||||||
|
'min_version'
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def convert(id, status, max, min):
|
||||||
|
version = Version()
|
||||||
|
version.id = id
|
||||||
|
version.links = [link.make_link('self', pecan.request.host_url,
|
||||||
|
id, '', bookmark=True)]
|
||||||
|
version.status = status
|
||||||
|
version.max_version = max
|
||||||
|
version.min_version = min
|
||||||
|
return version
|
||||||
|
|
||||||
|
|
||||||
|
class Root(base.APIBase):
|
||||||
|
|
||||||
|
fields = (
|
||||||
|
'name',
|
||||||
|
'description',
|
||||||
|
'versions',
|
||||||
|
'default_version',
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def convert():
|
||||||
|
root = Root()
|
||||||
|
root.name = "OpenStack Gyan API"
|
||||||
|
root.description = ("Gyan is an OpenStack project which aims to "
|
||||||
|
"provide ML infra service.")
|
||||||
|
|
||||||
|
root.versions = [Version.convert('v1', "CURRENT",
|
||||||
|
versions.CURRENT_MAX_VER,
|
||||||
|
versions.BASE_VER)]
|
||||||
|
root.default_version = Version.convert('v1', "CURRENT",
|
||||||
|
versions.CURRENT_MAX_VER,
|
||||||
|
versions.BASE_VER)
|
||||||
|
return root
|
||||||
|
|
||||||
|
|
||||||
|
class RootController(rest.RestController):
|
||||||
|
|
||||||
|
_versions = ['v1']
|
||||||
|
"""All supported API versions"""
|
||||||
|
|
||||||
|
_default_version = 'v1'
|
||||||
|
"""The default API version"""
|
||||||
|
|
||||||
|
v1 = v1.Controller()
|
||||||
|
|
||||||
|
@pecan.expose('json')
|
||||||
|
def get(self):
|
||||||
|
# NOTE: The reason why convert() it's being called for every
|
||||||
|
# request is because we need to get the host url from
|
||||||
|
# the request object to make the links.
|
||||||
|
return Root.convert()
|
||||||
|
|
||||||
|
@pecan.expose()
|
||||||
|
def _route(self, args):
|
||||||
|
"""Overrides the default routing behavior.
|
||||||
|
|
||||||
|
It redirects the request to the default version of the gyan API
|
||||||
|
if the version number is not specified in the url.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if args[0] and args[0] not in self._versions:
|
||||||
|
args = [self._default_version] + args
|
||||||
|
return super(RootController, self)._route(args)
|
155
gyan/api/controllers/v1/__init__.py
Normal file
155
gyan/api/controllers/v1/__init__.py
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Version 1 of the Gyan API
|
||||||
|
|
||||||
|
NOTE: IN PROGRESS AND NOT FULLY IMPLEMENTED.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from oslo_log import log as logging
|
||||||
|
import pecan
|
||||||
|
|
||||||
|
from gyan.api.controllers import base as controllers_base
|
||||||
|
from gyan.api.controllers import link
|
||||||
|
from gyan.api.controllers.v1 import hosts as host_controller
|
||||||
|
from gyan.api.controllers.v1 import ml_models as ml_model_controller
|
||||||
|
from gyan.api.controllers import versions as ver
|
||||||
|
from gyan.api import http_error
|
||||||
|
from gyan.common.i18n import _
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
BASE_VERSION = 1
|
||||||
|
|
||||||
|
MIN_VER_STR = '%s %s' % (ver.Version.service_string, ver.BASE_VER)
|
||||||
|
|
||||||
|
MAX_VER_STR = '%s %s' % (ver.Version.service_string, ver.CURRENT_MAX_VER)
|
||||||
|
|
||||||
|
MIN_VER = ver.Version({ver.Version.string: MIN_VER_STR},
|
||||||
|
MIN_VER_STR, MAX_VER_STR)
|
||||||
|
MAX_VER = ver.Version({ver.Version.string: MAX_VER_STR},
|
||||||
|
MIN_VER_STR, MAX_VER_STR)
|
||||||
|
|
||||||
|
|
||||||
|
class MediaType(controllers_base.APIBase):
|
||||||
|
"""A media type representation."""
|
||||||
|
|
||||||
|
fields = (
|
||||||
|
'base',
|
||||||
|
'type',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class V1(controllers_base.APIBase):
|
||||||
|
"""The representation of the version 1 of the API."""
|
||||||
|
|
||||||
|
fields = (
|
||||||
|
'id',
|
||||||
|
'media_types',
|
||||||
|
'links',
|
||||||
|
'hosts',
|
||||||
|
'ml_models'
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def convert():
|
||||||
|
v1 = V1()
|
||||||
|
v1.id = "v1"
|
||||||
|
v1.links = [link.make_link('self', pecan.request.host_url,
|
||||||
|
'v1', '', bookmark=True),
|
||||||
|
link.make_link('describedby',
|
||||||
|
'https://docs.openstack.org',
|
||||||
|
'developer/gyan/dev',
|
||||||
|
'api-spec-v1.html',
|
||||||
|
bookmark=True, type='text/html')]
|
||||||
|
v1.media_types = [MediaType(base='application/json',
|
||||||
|
type='application/vnd.openstack.gyan.v1+json')]
|
||||||
|
v1.hosts = [link.make_link('self', pecan.request.host_url,
|
||||||
|
'hosts', ''),
|
||||||
|
link.make_link('bookmark',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'hosts', '',
|
||||||
|
bookmark=True)]
|
||||||
|
v1.ml_models = [link.make_link('self', pecan.request.host_url,
|
||||||
|
'ml_models', ''),
|
||||||
|
link.make_link('bookmark',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'ml_models', '',
|
||||||
|
bookmark=True)]
|
||||||
|
return v1
|
||||||
|
|
||||||
|
|
||||||
|
class Controller(controllers_base.Controller):
|
||||||
|
"""Version 1 API controller root."""
|
||||||
|
|
||||||
|
hosts = host_controller.HostController()
|
||||||
|
ml_models = ml_model_controller.MLModelController()
|
||||||
|
|
||||||
|
@pecan.expose('json')
|
||||||
|
def get(self):
|
||||||
|
return V1.convert()
|
||||||
|
|
||||||
|
def _check_version(self, version, headers=None):
|
||||||
|
if headers is None:
|
||||||
|
headers = {}
|
||||||
|
# ensure that major version in the URL matches the header
|
||||||
|
if version.major != BASE_VERSION:
|
||||||
|
raise http_error.HTTPNotAcceptableAPIVersion(_(
|
||||||
|
"Mutually exclusive versions requested. Version %(ver)s "
|
||||||
|
"requested but not supported by this service. "
|
||||||
|
"The supported version range is: "
|
||||||
|
"[%(min)s, %(max)s].") % {'ver': version,
|
||||||
|
'min': MIN_VER_STR,
|
||||||
|
'max': MAX_VER_STR},
|
||||||
|
headers=headers,
|
||||||
|
max_version=str(MAX_VER),
|
||||||
|
min_version=str(MIN_VER))
|
||||||
|
# ensure the minor version is within the supported range
|
||||||
|
if version < MIN_VER or version > MAX_VER:
|
||||||
|
raise http_error.HTTPNotAcceptableAPIVersion(_(
|
||||||
|
"Version %(ver)s was requested but the minor version is not "
|
||||||
|
"supported by this service. The supported version range is: "
|
||||||
|
"[%(min)s, %(max)s].") % {'ver': version, 'min': MIN_VER_STR,
|
||||||
|
'max': MAX_VER_STR},
|
||||||
|
headers=headers,
|
||||||
|
max_version=str(MAX_VER),
|
||||||
|
min_version=str(MIN_VER))
|
||||||
|
|
||||||
|
@pecan.expose()
|
||||||
|
def _route(self, args):
|
||||||
|
version = ver.Version(
|
||||||
|
pecan.request.headers, MIN_VER_STR, MAX_VER_STR)
|
||||||
|
|
||||||
|
# Always set the basic version headers
|
||||||
|
pecan.response.headers[ver.Version.min_string] = MIN_VER_STR
|
||||||
|
pecan.response.headers[ver.Version.max_string] = MAX_VER_STR
|
||||||
|
pecan.response.headers[ver.Version.string] = " ".join(
|
||||||
|
[ver.Version.service_string, str(version)])
|
||||||
|
pecan.response.headers["vary"] = ver.Version.string
|
||||||
|
|
||||||
|
# assert that requested version is supported
|
||||||
|
self._check_version(version, pecan.response.headers)
|
||||||
|
pecan.request.version = version
|
||||||
|
if pecan.request.body:
|
||||||
|
msg = ("Processing request: url: %(url)s, %(method)s, "
|
||||||
|
"body: %(body)s" %
|
||||||
|
{'url': pecan.request.url,
|
||||||
|
'method': pecan.request.method,
|
||||||
|
'body': pecan.request.body})
|
||||||
|
LOG.debug(msg)
|
||||||
|
|
||||||
|
return super(Controller, self)._route(args)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ('Controller',)
|
41
gyan/api/controllers/v1/collection.py
Normal file
41
gyan/api/controllers/v1/collection.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# 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 pecan
|
||||||
|
|
||||||
|
from gyan.api.controllers import base
|
||||||
|
from gyan.api.controllers import link
|
||||||
|
|
||||||
|
|
||||||
|
class Collection(base.APIBase):
|
||||||
|
|
||||||
|
@property
|
||||||
|
def collection(self):
|
||||||
|
return getattr(self, self._type)
|
||||||
|
|
||||||
|
def has_next(self, limit):
|
||||||
|
"""Return whether collection has more items."""
|
||||||
|
return len(self.collection) and len(self.collection) == limit
|
||||||
|
|
||||||
|
def get_next(self, limit, url=None, **kwargs):
|
||||||
|
"""Return a link to the next subset of the collection."""
|
||||||
|
if not self.has_next(limit):
|
||||||
|
return None
|
||||||
|
|
||||||
|
resource_url = url or self._type
|
||||||
|
q_args = ''.join(['%s=%s&' % (key, kwargs[key]) for key in kwargs])
|
||||||
|
next_args = '?%(args)slimit=%(limit)d&marker=%(marker)s' % {
|
||||||
|
'args': q_args, 'limit': limit,
|
||||||
|
'marker': self.collection[-1]['uuid']}
|
||||||
|
|
||||||
|
return link.make_link('next', pecan.request.host_url,
|
||||||
|
resource_url, next_args)['href']
|
109
gyan/api/controllers/v1/hosts.py
Normal file
109
gyan/api/controllers/v1/hosts.py
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
# 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 pecan
|
||||||
|
|
||||||
|
from gyan.api.controllers import base
|
||||||
|
from gyan.api.controllers.v1 import collection
|
||||||
|
from gyan.api.controllers.v1.views import hosts_view as view
|
||||||
|
from gyan.api import utils as api_utils
|
||||||
|
from gyan.common import exception
|
||||||
|
from gyan.common import policy
|
||||||
|
from gyan import objects
|
||||||
|
|
||||||
|
|
||||||
|
def _get_host(host_ident):
|
||||||
|
host = api_utils.get_resource('ComputeHost', host_ident)
|
||||||
|
if not host:
|
||||||
|
pecan.abort(404, ('Not found; the host you requested '
|
||||||
|
'does not exist.'))
|
||||||
|
|
||||||
|
return host
|
||||||
|
|
||||||
|
|
||||||
|
def check_policy_on_host(host, action):
|
||||||
|
context = pecan.request.context
|
||||||
|
policy.enforce(context, action, host, action=action)
|
||||||
|
|
||||||
|
|
||||||
|
class HostCollection(collection.Collection):
|
||||||
|
"""API representation of a collection of hosts."""
|
||||||
|
|
||||||
|
fields = {
|
||||||
|
'hosts',
|
||||||
|
'next'
|
||||||
|
}
|
||||||
|
|
||||||
|
"""A list containing compute host objects"""
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super(HostCollection, self).__init__(**kwargs)
|
||||||
|
self._type = 'hosts'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def convert_with_links(hosts, limit, url=None,
|
||||||
|
expand=False, **kwargs):
|
||||||
|
collection = HostCollection()
|
||||||
|
collection.hosts = [view.format_host(url, p) for p in hosts]
|
||||||
|
collection.next = collection.get_next(limit, url=url, **kwargs)
|
||||||
|
return collection
|
||||||
|
|
||||||
|
|
||||||
|
class HostController(base.Controller):
|
||||||
|
"""Host info controller"""
|
||||||
|
|
||||||
|
@pecan.expose('json')
|
||||||
|
@base.Controller.api_version("1.0")
|
||||||
|
@exception.wrap_pecan_controller_exception
|
||||||
|
def get_all(self, **kwargs):
|
||||||
|
"""Retrieve a list of hosts"""
|
||||||
|
context = pecan.request.context
|
||||||
|
policy.enforce(context, "host:get_all",
|
||||||
|
action="host:get_all")
|
||||||
|
return self._get_host_collection(**kwargs)
|
||||||
|
|
||||||
|
def _get_host_collection(self, **kwargs):
|
||||||
|
context = pecan.request.context
|
||||||
|
limit = api_utils.validate_limit(kwargs.get('limit'))
|
||||||
|
sort_dir = api_utils.validate_sort_dir(kwargs.get('sort_dir', 'asc'))
|
||||||
|
sort_key = kwargs.get('sort_key', 'hostname')
|
||||||
|
expand = kwargs.get('expand')
|
||||||
|
filters = None
|
||||||
|
marker_obj = None
|
||||||
|
resource_url = kwargs.get('resource_url')
|
||||||
|
marker = kwargs.get('marker')
|
||||||
|
if marker:
|
||||||
|
marker_obj = objects.ComputeHost.get_by_uuid(context, marker)
|
||||||
|
hosts = objects.ComputeHost.list(context,
|
||||||
|
limit,
|
||||||
|
marker_obj,
|
||||||
|
sort_key,
|
||||||
|
sort_dir,
|
||||||
|
filters=filters)
|
||||||
|
return HostCollection.convert_with_links(hosts, limit,
|
||||||
|
url=resource_url,
|
||||||
|
expand=expand,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
|
||||||
|
@pecan.expose('json')
|
||||||
|
@base.Controller.api_version("1.0")
|
||||||
|
@exception.wrap_pecan_controller_exception
|
||||||
|
def get_one(self, host_ident):
|
||||||
|
"""Retrieve information about the given host.
|
||||||
|
|
||||||
|
:param host_ident: UUID or name of a host.
|
||||||
|
"""
|
||||||
|
context = pecan.request.context
|
||||||
|
policy.enforce(context, "host:get", action="host:get")
|
||||||
|
host = _get_host(host_ident)
|
||||||
|
return view.format_host(pecan.request.host_url, host)
|
289
gyan/api/controllers/v1/ml_models.py
Normal file
289
gyan/api/controllers/v1/ml_models.py
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
# 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 shlex
|
||||||
|
|
||||||
|
from oslo_log import log as logging
|
||||||
|
from oslo_utils import strutils
|
||||||
|
from oslo_utils import uuidutils
|
||||||
|
import pecan
|
||||||
|
import six
|
||||||
|
|
||||||
|
from gyan.api.controllers import base
|
||||||
|
from gyan.api.controllers import link
|
||||||
|
from gyan.api.controllers.v1 import collection
|
||||||
|
from gyan.api.controllers.v1.schemas import ml_models as schema
|
||||||
|
from gyan.api.controllers.v1.views import ml_models_view as view
|
||||||
|
from gyan.api import utils as api_utils
|
||||||
|
from gyan.api import validation
|
||||||
|
from gyan.common import consts
|
||||||
|
from gyan.common import context as gyan_context
|
||||||
|
from gyan.common import exception
|
||||||
|
from gyan.common.i18n import _
|
||||||
|
from gyan.common.policies import ml_model as policies
|
||||||
|
from gyan.common import policy
|
||||||
|
from gyan.common import utils
|
||||||
|
import gyan.conf
|
||||||
|
from gyan import objects
|
||||||
|
|
||||||
|
CONF = gyan.conf.CONF
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def check_policy_on_ml_model(ml_model, action):
|
||||||
|
context = pecan.request.context
|
||||||
|
policy.enforce(context, action, ml_model, action=action)
|
||||||
|
|
||||||
|
|
||||||
|
class MLModelCollection(collection.Collection):
|
||||||
|
"""API representation of a collection of ml models."""
|
||||||
|
|
||||||
|
fields = {
|
||||||
|
'ml_models',
|
||||||
|
'next'
|
||||||
|
}
|
||||||
|
|
||||||
|
"""A list containing ml models objects"""
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super(MLModelCollection, self).__init__(**kwargs)
|
||||||
|
self._type = 'ml_models'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def convert_with_links(rpc_ml_models, limit, url=None,
|
||||||
|
expand=False, **kwargs):
|
||||||
|
context = pecan.request.context
|
||||||
|
collection = MLModelCollection()
|
||||||
|
collection.ml_models = \
|
||||||
|
[view.format_ml_model(context, url, p.as_dict())
|
||||||
|
for p in rpc_ml_models]
|
||||||
|
collection.next = collection.get_next(limit, url=url, **kwargs)
|
||||||
|
return collection
|
||||||
|
|
||||||
|
|
||||||
|
class MLModelController(base.Controller):
|
||||||
|
"""Controller for MLModels."""
|
||||||
|
|
||||||
|
_custom_actions = {
|
||||||
|
'train': ['POST'],
|
||||||
|
'deploy': ['GET'],
|
||||||
|
'undeploy': ['GET']
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pecan.expose('json')
|
||||||
|
@exception.wrap_pecan_controller_exception
|
||||||
|
def get_all(self, **kwargs):
|
||||||
|
"""Retrieve a list of ml models.
|
||||||
|
|
||||||
|
"""
|
||||||
|
context = pecan.request.context
|
||||||
|
policy.enforce(context, "ml_model:get_all",
|
||||||
|
action="ml_model:get_all")
|
||||||
|
return self._get_ml_models_collection(**kwargs)
|
||||||
|
|
||||||
|
def _get_ml_models_collection(self, **kwargs):
|
||||||
|
context = pecan.request.context
|
||||||
|
if utils.is_all_projects(kwargs):
|
||||||
|
policy.enforce(context, "ml_model:get_all_all_projects",
|
||||||
|
action="ml_model:get_all_all_projects")
|
||||||
|
context.all_projects = True
|
||||||
|
kwargs.pop('all_projects', None)
|
||||||
|
limit = api_utils.validate_limit(kwargs.pop('limit', None))
|
||||||
|
sort_dir = api_utils.validate_sort_dir(kwargs.pop('sort_dir', 'asc'))
|
||||||
|
sort_key = kwargs.pop('sort_key', 'id')
|
||||||
|
resource_url = kwargs.pop('resource_url', None)
|
||||||
|
expand = kwargs.pop('expand', None)
|
||||||
|
|
||||||
|
ml_model_allowed_filters = ['name', 'status', 'project_id', 'user_id',
|
||||||
|
'type']
|
||||||
|
filters = {}
|
||||||
|
for filter_key in ml_model_allowed_filters:
|
||||||
|
if filter_key in kwargs:
|
||||||
|
policy_action = policies.MLMODEL % ('get_one:' + filter_key)
|
||||||
|
context.can(policy_action, might_not_exist=True)
|
||||||
|
filter_value = kwargs.pop(filter_key)
|
||||||
|
filters[filter_key] = filter_value
|
||||||
|
marker_obj = None
|
||||||
|
marker = kwargs.pop('marker', None)
|
||||||
|
if marker:
|
||||||
|
marker_obj = objects.ML_Model.get_by_uuid(context,
|
||||||
|
marker)
|
||||||
|
if kwargs:
|
||||||
|
unknown_params = [str(k) for k in kwargs]
|
||||||
|
msg = _("Unknown parameters: %s") % ", ".join(unknown_params)
|
||||||
|
raise exception.InvalidValue(msg)
|
||||||
|
|
||||||
|
ml_models = objects.ML_Model.list(context,
|
||||||
|
limit,
|
||||||
|
marker_obj,
|
||||||
|
sort_key,
|
||||||
|
sort_dir,
|
||||||
|
filters=filters)
|
||||||
|
return MLModelCollection.convert_with_links(ml_models, limit,
|
||||||
|
url=resource_url,
|
||||||
|
expand=expand,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
|
||||||
|
@pecan.expose('json')
|
||||||
|
@exception.wrap_pecan_controller_exception
|
||||||
|
def get_one(self, ml_model_ident, **kwargs):
|
||||||
|
"""Retrieve information about the given ml_model.
|
||||||
|
|
||||||
|
:param ml_model_ident: UUID or name of a ml_model.
|
||||||
|
"""
|
||||||
|
context = pecan.request.context
|
||||||
|
if utils.is_all_projects(kwargs):
|
||||||
|
policy.enforce(context, "ml_model:get_one_all_projects",
|
||||||
|
action="ml_model:get_one_all_projects")
|
||||||
|
context.all_projects = True
|
||||||
|
ml_model = utils.get_ml_model(ml_model_ident)
|
||||||
|
check_policy_on_ml_model(ml_model.as_dict(), "ml_model:get_one")
|
||||||
|
if ml_model.node:
|
||||||
|
compute_api = pecan.request.compute_api
|
||||||
|
try:
|
||||||
|
ml_model = compute_api.ml_model_show(context, ml_model)
|
||||||
|
except exception.MLModelHostNotUp:
|
||||||
|
raise exception.ServerNotUsable
|
||||||
|
|
||||||
|
return view.format_ml_model(context, pecan.request.host_url,
|
||||||
|
ml_model.as_dict())
|
||||||
|
|
||||||
|
@base.Controller.api_version("1.0")
|
||||||
|
@pecan.expose('json')
|
||||||
|
@api_utils.enforce_content_types(['application/json'])
|
||||||
|
@exception.wrap_pecan_controller_exception
|
||||||
|
@validation.validate_query_param(pecan.request, schema.query_param_create)
|
||||||
|
@validation.validated(schema.ml_model_create)
|
||||||
|
def post(self, **ml_model_dict):
|
||||||
|
return self._do_post(**ml_model_dict)
|
||||||
|
|
||||||
|
|
||||||
|
def _do_post(self, **ml_model_dict):
|
||||||
|
"""Create or run a new ml model.
|
||||||
|
|
||||||
|
:param ml_model_dict: a ml_model within the request body.
|
||||||
|
"""
|
||||||
|
context = pecan.request.context
|
||||||
|
compute_api = pecan.request.compute_api
|
||||||
|
policy.enforce(context, "ml_model:create",
|
||||||
|
action="ml_model:create")
|
||||||
|
|
||||||
|
ml_model_dict['project_id'] = context.project_id
|
||||||
|
ml_model_dict['user_id'] = context.user_id
|
||||||
|
name = ml_model_dict.get('name')
|
||||||
|
ml_model_dict['name'] = name
|
||||||
|
|
||||||
|
ml_model_dict['status'] = consts.CREATING
|
||||||
|
extra_spec = {}
|
||||||
|
extra_spec['hints'] = ml_model_dict.get('hints', None)
|
||||||
|
new_ml_model = objects.ML_Model(context, **ml_model_dict)
|
||||||
|
new_ml_model.create(context)
|
||||||
|
|
||||||
|
compute_api.ml_model_create(context, new_ml_model, **kwargs)
|
||||||
|
# Set the HTTP Location Header
|
||||||
|
pecan.response.location = link.build_url('ml_models',
|
||||||
|
new_ml_model.uuid)
|
||||||
|
pecan.response.status = 202
|
||||||
|
return view.format_ml_model(context, pecan.request.node_url,
|
||||||
|
new_ml_model.as_dict())
|
||||||
|
|
||||||
|
|
||||||
|
@pecan.expose('json')
|
||||||
|
@exception.wrap_pecan_controller_exception
|
||||||
|
@validation.validated(schema.ml_model_update)
|
||||||
|
def patch(self, ml_model_ident, **patch):
|
||||||
|
"""Update an existing ml model.
|
||||||
|
|
||||||
|
:param ml_model_ident: UUID or name of a ml model.
|
||||||
|
:param patch: a json PATCH document to apply to this ml model.
|
||||||
|
"""
|
||||||
|
ml_model = utils.get_ml_model(ml_model_ident)
|
||||||
|
check_policy_on_ml_model(ml_model.as_dict(), "ml_model:update")
|
||||||
|
utils.validate_ml_model_state(ml_model, 'update')
|
||||||
|
context = pecan.request.context
|
||||||
|
compute_api = pecan.request.compute_api
|
||||||
|
ml_model = compute_api.ml_model_update(context, ml_model, patch)
|
||||||
|
return view.format_ml_model(context, pecan.request.node_url,
|
||||||
|
ml_model.as_dict())
|
||||||
|
|
||||||
|
|
||||||
|
@pecan.expose('json')
|
||||||
|
@exception.wrap_pecan_controller_exception
|
||||||
|
@validation.validate_query_param(pecan.request, schema.query_param_delete)
|
||||||
|
def delete(self, ml_model_ident, force=False, **kwargs):
|
||||||
|
"""Delete a ML Model.
|
||||||
|
|
||||||
|
:param ml_model_ident: UUID or Name of a ML Model.
|
||||||
|
:param force: If True, allow to force delete the ML Model.
|
||||||
|
"""
|
||||||
|
context = pecan.request.context
|
||||||
|
ml_model = utils.get_ml_model(ml_model_ident)
|
||||||
|
check_policy_on_ml_model(ml_model.as_dict(), "ml_model:delete")
|
||||||
|
try:
|
||||||
|
force = strutils.bool_from_string(force, strict=True)
|
||||||
|
except ValueError:
|
||||||
|
bools = ', '.join(strutils.TRUE_STRINGS + strutils.FALSE_STRINGS)
|
||||||
|
raise exception.InvalidValue(_('Valid force values are: %s')
|
||||||
|
% bools)
|
||||||
|
stop = kwargs.pop('stop', False)
|
||||||
|
try:
|
||||||
|
stop = strutils.bool_from_string(stop, strict=True)
|
||||||
|
except ValueError:
|
||||||
|
bools = ', '.join(strutils.TRUE_STRINGS + strutils.FALSE_STRINGS)
|
||||||
|
raise exception.InvalidValue(_('Valid stop values are: %s')
|
||||||
|
% bools)
|
||||||
|
compute_api = pecan.request.compute_api
|
||||||
|
if not force:
|
||||||
|
utils.validate_ml_model_state(ml_model, 'delete')
|
||||||
|
ml_model.status = consts.DELETING
|
||||||
|
if ml_model.node:
|
||||||
|
compute_api.ml_model_delete(context, ml_model, force)
|
||||||
|
else:
|
||||||
|
ml_model.destroy(context)
|
||||||
|
pecan.response.status = 204
|
||||||
|
|
||||||
|
|
||||||
|
@pecan.expose('json')
|
||||||
|
@exception.wrap_pecan_controller_exception
|
||||||
|
def deploy(self, ml_model_ident, **kwargs):
|
||||||
|
"""Deploy ML Model.
|
||||||
|
|
||||||
|
:param ml_model_ident: UUID or Name of a ML Model.
|
||||||
|
"""
|
||||||
|
ml_model = utils.get_ml_model(ml_model_ident)
|
||||||
|
check_policy_on_ml_model(ml_model.as_dict(), "ml_model:deploy")
|
||||||
|
utils.validate_ml_model_state(ml_model, 'deploy')
|
||||||
|
LOG.debug('Calling compute.ml_model_deploy with %s',
|
||||||
|
ml_model.uuid)
|
||||||
|
context = pecan.request.context
|
||||||
|
compute_api = pecan.request.compute_api
|
||||||
|
compute_api.ml_model_deploy(context, ml_model)
|
||||||
|
pecan.response.status = 202
|
||||||
|
|
||||||
|
@pecan.expose('json')
|
||||||
|
@exception.wrap_pecan_controller_exception
|
||||||
|
def undeploy(self, ml_model_ident, **kwargs):
|
||||||
|
"""Undeploy ML Model.
|
||||||
|
|
||||||
|
:param ml_model_ident: UUID or Name of a ML Model.
|
||||||
|
"""
|
||||||
|
ml_model = utils.get_ml_model(ml_model_ident)
|
||||||
|
check_policy_on_ml_model(ml_model.as_dict(), "ml_model:deploy")
|
||||||
|
utils.validate_ml_model_state(ml_model, 'undeploy')
|
||||||
|
LOG.debug('Calling compute.ml_model_deploy with %s',
|
||||||
|
ml_model.uuid)
|
||||||
|
context = pecan.request.context
|
||||||
|
compute_api = pecan.request.compute_api
|
||||||
|
compute_api.ml_model_undeploy(context, ml_model)
|
||||||
|
pecan.response.status = 202
|
0
gyan/api/controllers/v1/schemas/__init__.py
Normal file
0
gyan/api/controllers/v1/schemas/__init__.py
Normal file
49
gyan/api/controllers/v1/schemas/ml_models.py
Normal file
49
gyan/api/controllers/v1/schemas/ml_models.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
# 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 copy
|
||||||
|
|
||||||
|
from gyan.api.controllers.v1.schemas import parameter_types
|
||||||
|
|
||||||
|
_ml_model_properties = {}
|
||||||
|
|
||||||
|
ml_model_create = {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': _ml_model_properties,
|
||||||
|
'required': ['name'],
|
||||||
|
'additionalProperties': False
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
query_param_create = {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'run': parameter_types.boolean_extended
|
||||||
|
},
|
||||||
|
'additionalProperties': False
|
||||||
|
}
|
||||||
|
|
||||||
|
ml_model_update = {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {},
|
||||||
|
'additionalProperties': False
|
||||||
|
}
|
||||||
|
|
||||||
|
query_param_delete = {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'force': parameter_types.boolean_extended,
|
||||||
|
'all_projects': parameter_types.boolean_extended,
|
||||||
|
'stop': parameter_types.boolean_extended
|
||||||
|
},
|
||||||
|
'additionalProperties': False
|
||||||
|
}
|
97
gyan/api/controllers/v1/schemas/parameter_types.py
Normal file
97
gyan/api/controllers/v1/schemas/parameter_types.py
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
# 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 copy
|
||||||
|
import signal
|
||||||
|
import sys
|
||||||
|
import gyan.conf
|
||||||
|
|
||||||
|
CONF = gyan.conf.CONF
|
||||||
|
|
||||||
|
non_negative_integer = {
|
||||||
|
'type': ['integer', 'string'],
|
||||||
|
'pattern': '^[0-9]*$', 'minimum': 0
|
||||||
|
}
|
||||||
|
|
||||||
|
positive_integer = {
|
||||||
|
'type': ['integer', 'string'],
|
||||||
|
'pattern': '^[0-9]*$', 'minimum': 1
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean_extended = {
|
||||||
|
'type': ['boolean', 'string'],
|
||||||
|
'enum': [True, 'True', 'TRUE', 'true', '1', 'ON', 'On', 'on',
|
||||||
|
'YES', 'Yes', 'yes',
|
||||||
|
False, 'False', 'FALSE', 'false', '0', 'OFF', 'Off', 'off',
|
||||||
|
'NO', 'No', 'no'],
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean = {
|
||||||
|
'type': ['boolean', 'string'],
|
||||||
|
'enum': [True, 'True', 'true', False, 'False', 'false'],
|
||||||
|
}
|
||||||
|
|
||||||
|
ml_model_name = {
|
||||||
|
'type': ['string', 'null'],
|
||||||
|
'minLength': 2,
|
||||||
|
'maxLength': 255,
|
||||||
|
'pattern': '^[a-zA-Z0-9][a-zA-Z0-9_.-]+$'
|
||||||
|
}
|
||||||
|
|
||||||
|
hex_uuid = {
|
||||||
|
'type': 'string',
|
||||||
|
'maxLength': 32,
|
||||||
|
'minLength': 32,
|
||||||
|
'pattern': '^[a-fA-F0-9]*$'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
labels = {
|
||||||
|
'type': ['object', 'null']
|
||||||
|
}
|
||||||
|
|
||||||
|
hints = {
|
||||||
|
'type': ['object', 'null']
|
||||||
|
}
|
||||||
|
hostname = {
|
||||||
|
'type': ['string', 'null'],
|
||||||
|
'minLength': 2,
|
||||||
|
'maxLength': 63
|
||||||
|
}
|
||||||
|
|
||||||
|
repo = {
|
||||||
|
'type': 'string',
|
||||||
|
'minLength': 2,
|
||||||
|
'maxLength': 255,
|
||||||
|
'pattern': '[a-zA-Z0-9][a-zA-Z0-9_.-]'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
string_ps_args = {
|
||||||
|
'type': ['string'],
|
||||||
|
'pattern': '[a-zA-Z- ,+]*'
|
||||||
|
}
|
||||||
|
|
||||||
|
str_and_int = {
|
||||||
|
'type': ['string', 'integer', 'null'],
|
||||||
|
}
|
||||||
|
|
||||||
|
hostname = {
|
||||||
|
'type': 'string', 'minLength': 1, 'maxLength': 255,
|
||||||
|
# NOTE: 'host' is defined in "services" table, and that
|
||||||
|
# means a hostname. The hostname grammar in RFC952 does
|
||||||
|
# not allow for underscores in hostnames. However, this
|
||||||
|
# schema allows them, because it sometimes occurs in
|
||||||
|
# real systems.
|
||||||
|
'pattern': '^[a-zA-Z0-9-._]*$',
|
||||||
|
}
|
50
gyan/api/controllers/v1/schemas/services.py
Normal file
50
gyan/api/controllers/v1/schemas/services.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
# 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 gyan.api.controllers.v1.schemas import parameter_types
|
||||||
|
|
||||||
|
query_param_enable = {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'host': parameter_types.hostname,
|
||||||
|
'binary': {
|
||||||
|
'type': 'string', 'minLength': 1, 'maxLength': 255,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'additionalProperties': False
|
||||||
|
}
|
||||||
|
|
||||||
|
query_param_disable = {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'host': parameter_types.hostname,
|
||||||
|
'binary': {
|
||||||
|
'type': 'string', 'minLength': 1, 'maxLength': 255,
|
||||||
|
},
|
||||||
|
'disabled_reason': {
|
||||||
|
'type': 'string', 'minLength': 1, 'maxLength': 255,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'additionalProperties': False
|
||||||
|
}
|
||||||
|
|
||||||
|
query_param_force_down = {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'host': parameter_types.hostname,
|
||||||
|
'binary': {
|
||||||
|
'type': 'string', 'minLength': 1, 'maxLength': 255,
|
||||||
|
},
|
||||||
|
'forced_down': parameter_types.boolean
|
||||||
|
},
|
||||||
|
'additionalProperties': False
|
||||||
|
}
|
0
gyan/api/controllers/v1/views/__init__.py
Normal file
0
gyan/api/controllers/v1/views/__init__.py
Normal file
43
gyan/api/controllers/v1/views/hosts_view.py
Normal file
43
gyan/api/controllers/v1/views/hosts_view.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
#
|
||||||
|
# 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 itertools
|
||||||
|
|
||||||
|
from gyan.api.controllers import link
|
||||||
|
|
||||||
|
|
||||||
|
_basic_keys = (
|
||||||
|
'id',
|
||||||
|
'hostname',
|
||||||
|
'type',
|
||||||
|
'status'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def format_host(url, host):
|
||||||
|
def transform(key, value):
|
||||||
|
if key not in _basic_keys:
|
||||||
|
return
|
||||||
|
if key == 'id':
|
||||||
|
yield ('id', value)
|
||||||
|
yield ('links', [link.make_link(
|
||||||
|
'self', url, 'hosts', value),
|
||||||
|
link.make_link(
|
||||||
|
'bookmark', url,
|
||||||
|
'hosts', value,
|
||||||
|
bookmark=True)])
|
||||||
|
else:
|
||||||
|
yield (key, value)
|
||||||
|
|
||||||
|
return dict(itertools.chain.from_iterable(
|
||||||
|
transform(k, v) for k, v in host.as_dict().items()))
|
55
gyan/api/controllers/v1/views/ml_models_view.py
Normal file
55
gyan/api/controllers/v1/views/ml_models_view.py
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
#
|
||||||
|
# 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 itertools
|
||||||
|
|
||||||
|
from gyan.api.controllers import link
|
||||||
|
from gyan.common.policies import ml_model as policies
|
||||||
|
|
||||||
|
_basic_keys = (
|
||||||
|
'uuid',
|
||||||
|
'user_id',
|
||||||
|
'project_id',
|
||||||
|
'name',
|
||||||
|
'url',
|
||||||
|
'status',
|
||||||
|
'status_reason',
|
||||||
|
'task_state',
|
||||||
|
'labels',
|
||||||
|
'host',
|
||||||
|
'status_detail'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def format_ml_model(context, url, ml_model):
|
||||||
|
def transform(key, value):
|
||||||
|
if key not in _basic_keys:
|
||||||
|
return
|
||||||
|
# strip the key if it is not allowed by policy
|
||||||
|
policy_action = policies.ML_MODEL % ('get_one:%s' % key)
|
||||||
|
if not context.can(policy_action, fatal=False, might_not_exist=True):
|
||||||
|
return
|
||||||
|
if key == 'uuid':
|
||||||
|
yield ('uuid', value)
|
||||||
|
if url:
|
||||||
|
yield ('links', [link.make_link(
|
||||||
|
'self', url, 'ml_models', value),
|
||||||
|
link.make_link(
|
||||||
|
'bookmark', url,
|
||||||
|
'ml_models', value,
|
||||||
|
bookmark=True)])
|
||||||
|
else:
|
||||||
|
yield (key, value)
|
||||||
|
|
||||||
|
return dict(itertools.chain.from_iterable(
|
||||||
|
transform(k, v) for k, v in ml_model.items()))
|
145
gyan/api/controllers/versions.py
Normal file
145
gyan/api/controllers/versions.py
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
# 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 webob import exc
|
||||||
|
|
||||||
|
from gyan.common.i18n import _
|
||||||
|
|
||||||
|
# NOTE(tbh): v1.0 is reserved to indicate Ocata's API, but is not presently
|
||||||
|
# supported by the API service. All changes between Ocata and the
|
||||||
|
# point where we added microversioning are considered backwards-
|
||||||
|
# compatible, but are not specifically discoverable at this time.
|
||||||
|
#
|
||||||
|
# The v1.1 version indicates this "initial" version as being
|
||||||
|
# different from Ocata (v1.0), and includes the following changes:
|
||||||
|
#
|
||||||
|
# Add details of new api versions here:
|
||||||
|
|
||||||
|
#
|
||||||
|
# For each newly added microversion change, update the API version history
|
||||||
|
# string below with a one or two line description. Also update
|
||||||
|
# rest_api_version_history.rst for extra information on microversion.
|
||||||
|
REST_API_VERSION_HISTORY = """REST API Version History:
|
||||||
|
|
||||||
|
* 1.1 - Initial version
|
||||||
|
"""
|
||||||
|
|
||||||
|
BASE_VER = '1.1'
|
||||||
|
CURRENT_MAX_VER = '1.1'
|
||||||
|
|
||||||
|
|
||||||
|
class Version(object):
|
||||||
|
"""API Version object."""
|
||||||
|
|
||||||
|
string = 'OpenStack-API-Version'
|
||||||
|
"""HTTP Header string carrying the requested version"""
|
||||||
|
|
||||||
|
min_string = 'OpenStack-API-Minimum-Version'
|
||||||
|
"""HTTP response header"""
|
||||||
|
|
||||||
|
max_string = 'OpenStack-API-Maximum-Version'
|
||||||
|
"""HTTP response header"""
|
||||||
|
|
||||||
|
service_string = 'ml'
|
||||||
|
|
||||||
|
def __init__(self, headers, default_version, latest_version,
|
||||||
|
from_string=None):
|
||||||
|
"""Create an API Version object from the supplied headers.
|
||||||
|
|
||||||
|
:param headers: webob headers
|
||||||
|
:param default_version: version to use if not specified in headers
|
||||||
|
:param latest_version: version to use if latest is requested
|
||||||
|
:param from_string: create the version from string not headers
|
||||||
|
:raises: webob.HTTPNotAcceptable
|
||||||
|
"""
|
||||||
|
if from_string:
|
||||||
|
(self.major, self.minor) = tuple(int(i)
|
||||||
|
for i in from_string.split('.'))
|
||||||
|
|
||||||
|
else:
|
||||||
|
(self.major, self.minor) = Version.parse_headers(headers,
|
||||||
|
default_version,
|
||||||
|
latest_version)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '%s.%s' % (self.major, self.minor)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_headers(headers, default_version, latest_version):
|
||||||
|
"""Determine the API version requested based on the headers supplied.
|
||||||
|
|
||||||
|
:param headers: webob headers
|
||||||
|
:param default_version: version to use if not specified in headers
|
||||||
|
:param latest_version: version to use if latest is requested
|
||||||
|
:returns: a tuple of (major, minor) version numbers
|
||||||
|
:raises: webob.HTTPNotAcceptable
|
||||||
|
"""
|
||||||
|
|
||||||
|
version_hdr = headers.get(Version.string, default_version)
|
||||||
|
|
||||||
|
try:
|
||||||
|
version_service, version_str = version_hdr.split()
|
||||||
|
except ValueError:
|
||||||
|
raise exc.HTTPNotAcceptable(_(
|
||||||
|
"Invalid service type for %s header") % Version.string)
|
||||||
|
|
||||||
|
if version_str.lower() == 'latest':
|
||||||
|
version_service, version_str = latest_version.split()
|
||||||
|
|
||||||
|
if version_service != Version.service_string:
|
||||||
|
raise exc.HTTPNotAcceptable(_(
|
||||||
|
"Invalid service type for %s header") % Version.string)
|
||||||
|
try:
|
||||||
|
version = tuple(int(i) for i in version_str.split('.'))
|
||||||
|
except ValueError:
|
||||||
|
version = ()
|
||||||
|
|
||||||
|
if len(version) != 2:
|
||||||
|
raise exc.HTTPNotAcceptable(_(
|
||||||
|
"Invalid value for %s header") % Version.string)
|
||||||
|
return version
|
||||||
|
|
||||||
|
def is_null(self):
|
||||||
|
return self.major == 0 and self.minor == 0
|
||||||
|
|
||||||
|
def matches(self, start_version, end_version):
|
||||||
|
if self.is_null():
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
return start_version <= self <= end_version
|
||||||
|
|
||||||
|
def __lt__(self, other):
|
||||||
|
if self.major < other.major:
|
||||||
|
return True
|
||||||
|
if self.major == other.major and self.minor < other.minor:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __gt__(self, other):
|
||||||
|
if self.major > other.major:
|
||||||
|
return True
|
||||||
|
if self.major == other.major and self.minor > other.minor:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.major == other.major and self.minor == other.minor
|
||||||
|
|
||||||
|
def __le__(self, other):
|
||||||
|
return self < other or self == other
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return not self.__eq__(other)
|
||||||
|
|
||||||
|
def __ge__(self, other):
|
||||||
|
return self > other or self == other
|
114
gyan/api/hooks.py
Normal file
114
gyan/api/hooks.py
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
# 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 pecan import hooks
|
||||||
|
|
||||||
|
from gyan.common import context
|
||||||
|
from gyan.compute import api as compute_api
|
||||||
|
import gyan.conf
|
||||||
|
|
||||||
|
CONF = gyan.conf.CONF
|
||||||
|
|
||||||
|
|
||||||
|
class ContextHook(hooks.PecanHook):
|
||||||
|
"""Configures a request context and attaches it to the request.
|
||||||
|
|
||||||
|
The following HTTP request headers are used:
|
||||||
|
|
||||||
|
X-User-Name:
|
||||||
|
Used for context.user_name.
|
||||||
|
|
||||||
|
X-User-Id:
|
||||||
|
Used for context.user_id.
|
||||||
|
|
||||||
|
X-Project-Name:
|
||||||
|
Used for context.project.
|
||||||
|
|
||||||
|
X-Project-Id:
|
||||||
|
Used for context.project_id.
|
||||||
|
|
||||||
|
X-Auth-Token:
|
||||||
|
Used for context.auth_token.
|
||||||
|
|
||||||
|
X-Roles:
|
||||||
|
Used for context.roles.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def before(self, state):
|
||||||
|
headers = state.request.headers
|
||||||
|
user_name = headers.get('X-User-Name')
|
||||||
|
user_id = headers.get('X-User-Id')
|
||||||
|
project = headers.get('X-Project-Name')
|
||||||
|
project_id = headers.get('X-Project-Id')
|
||||||
|
domain_id = headers.get('X-User-Domain-Id')
|
||||||
|
domain_name = headers.get('X-User-Domain-Name')
|
||||||
|
auth_token = headers.get('X-Auth-Token')
|
||||||
|
roles = headers.get('X-Roles', '').split(',')
|
||||||
|
auth_token_info = state.request.environ.get('keystone.token_info')
|
||||||
|
|
||||||
|
state.request.context = context.make_context(
|
||||||
|
auth_token=auth_token,
|
||||||
|
auth_token_info=auth_token_info,
|
||||||
|
user_name=user_name,
|
||||||
|
user_id=user_id,
|
||||||
|
project_name=project,
|
||||||
|
project_id=project_id,
|
||||||
|
domain_id=domain_id,
|
||||||
|
domain_name=domain_name,
|
||||||
|
roles=roles)
|
||||||
|
|
||||||
|
|
||||||
|
class RPCHook(hooks.PecanHook):
|
||||||
|
"""Attach the rpcapi object to the request so controllers can get to it."""
|
||||||
|
|
||||||
|
def before(self, state):
|
||||||
|
context = state.request.context
|
||||||
|
state.request.compute_api = compute_api.API(context)
|
||||||
|
|
||||||
|
|
||||||
|
class NoExceptionTracebackHook(hooks.PecanHook):
|
||||||
|
"""Workaround rpc.common: deserialize_remote_exception.
|
||||||
|
|
||||||
|
deserialize_remote_exception builds rpc exception traceback into error
|
||||||
|
message which is then sent to the client. Such behavior is a security
|
||||||
|
concern so this hook is aimed to cut-off traceback from the error message.
|
||||||
|
"""
|
||||||
|
# NOTE(tbh): 'after' hook used instead of 'on_error' because
|
||||||
|
# 'on_error' never fired for wsme+pecan pair. wsme @wsexpose decorator
|
||||||
|
# catches and handles all the errors, so 'on_error' dedicated for unhandled
|
||||||
|
# exceptions never fired.
|
||||||
|
def after(self, state):
|
||||||
|
# Omit empty body. Some errors may not have body at this level yet.
|
||||||
|
if not state.response.body:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Do nothing if there is no error.
|
||||||
|
if 200 <= state.response.status_int < 400:
|
||||||
|
return
|
||||||
|
|
||||||
|
json_body = state.response.json
|
||||||
|
# Do not remove traceback when server in debug mode (except 'Server'
|
||||||
|
# errors when 'debuginfo' will be used for traces).
|
||||||
|
if CONF.debug and json_body.get('faultcode') != 'Server':
|
||||||
|
return
|
||||||
|
|
||||||
|
title = json_body.get('title')
|
||||||
|
traceback_marker = 'Traceback (most recent call last):'
|
||||||
|
if title and (traceback_marker in title):
|
||||||
|
# Cut-off traceback.
|
||||||
|
title = title.split(traceback_marker, 1)[0]
|
||||||
|
# Remove trailing newlines and spaces if any.
|
||||||
|
json_body['title'] = title.rstrip()
|
||||||
|
# Replace the whole json. Cannot change original one beacause it's
|
||||||
|
# generated on the fly.
|
||||||
|
state.response.json = json_body
|
69
gyan/api/http_error.py
Normal file
69
gyan/api/http_error.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
# 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 six
|
||||||
|
|
||||||
|
from oslo_serialization import jsonutils as json
|
||||||
|
from webob import exc
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPNotAcceptableAPIVersion(exc.HTTPNotAcceptable):
|
||||||
|
# subclass of :class:`~HTTPNotAcceptable`
|
||||||
|
#
|
||||||
|
# This indicates the resource identified by the request is only
|
||||||
|
# capable of generating response entities which have content
|
||||||
|
# characteristics not acceptable according to the accept headers
|
||||||
|
# sent in the request.
|
||||||
|
#
|
||||||
|
# code: 406, title: Not Acceptable
|
||||||
|
#
|
||||||
|
# differences from webob.exc.HTTPNotAcceptable:
|
||||||
|
#
|
||||||
|
# - additional max and min version parameters
|
||||||
|
# - additional error info for code, title, and links
|
||||||
|
code = 406
|
||||||
|
title = 'Not Acceptable'
|
||||||
|
max_version = ''
|
||||||
|
min_version = ''
|
||||||
|
|
||||||
|
def __init__(self, detail=None, headers=None, comment=None,
|
||||||
|
body_template=None, max_version='', min_version='', **kwargs):
|
||||||
|
|
||||||
|
super(HTTPNotAcceptableAPIVersion, self).__init__(
|
||||||
|
detail=detail, headers=headers, comment=comment,
|
||||||
|
body_template=body_template, **kwargs)
|
||||||
|
|
||||||
|
self.max_version = max_version
|
||||||
|
self.min_version = min_version
|
||||||
|
|
||||||
|
def __call__(self, environ, start_response):
|
||||||
|
for err_str in self.app_iter:
|
||||||
|
err = {}
|
||||||
|
try:
|
||||||
|
err = json.loads(err_str.decode('utf-8'))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
links = {'rel': 'help', 'href': 'https://developer.openstack.org'
|
||||||
|
'/api-guide/compute/microversions.html'}
|
||||||
|
|
||||||
|
err['max_version'] = self.max_version
|
||||||
|
err['min_version'] = self.min_version
|
||||||
|
err['code'] = "gyan.microversion-unsupported"
|
||||||
|
err['links'] = [links]
|
||||||
|
err['title'] = "Requested microversion is unsupported"
|
||||||
|
|
||||||
|
self.app_iter = [six.b(json.dump_as_bytes(err))]
|
||||||
|
self.headers['Content-Length'] = str(len(self.app_iter[0]))
|
||||||
|
|
||||||
|
return super(HTTPNotAcceptableAPIVersion, self).__call__(
|
||||||
|
environ, start_response)
|
21
gyan/api/middleware/__init__.py
Normal file
21
gyan/api/middleware/__init__.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# 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 gyan.api.middleware import auth_token
|
||||||
|
from gyan.api.middleware import parsable_error
|
||||||
|
|
||||||
|
|
||||||
|
AuthTokenMiddleware = auth_token.AuthTokenMiddleware
|
||||||
|
ParsableErrorMiddleware = parsable_error.ParsableErrorMiddleware
|
||||||
|
|
||||||
|
__all__ = ('AuthTokenMiddleware',
|
||||||
|
'ParsableErrorMiddleware')
|
71
gyan/api/middleware/auth_token.py
Normal file
71
gyan/api/middleware/auth_token.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
# 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 re
|
||||||
|
|
||||||
|
from keystonemiddleware import auth_token
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
|
from gyan.common import exception
|
||||||
|
from gyan.common.i18n import _
|
||||||
|
from gyan.common import utils
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AuthTokenMiddleware(auth_token.AuthProtocol):
|
||||||
|
"""A wrapper on Keystone auth_token middleware.
|
||||||
|
|
||||||
|
Does not perform verification of authentication tokens
|
||||||
|
for public routes in the API.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, app, conf, public_api_routes=None):
|
||||||
|
if public_api_routes is None:
|
||||||
|
public_api_routes = []
|
||||||
|
route_pattern_tpl = '%s(\.json)?$'
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.public_api_routes = [re.compile(route_pattern_tpl % route_tpl)
|
||||||
|
for route_tpl in public_api_routes]
|
||||||
|
except re.error as e:
|
||||||
|
msg = _('Cannot compile public API routes: %s') % e
|
||||||
|
|
||||||
|
LOG.error(msg)
|
||||||
|
raise exception.ConfigInvalid(error_msg=msg)
|
||||||
|
|
||||||
|
super(AuthTokenMiddleware, self).__init__(app, conf)
|
||||||
|
|
||||||
|
def __call__(self, env, start_response):
|
||||||
|
path = utils.safe_rstrip(env.get('PATH_INFO'), '/')
|
||||||
|
|
||||||
|
# The information whether the API call is being performed against the
|
||||||
|
# public API is required for some other components. Saving it to the
|
||||||
|
# WSGI environment is reasonable thereby.
|
||||||
|
env['is_public_api'] = any([re.match(pattern, path)
|
||||||
|
for pattern in self.public_api_routes])
|
||||||
|
|
||||||
|
if env['is_public_api']:
|
||||||
|
return self._app(env, start_response)
|
||||||
|
|
||||||
|
return super(AuthTokenMiddleware, self).__call__(env, start_response)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def factory(cls, global_config, **local_conf):
|
||||||
|
public_routes = local_conf.get('acl_public_routes', '')
|
||||||
|
public_api_routes = [path.strip() for path in public_routes.split(',')]
|
||||||
|
|
||||||
|
def _factory(app):
|
||||||
|
return cls(app, global_config, public_api_routes=public_api_routes)
|
||||||
|
|
||||||
|
return _factory
|
99
gyan/api/middleware/parsable_error.py
Normal file
99
gyan/api/middleware/parsable_error.py
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
# Copyright ? 2012 New Dream Network, LLC (DreamHost)
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
"""
|
||||||
|
Middleware to replace the plain text message body of an error
|
||||||
|
response with one formatted so the client can parse it.
|
||||||
|
|
||||||
|
Based on pecan.middleware.errordocument
|
||||||
|
"""
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
from oslo_serialization import jsonutils as json
|
||||||
|
from gyan.common.i18n import _
|
||||||
|
|
||||||
|
|
||||||
|
class ParsableErrorMiddleware(object):
|
||||||
|
"""Replace error body with something the client can parse."""
|
||||||
|
|
||||||
|
def __init__(self, app):
|
||||||
|
self.app = app
|
||||||
|
|
||||||
|
def __call__(self, environ, start_response):
|
||||||
|
# Request for this state, modified by replace_start_response()
|
||||||
|
# and used when an error is being reported.
|
||||||
|
state = {}
|
||||||
|
|
||||||
|
def replacement_start_response(status, headers, exc_info=None):
|
||||||
|
"""Overrides the default response to make errors parsable."""
|
||||||
|
try:
|
||||||
|
status_code = int(status.split(' ')[0])
|
||||||
|
state['status_code'] = status_code
|
||||||
|
except (ValueError, TypeError): # pragma: nocover
|
||||||
|
raise Exception(_(
|
||||||
|
'ErrorDocumentMiddleware received an invalid '
|
||||||
|
'status %s') % status)
|
||||||
|
else:
|
||||||
|
if (state['status_code'] // 100) not in (2, 3):
|
||||||
|
# Remove some headers so we can replace them later
|
||||||
|
# when we have the full error message and can
|
||||||
|
# compute the length.
|
||||||
|
headers = [(h, v)
|
||||||
|
for (h, v) in headers
|
||||||
|
if h not in ('Content-Length', 'Content-Type')
|
||||||
|
]
|
||||||
|
# Save the headers in case we need to modify them.
|
||||||
|
state['headers'] = headers
|
||||||
|
return start_response(status, headers, exc_info)
|
||||||
|
|
||||||
|
app_iter = self.app(environ, replacement_start_response)
|
||||||
|
|
||||||
|
if (state['status_code'] // 100) not in (2, 3):
|
||||||
|
errs = []
|
||||||
|
for err_str in app_iter:
|
||||||
|
err = {}
|
||||||
|
try:
|
||||||
|
err = json.loads(err_str.decode('utf-8'))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if 'title' in err and 'description' in err:
|
||||||
|
title = err['title']
|
||||||
|
desc = err['description']
|
||||||
|
else:
|
||||||
|
title = ''
|
||||||
|
desc = ''
|
||||||
|
|
||||||
|
error_code = err['faultstring'].lower() \
|
||||||
|
if 'faultstring' in err else ''
|
||||||
|
# 'ml-infra' is the service-name. The general form of the
|
||||||
|
# code is service-name.error-code.
|
||||||
|
code = '.'.join(['ml-infra', error_code])
|
||||||
|
|
||||||
|
errs.append({
|
||||||
|
'request_id': '',
|
||||||
|
'code': code,
|
||||||
|
'status': state['status_code'],
|
||||||
|
'title': title,
|
||||||
|
'detail': desc,
|
||||||
|
'links': []
|
||||||
|
})
|
||||||
|
|
||||||
|
body = [six.b(json.dumps({'errors': errs}))]
|
||||||
|
|
||||||
|
state['headers'].append(('Content-Type', 'application/json'))
|
||||||
|
state['headers'].append(('Content-Length', str(len(body[0]))))
|
||||||
|
else:
|
||||||
|
body = app_iter
|
||||||
|
return body
|
37
gyan/api/servicegroup.py
Normal file
37
gyan/api/servicegroup.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# 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_utils import timeutils
|
||||||
|
|
||||||
|
import gyan.conf
|
||||||
|
from gyan import objects
|
||||||
|
|
||||||
|
CONF = gyan.conf.CONF
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceGroup(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.service_down_time = CONF.service_down_time
|
||||||
|
|
||||||
|
def service_is_up(self, member):
|
||||||
|
if not isinstance(member, objects.GyanService):
|
||||||
|
raise TypeError
|
||||||
|
if member.forced_down:
|
||||||
|
return False
|
||||||
|
|
||||||
|
last_heartbeat = (member.last_seen_up or
|
||||||
|
member.updated_at or member.created_at)
|
||||||
|
now = timeutils.utcnow()
|
||||||
|
elapsed = timeutils.delta_seconds(last_heartbeat, now)
|
||||||
|
is_up = abs(elapsed) <= self.service_down_time
|
||||||
|
return is_up
|
116
gyan/api/utils.py
Normal file
116
gyan/api/utils.py
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
# 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 oslo_utils import uuidutils
|
||||||
|
import pecan
|
||||||
|
|
||||||
|
from gyan.api.controllers import versions
|
||||||
|
from gyan.common import exception
|
||||||
|
from gyan.common.i18n import _
|
||||||
|
import gyan.conf
|
||||||
|
from gyan import objects
|
||||||
|
|
||||||
|
CONF = gyan.conf.CONF
|
||||||
|
|
||||||
|
|
||||||
|
def string_or_none(value):
|
||||||
|
if value in [None, 'None']:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def validate_limit(limit):
|
||||||
|
try:
|
||||||
|
if limit is not None and int(limit) <= 0:
|
||||||
|
raise exception.InvalidValue(_("Limit must be positive integer"))
|
||||||
|
except ValueError:
|
||||||
|
raise exception.InvalidValue(_("Limit must be positive integer"))
|
||||||
|
|
||||||
|
if limit is not None:
|
||||||
|
return min(CONF.api.max_limit, int(limit))
|
||||||
|
else:
|
||||||
|
return CONF.api.max_limit
|
||||||
|
|
||||||
|
|
||||||
|
def validate_sort_dir(sort_dir):
|
||||||
|
if sort_dir not in ['asc', 'desc']:
|
||||||
|
raise exception.InvalidValue(_("Invalid sort direction: %s. "
|
||||||
|
"Acceptable values are "
|
||||||
|
"'asc' or 'desc'") % sort_dir)
|
||||||
|
return sort_dir
|
||||||
|
|
||||||
|
|
||||||
|
def get_resource(resource, resource_ident):
|
||||||
|
"""Get the resource from the uuid or logical name.
|
||||||
|
|
||||||
|
:param resource: the resource type.
|
||||||
|
:param resource_ident: the UUID or logical name of the resource.
|
||||||
|
|
||||||
|
:returns: The resource.
|
||||||
|
"""
|
||||||
|
resource = getattr(objects, resource)
|
||||||
|
context = pecan.request.context
|
||||||
|
if context.is_admin:
|
||||||
|
context.all_projects = True
|
||||||
|
if uuidutils.is_uuid_like(resource_ident):
|
||||||
|
return resource.get_by_uuid(context, resource_ident)
|
||||||
|
|
||||||
|
return resource.get_by_name(context, resource_ident)
|
||||||
|
|
||||||
|
|
||||||
|
def _do_enforce_content_types(pecan_req, valid_content_types):
|
||||||
|
"""Content type enforcement
|
||||||
|
|
||||||
|
Check to see that content type in the request is one of the valid
|
||||||
|
types passed in by our caller.
|
||||||
|
"""
|
||||||
|
if pecan_req.content_type not in valid_content_types:
|
||||||
|
m = (
|
||||||
|
"Unexpected content type: {type}. Expected content types "
|
||||||
|
"are: {expected}"
|
||||||
|
).format(
|
||||||
|
type=pecan_req.content_type.decode('utf-8'),
|
||||||
|
expected=valid_content_types
|
||||||
|
)
|
||||||
|
pecan.abort(415, m)
|
||||||
|
|
||||||
|
|
||||||
|
def enforce_content_types(valid_content_types):
|
||||||
|
"""Decorator handling content type enforcement on behalf of REST verbs."""
|
||||||
|
|
||||||
|
def content_types_decorator(fn):
|
||||||
|
|
||||||
|
@functools.wraps(fn)
|
||||||
|
def content_types_enforcer(inst, *args, **kwargs):
|
||||||
|
_do_enforce_content_types(pecan.request, valid_content_types)
|
||||||
|
return fn(inst, *args, **kwargs)
|
||||||
|
|
||||||
|
return content_types_enforcer
|
||||||
|
|
||||||
|
return content_types_decorator
|
||||||
|
|
||||||
|
|
||||||
|
def version_check(action, version):
|
||||||
|
"""Check whether the current version supports the operation.
|
||||||
|
|
||||||
|
:param action: Operations to be executed.
|
||||||
|
:param version: The minimum version required to perform the operation.
|
||||||
|
|
||||||
|
"""
|
||||||
|
req_version = pecan.request.version
|
||||||
|
min_version = versions.Version('', '', '', version)
|
||||||
|
if req_version < min_version:
|
||||||
|
raise exception.InvalidParamInVersion(param=action,
|
||||||
|
req_version=req_version,
|
||||||
|
min_version=min_version)
|
57
gyan/api/validation/__init__.py
Normal file
57
gyan/api/validation/__init__.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
# 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 gyan.api.validation import validators
|
||||||
|
|
||||||
|
|
||||||
|
def validated(request_body_schema):
|
||||||
|
"""Register a schema to validate a resource reference.
|
||||||
|
|
||||||
|
Registered schema will be used for validating a request body just before
|
||||||
|
API method execution.
|
||||||
|
|
||||||
|
:param request_body_schema: a schema to validate the resource reference
|
||||||
|
"""
|
||||||
|
schema_validator = validators.SchemaValidator(request_body_schema,
|
||||||
|
is_body=True)
|
||||||
|
|
||||||
|
def add_validator(func):
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
schema_validator.validate(kwargs)
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
return wrapper
|
||||||
|
return add_validator
|
||||||
|
|
||||||
|
|
||||||
|
def validate_query_param(req, query_param_schema):
|
||||||
|
"""Register a schema to validate a resource reference.
|
||||||
|
|
||||||
|
Registered schema will be used for validating a request query params
|
||||||
|
just before API method execution.
|
||||||
|
|
||||||
|
:param req: the request object
|
||||||
|
:param query_param_schema: a schema to validate the resource reference
|
||||||
|
"""
|
||||||
|
|
||||||
|
schema_validator = validators.SchemaValidator(query_param_schema,
|
||||||
|
is_body=False)
|
||||||
|
|
||||||
|
def add_validator(func):
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
schema_validator.validate(req.params.mixed())
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
return wrapper
|
||||||
|
return add_validator
|
80
gyan/api/validation/validators.py
Normal file
80
gyan/api/validation/validators.py
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
# 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 jsonschema
|
||||||
|
import six
|
||||||
|
|
||||||
|
from gyan.common import exception
|
||||||
|
from gyan.common.i18n import _
|
||||||
|
|
||||||
|
|
||||||
|
class SchemaValidator(object):
|
||||||
|
"""Resource reference validator class."""
|
||||||
|
|
||||||
|
validator_org = jsonschema.Draft4Validator
|
||||||
|
|
||||||
|
def __init__(self, schema, is_body=True):
|
||||||
|
self.is_body = is_body
|
||||||
|
validators = {
|
||||||
|
'minimum': self._validate_minimum,
|
||||||
|
'maximum': self._validate_maximum
|
||||||
|
}
|
||||||
|
validator_cls = jsonschema.validators.extend(self.validator_org,
|
||||||
|
validators)
|
||||||
|
fc = jsonschema.FormatChecker()
|
||||||
|
self.validator = validator_cls(schema, format_checker=fc)
|
||||||
|
|
||||||
|
def validate(self, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
self.validator.validate(*args, **kwargs)
|
||||||
|
except jsonschema.ValidationError as ex:
|
||||||
|
if len(ex.path) > 0:
|
||||||
|
if self.is_body:
|
||||||
|
detail = _("Invalid input for field '%(path)s'."
|
||||||
|
"Value: '%(value)s'. %(message)s")
|
||||||
|
else:
|
||||||
|
detail = _("Invalid input for query parameters "
|
||||||
|
"'%(path)s'. Value: '%(value)s'. %(message)s")
|
||||||
|
detail = detail % {
|
||||||
|
'path': ex.path.pop(), 'value': ex.instance,
|
||||||
|
'message': six.text_type(ex)
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
detail = six.text_type(ex)
|
||||||
|
raise exception.SchemaValidationError(detail=detail)
|
||||||
|
|
||||||
|
def _number_from_str(self, instance):
|
||||||
|
if isinstance(instance, float) or isinstance(instance, int):
|
||||||
|
return instance
|
||||||
|
|
||||||
|
try:
|
||||||
|
value = int(instance)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
try:
|
||||||
|
value = float(instance)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return None
|
||||||
|
return value
|
||||||
|
|
||||||
|
def _validate_minimum(self, validator, minimum, instance, schema):
|
||||||
|
instance = self._number_from_str(instance)
|
||||||
|
if instance is None:
|
||||||
|
return
|
||||||
|
return self.validator_org.VALIDATORS['minimum'](validator, minimum,
|
||||||
|
instance, schema)
|
||||||
|
|
||||||
|
def _validate_maximum(self, validator, maximum, instance, schema):
|
||||||
|
instance = self._number_from_str(instance)
|
||||||
|
if instance is None:
|
||||||
|
return
|
||||||
|
return self.validator_org.VALIDATORS['maximum'](validator, maximum,
|
||||||
|
instance, schema)
|
33
gyan/api/versioned_method.py
Normal file
33
gyan/api/versioned_method.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
|
||||||
|
class VersionedMethod(object):
|
||||||
|
|
||||||
|
def __init__(self, name, start_version, end_version, func):
|
||||||
|
"""Versioning information for a single method
|
||||||
|
|
||||||
|
@name: Name of the method
|
||||||
|
@start_version: Minimum acceptable version
|
||||||
|
@end_version: Maximum acceptable_version
|
||||||
|
@func: Method to call
|
||||||
|
|
||||||
|
Minimum and maximums are inclusive
|
||||||
|
"""
|
||||||
|
self.name = name
|
||||||
|
self.start_version = start_version
|
||||||
|
self.end_version = end_version
|
||||||
|
self.func = func
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return ("Version Method %s: min: %s, max: %s"
|
||||||
|
% (self.name, self.start_version, self.end_version))
|
36
gyan/api/wsgi.py
Normal file
36
gyan/api/wsgi.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# 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 sys
|
||||||
|
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
|
from gyan.api import app
|
||||||
|
from gyan.common import profiler
|
||||||
|
from gyan.common import service
|
||||||
|
import gyan.conf
|
||||||
|
|
||||||
|
|
||||||
|
CONF = gyan.conf.CONF
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def init_application():
|
||||||
|
# Initialize the oslo configuration library and logging
|
||||||
|
service.prepare_service(sys.argv)
|
||||||
|
# NOTE:(tbh) change localhost to CONF.host
|
||||||
|
profiler.setup('gyan-api', "localhost")
|
||||||
|
|
||||||
|
LOG.debug("Configuration:")
|
||||||
|
CONF.log_opt_values(LOG, log.DEBUG)
|
||||||
|
|
||||||
|
return app.load_app()
|
17
gyan/cmd/__init__.py
Normal file
17
gyan/cmd/__init__.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
# NOTE(tbh): we monkey patch all eventlet services for easier tracking/debug
|
||||||
|
|
||||||
|
import eventlet
|
||||||
|
|
||||||
|
eventlet.monkey_patch()
|
45
gyan/cmd/api.py
Normal file
45
gyan/cmd/api.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
"""The gyan Service API."""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from gyan.common import profiler
|
||||||
|
from gyan.common import service as gyan_service
|
||||||
|
import gyan.conf
|
||||||
|
|
||||||
|
CONF = gyan.conf.CONF
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Parse config file and command line options, then start logging
|
||||||
|
gyan_service.prepare_service(sys.argv)
|
||||||
|
|
||||||
|
# Enable object backporting via the conductor
|
||||||
|
# TODO(tbh): Uncomment after rpc services are implemented
|
||||||
|
# base.gyanObject.indirection_api = base.gyanObjectIndirectionAPI()
|
||||||
|
|
||||||
|
# Setup OSprofiler for WSGI service
|
||||||
|
profiler.setup('gyan-api', CONF.api.host_ip)
|
||||||
|
|
||||||
|
# Build and start the WSGI app
|
||||||
|
launcher = gyan_service.process_launcher()
|
||||||
|
server = gyan_service.WSGIService(
|
||||||
|
'gyan_api',
|
||||||
|
CONF.api.enable_ssl_api
|
||||||
|
)
|
||||||
|
launcher.launch_service(server, workers=server.workers)
|
||||||
|
launcher.wait()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
47
gyan/cmd/compute.py
Normal file
47
gyan/cmd/compute.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# 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 os
|
||||||
|
import shlex
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from oslo_log import log as logging
|
||||||
|
from oslo_privsep import priv_context
|
||||||
|
from oslo_service import service
|
||||||
|
|
||||||
|
from gyan.common import rpc_service
|
||||||
|
from gyan.common import service as gyan_service
|
||||||
|
from gyan.common import utils
|
||||||
|
import gyan.conf
|
||||||
|
|
||||||
|
CONF = gyan.conf.CONF
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
gyan_service.prepare_service(sys.argv)
|
||||||
|
|
||||||
|
LOG.info('Starting server in PID %s', os.getpid())
|
||||||
|
CONF.log_opt_values(LOG, logging.DEBUG)
|
||||||
|
|
||||||
|
CONF.import_opt('topic', 'gyan.conf.compute', group='compute')
|
||||||
|
CONF.import_opt('host', 'gyan.conf.compute', group='compute')
|
||||||
|
|
||||||
|
|
||||||
|
from gyan.compute import manager as compute_manager
|
||||||
|
endpoints = [
|
||||||
|
compute_manager.Manager(),
|
||||||
|
]
|
||||||
|
server = rpc_service.Service.create(CONF.compute.topic, CONF.compute.host,
|
||||||
|
endpoints, binary='gyan-compute')
|
||||||
|
launcher = service.launch(CONF, server, restart_method='mutate')
|
||||||
|
launcher.wait()
|
67
gyan/cmd/db_manage.py
Normal file
67
gyan/cmd/db_manage.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""Starter script for gyan-db-manage."""
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
from gyan.db import migration
|
||||||
|
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
|
def do_version():
|
||||||
|
print('Current DB revision is %s' % migration.version())
|
||||||
|
|
||||||
|
|
||||||
|
def do_upgrade():
|
||||||
|
migration.upgrade(CONF.command.revision)
|
||||||
|
|
||||||
|
|
||||||
|
def do_stamp():
|
||||||
|
migration.stamp(CONF.command.revision)
|
||||||
|
|
||||||
|
|
||||||
|
def do_revision():
|
||||||
|
migration.revision(message=CONF.command.message,
|
||||||
|
autogenerate=CONF.command.autogenerate)
|
||||||
|
|
||||||
|
|
||||||
|
def add_command_parsers(subparsers):
|
||||||
|
parser = subparsers.add_parser('version')
|
||||||
|
parser.set_defaults(func=do_version)
|
||||||
|
|
||||||
|
parser = subparsers.add_parser('upgrade')
|
||||||
|
parser.add_argument('revision', nargs='?')
|
||||||
|
parser.set_defaults(func=do_upgrade)
|
||||||
|
|
||||||
|
parser = subparsers.add_parser('stamp')
|
||||||
|
parser.add_argument('revision')
|
||||||
|
parser.set_defaults(func=do_stamp)
|
||||||
|
|
||||||
|
parser = subparsers.add_parser('revision')
|
||||||
|
parser.add_argument('-m', '--message')
|
||||||
|
parser.add_argument('--autogenerate', action='store_true')
|
||||||
|
parser.set_defaults(func=do_revision)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
command_opt = cfg.SubCommandOpt('command',
|
||||||
|
title='Command',
|
||||||
|
help='Available commands',
|
||||||
|
handler=add_command_parsers)
|
||||||
|
CONF.register_cli_opt(command_opt)
|
||||||
|
|
||||||
|
CONF(project='gyan')
|
||||||
|
CONF.command.func()
|
0
gyan/common/__init__.py
Normal file
0
gyan/common/__init__.py
Normal file
54
gyan/common/config.py
Normal file
54
gyan/common/config.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
# 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_middleware import cors
|
||||||
|
|
||||||
|
from gyan.common import rpc
|
||||||
|
import gyan.conf
|
||||||
|
from gyan import version
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args(argv, default_config_files=None):
|
||||||
|
rpc.set_defaults(control_exchange='gyan')
|
||||||
|
gyan.conf.CONF(argv[1:],
|
||||||
|
project='gyan',
|
||||||
|
version=version.version_info.release_string(),
|
||||||
|
default_config_files=default_config_files)
|
||||||
|
rpc.init(gyan.conf.CONF)
|
||||||
|
|
||||||
|
|
||||||
|
def set_config_defaults():
|
||||||
|
"""This method updates all configuration default values."""
|
||||||
|
set_cors_middleware_defaults()
|
||||||
|
|
||||||
|
|
||||||
|
def set_cors_middleware_defaults():
|
||||||
|
"""Update default configuration options for oslo.middleware."""
|
||||||
|
cors.set_defaults(
|
||||||
|
allow_headers=['X-Auth-Token',
|
||||||
|
'X-Identity-Status',
|
||||||
|
'X-Roles',
|
||||||
|
'X-Service-Catalog',
|
||||||
|
'X-User-Id',
|
||||||
|
'X-Project-Id',
|
||||||
|
'X-OpenStack-Request-ID',
|
||||||
|
'X-Server-Management-Url'],
|
||||||
|
expose_headers=['X-Auth-Token',
|
||||||
|
'X-Subject-Token',
|
||||||
|
'X-Service-Token',
|
||||||
|
'X-OpenStack-Request-ID',
|
||||||
|
'X-Server-Management-Url'],
|
||||||
|
allow_methods=['GET',
|
||||||
|
'PUT',
|
||||||
|
'POST',
|
||||||
|
'DELETE',
|
||||||
|
'PATCH'])
|
17
gyan/common/consts.py
Normal file
17
gyan/common/consts.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
ALLOCATED = 'allocated'
|
||||||
|
CREATED = 'created'
|
||||||
|
UNDEPLOYED = 'undeployed'
|
||||||
|
DEPLOYED = 'deployed'
|
180
gyan/common/context.py
Normal file
180
gyan/common/context.py
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
import copy
|
||||||
|
from oslo_context import context
|
||||||
|
from oslo_utils import timeutils
|
||||||
|
import six
|
||||||
|
|
||||||
|
from gyan.common import exception
|
||||||
|
from gyan.common import policy
|
||||||
|
|
||||||
|
|
||||||
|
class RequestContext(context.RequestContext):
|
||||||
|
"""Extends security contexts from the OpenStack common library."""
|
||||||
|
|
||||||
|
def __init__(self, auth_token=None, domain_id=None,
|
||||||
|
domain_name=None, user_name=None, user_id=None,
|
||||||
|
user_domain_name=None, user_domain_id=None,
|
||||||
|
project_name=None, project_id=None, roles=None,
|
||||||
|
is_admin=None, read_only=False, show_deleted=False,
|
||||||
|
request_id=None, trust_id=None, auth_token_info=None,
|
||||||
|
all_projects=False, password=None, timestamp=None, **kwargs):
|
||||||
|
"""Stores several additional request parameters:
|
||||||
|
|
||||||
|
:param domain_id: The ID of the domain.
|
||||||
|
:param domain_name: The name of the domain.
|
||||||
|
:param user_domain_id: The ID of the domain to
|
||||||
|
authenticate a user against.
|
||||||
|
:param user_domain_name: The name of the domain to
|
||||||
|
authenticate a user against.
|
||||||
|
|
||||||
|
"""
|
||||||
|
super(RequestContext, self).__init__(auth_token=auth_token,
|
||||||
|
user_id=user_name,
|
||||||
|
project_id=project_name,
|
||||||
|
is_admin=is_admin,
|
||||||
|
read_only=read_only,
|
||||||
|
show_deleted=show_deleted,
|
||||||
|
request_id=request_id,
|
||||||
|
roles=roles)
|
||||||
|
|
||||||
|
self.user_name = user_name
|
||||||
|
self.user_id = user_id
|
||||||
|
self.project_name = project_name
|
||||||
|
self.project_id = project_id
|
||||||
|
self.domain_id = domain_id
|
||||||
|
self.domain_name = domain_name
|
||||||
|
self.user_domain_id = user_domain_id
|
||||||
|
self.user_domain_name = user_domain_name
|
||||||
|
self.auth_token_info = auth_token_info
|
||||||
|
self.trust_id = trust_id
|
||||||
|
self.all_projects = all_projects
|
||||||
|
self.password = password
|
||||||
|
if is_admin is None:
|
||||||
|
self.is_admin = policy.check_is_admin(self)
|
||||||
|
else:
|
||||||
|
self.is_admin = is_admin
|
||||||
|
|
||||||
|
if not timestamp:
|
||||||
|
timestamp = timeutils.utcnow()
|
||||||
|
if isinstance(timestamp, six.string_types):
|
||||||
|
timestamp = timeutils.parse_strtime(timestamp)
|
||||||
|
self.timestamp = timestamp
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
value = super(RequestContext, self).to_dict()
|
||||||
|
value.update({'auth_token': self.auth_token,
|
||||||
|
'domain_id': self.domain_id,
|
||||||
|
'domain_name': self.domain_name,
|
||||||
|
'user_domain_id': self.user_domain_id,
|
||||||
|
'user_domain_name': self.user_domain_name,
|
||||||
|
'user_name': self.user_name,
|
||||||
|
'user_id': self.user_id,
|
||||||
|
'project_name': self.project_name,
|
||||||
|
'project_id': self.project_id,
|
||||||
|
'is_admin': self.is_admin,
|
||||||
|
'read_only': self.read_only,
|
||||||
|
'roles': self.roles,
|
||||||
|
'show_deleted': self.show_deleted,
|
||||||
|
'request_id': self.request_id,
|
||||||
|
'trust_id': self.trust_id,
|
||||||
|
'auth_token_info': self.auth_token_info,
|
||||||
|
'password': self.password,
|
||||||
|
'all_projects': self.all_projects,
|
||||||
|
'timestamp': timeutils.strtime(self.timestamp) if
|
||||||
|
hasattr(self, 'timestamp') else None
|
||||||
|
})
|
||||||
|
return value
|
||||||
|
|
||||||
|
def to_policy_values(self):
|
||||||
|
policy = super(RequestContext, self).to_policy_values()
|
||||||
|
policy['is_admin'] = self.is_admin
|
||||||
|
return policy
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, values):
|
||||||
|
return cls(**values)
|
||||||
|
|
||||||
|
def elevated(self):
|
||||||
|
"""Return a version of this context with admin flag set."""
|
||||||
|
context = copy.copy(self)
|
||||||
|
# context.roles must be deepcopied to leave original roles
|
||||||
|
# without changes
|
||||||
|
context.roles = copy.deepcopy(self.roles)
|
||||||
|
context.is_admin = True
|
||||||
|
|
||||||
|
if 'admin' not in context.roles:
|
||||||
|
context.roles.append('admin')
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
def can(self, action, target=None, fatal=True, might_not_exist=False):
|
||||||
|
"""Verifies that the given action is valid on the target in this context.
|
||||||
|
|
||||||
|
:param action: string representing the action to be checked.
|
||||||
|
:param target: dictionary representing the object of the action
|
||||||
|
for object creation this should be a dictionary representing the
|
||||||
|
location of the object e.g. ``{'project_id': context.project_id}``.
|
||||||
|
If None, then this default target will be considered:
|
||||||
|
{'project_id': self.project_id, 'user_id': self.user_id}
|
||||||
|
:param fatal: if False, will return False when an
|
||||||
|
exception.NotAuthorized occurs.
|
||||||
|
:param might_not_exist: If True the policy check is skipped (and the
|
||||||
|
function returns True) if the specified policy does not exist.
|
||||||
|
Defaults to false.
|
||||||
|
|
||||||
|
:raises gyan.common.exception.NotAuthorized: if verification fails and
|
||||||
|
fatal is True.
|
||||||
|
|
||||||
|
:return: returns a non-False value (not necessarily "True") if
|
||||||
|
authorized and False if not authorized and fatal is False.
|
||||||
|
"""
|
||||||
|
if target is None:
|
||||||
|
target = {'project_id': self.project_id,
|
||||||
|
'user_id': self.user_id}
|
||||||
|
|
||||||
|
try:
|
||||||
|
return policy.authorize(self, action, target,
|
||||||
|
might_not_exist=might_not_exist)
|
||||||
|
except exception.NotAuthorized:
|
||||||
|
if fatal:
|
||||||
|
raise
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def make_context(*args, **kwargs):
|
||||||
|
return RequestContext(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def get_admin_context(show_deleted=False, all_projects=False):
|
||||||
|
"""Create an administrator context.
|
||||||
|
|
||||||
|
:param show_deleted: if True, will show deleted items when query db
|
||||||
|
"""
|
||||||
|
context = RequestContext(user_id=None,
|
||||||
|
project=None,
|
||||||
|
is_admin=True,
|
||||||
|
show_deleted=show_deleted,
|
||||||
|
all_projects=all_projects)
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
def set_context(func):
|
||||||
|
@functools.wraps(func)
|
||||||
|
def handler(self, ctx):
|
||||||
|
if ctx is None:
|
||||||
|
ctx = get_admin_context(all_projects=True)
|
||||||
|
func(self, ctx)
|
||||||
|
return handler
|
485
gyan/common/exception.py
Normal file
485
gyan/common/exception.py
Normal file
@ -0,0 +1,485 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
"""Gyan base exception handling.
|
||||||
|
|
||||||
|
Includes decorator for re-raising Gyan-type exceptions.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import functools
|
||||||
|
import inspect
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from keystoneclient import exceptions as keystone_exceptions
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log as logging
|
||||||
|
from oslo_utils import excutils
|
||||||
|
from oslo_utils import uuidutils
|
||||||
|
import pecan
|
||||||
|
import six
|
||||||
|
from webob import util as woutil
|
||||||
|
|
||||||
|
from gyan.common.i18n import _
|
||||||
|
import gyan.conf
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CONF = gyan.conf.CONF
|
||||||
|
|
||||||
|
try:
|
||||||
|
CONF.import_opt('fatal_exception_format_errors',
|
||||||
|
'oslo_versionedobjects.exception')
|
||||||
|
except cfg.NoSuchOptError as e:
|
||||||
|
# Note:work around for gyan run against master branch
|
||||||
|
# in devstack gate job, as gyan not branched yet
|
||||||
|
# versionobjects kilo/master different version can
|
||||||
|
# cause issue here. As it changed import group. So
|
||||||
|
# add here before branch to prevent gate failure.
|
||||||
|
# Bug: #1447873
|
||||||
|
CONF.import_opt('fatal_exception_format_errors',
|
||||||
|
'oslo_versionedobjects.exception',
|
||||||
|
group='oslo_versionedobjects')
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_exception(notifier=None, event_type=None):
|
||||||
|
"""This decorator wraps a method to catch any exceptions.
|
||||||
|
|
||||||
|
It logs the exception as well as optionally sending
|
||||||
|
it to the notification system.
|
||||||
|
"""
|
||||||
|
def inner(f):
|
||||||
|
def wrapped(self, context, *args, **kwargs):
|
||||||
|
# Don't store self or context in the payload, it now seems to
|
||||||
|
# contain confidential information.
|
||||||
|
try:
|
||||||
|
return f(self, context, *args, **kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
if notifier:
|
||||||
|
call_dict = inspect.getcallargs(f, self, context,
|
||||||
|
*args, **kwargs)
|
||||||
|
payload = dict(exception=e,
|
||||||
|
private=dict(args=call_dict)
|
||||||
|
)
|
||||||
|
|
||||||
|
temp_type = event_type
|
||||||
|
if not temp_type:
|
||||||
|
# If f has multiple decorators, they must use
|
||||||
|
# functools.wraps to ensure the name is
|
||||||
|
# propagated.
|
||||||
|
temp_type = f.__name__
|
||||||
|
|
||||||
|
notifier.error(context, temp_type, payload)
|
||||||
|
|
||||||
|
return functools.wraps(f)(wrapped)
|
||||||
|
return inner
|
||||||
|
|
||||||
|
|
||||||
|
OBFUSCATED_MSG = _('Your request could not be handled '
|
||||||
|
'because of a problem in the server. '
|
||||||
|
'Error Correlation id is: %s')
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_controller_exception(func, func_server_error, func_client_error):
|
||||||
|
"""This decorator wraps controllers methods to handle exceptions:
|
||||||
|
|
||||||
|
- if an unhandled Exception or a GyanException with an error code >=500
|
||||||
|
is catched, raise a http 5xx ClientSideError and correlates it with a log
|
||||||
|
message
|
||||||
|
|
||||||
|
- if a GyanException is catched and its error code is <500, raise a http
|
||||||
|
4xx and logs the excp in debug mode
|
||||||
|
|
||||||
|
"""
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapped(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
except Exception as excp:
|
||||||
|
if isinstance(excp, GyanException):
|
||||||
|
http_error_code = excp.code
|
||||||
|
else:
|
||||||
|
http_error_code = 500
|
||||||
|
|
||||||
|
if http_error_code >= 500:
|
||||||
|
# log the error message with its associated
|
||||||
|
# correlation id
|
||||||
|
log_correlation_id = uuidutils.generate_uuid()
|
||||||
|
LOG.exception("%(correlation_id)s:%(excp)s",
|
||||||
|
{'correlation_id': log_correlation_id,
|
||||||
|
'excp': str(excp)})
|
||||||
|
# raise a client error with an obfuscated message
|
||||||
|
return func_server_error(log_correlation_id, http_error_code)
|
||||||
|
else:
|
||||||
|
# raise a client error the original message
|
||||||
|
LOG.debug(excp)
|
||||||
|
return func_client_error(excp, http_error_code)
|
||||||
|
return wrapped
|
||||||
|
|
||||||
|
|
||||||
|
def convert_excp_to_err_code(excp_name):
|
||||||
|
"""Convert Exception class name (CamelCase) to error-code (Snake-case)"""
|
||||||
|
words = re.findall(r'[A-Z]?[a-z]+|[A-Z]{2,}(?=[A-Z][a-z]|\d|\W|$)|\d+',
|
||||||
|
excp_name)
|
||||||
|
return '-'.join([str.lower(word) for word in words])
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_pecan_controller_exception(func):
|
||||||
|
"""This decorator wraps pecan controllers to handle exceptions."""
|
||||||
|
def _func_server_error(log_correlation_id, status_code):
|
||||||
|
pecan.response.status = status_code
|
||||||
|
return {
|
||||||
|
'faultcode': 'Server',
|
||||||
|
'status_code': status_code,
|
||||||
|
'title': woutil.status_reasons[status_code],
|
||||||
|
'description': six.text_type(OBFUSCATED_MSG % log_correlation_id),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _func_client_error(excp, status_code):
|
||||||
|
pecan.response.status = status_code
|
||||||
|
return {
|
||||||
|
'faultcode': 'Client',
|
||||||
|
'faultstring': convert_excp_to_err_code(excp.__class__.__name__),
|
||||||
|
'status_code': status_code,
|
||||||
|
'title': six.text_type(excp),
|
||||||
|
'description': six.text_type(excp),
|
||||||
|
}
|
||||||
|
|
||||||
|
return wrap_controller_exception(func,
|
||||||
|
_func_server_error,
|
||||||
|
_func_client_error)
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_keystone_exception(func):
|
||||||
|
"""Wrap keystone exceptions and throw gyan specific exceptions."""
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapped(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
except keystone_exceptions.AuthorizationFailure:
|
||||||
|
raise AuthorizationFailure(
|
||||||
|
client=func.__name__, message="reason: %s" % sys.exc_info()[1])
|
||||||
|
except keystone_exceptions.ClientException:
|
||||||
|
raise AuthorizationFailure(
|
||||||
|
client=func.__name__,
|
||||||
|
message="unexpected keystone client error occurred: %s"
|
||||||
|
% sys.exc_info()[1])
|
||||||
|
return wrapped
|
||||||
|
|
||||||
|
|
||||||
|
class GyanException(Exception):
|
||||||
|
"""Base gyan Exception
|
||||||
|
|
||||||
|
To correctly use this class, inherit from it and define
|
||||||
|
a 'message' property. That message will get printf'd
|
||||||
|
with the keyword arguments provided to the constructor.
|
||||||
|
|
||||||
|
"""
|
||||||
|
message = _("An unknown exception occurred.")
|
||||||
|
code = 500
|
||||||
|
|
||||||
|
def __init__(self, message=None, **kwargs):
|
||||||
|
self.kwargs = kwargs
|
||||||
|
|
||||||
|
if 'code' not in self.kwargs and hasattr(self, 'code'):
|
||||||
|
self.kwargs['code'] = self.code
|
||||||
|
|
||||||
|
if message:
|
||||||
|
self.message = message
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.message = str(self.message) % kwargs
|
||||||
|
except KeyError:
|
||||||
|
# kwargs doesn't match a variable in the message
|
||||||
|
# log the issue and the kwargs
|
||||||
|
LOG.exception('Exception in string format operation, '
|
||||||
|
'kwargs: %s', kwargs)
|
||||||
|
try:
|
||||||
|
ferr = CONF.fatal_exception_format_errors
|
||||||
|
except cfg.NoSuchOptError:
|
||||||
|
ferr = CONF.oslo_versionedobjects.fatal_exception_format_errors
|
||||||
|
if ferr:
|
||||||
|
raise
|
||||||
|
|
||||||
|
super(GyanException, self).__init__(self.message)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if six.PY3:
|
||||||
|
return self.message
|
||||||
|
return self.message.encode('utf-8')
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return self.message
|
||||||
|
|
||||||
|
def format_message(self):
|
||||||
|
if self.__class__.__name__.endswith('_Remote'):
|
||||||
|
return self.args[0]
|
||||||
|
else:
|
||||||
|
return six.text_type(self)
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectNotFound(GyanException):
|
||||||
|
message = _("The %(name)s %(id)s could not be found.")
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectNotUnique(GyanException):
|
||||||
|
message = _("The %(name)s already exists.")
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectActionError(GyanException):
|
||||||
|
message = _('Object action %(action)s failed because: %(reason)s')
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceNotFound(ObjectNotFound):
|
||||||
|
message = _("The %(name)s resource %(id)s could not be found.")
|
||||||
|
code = 404
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceExists(ObjectNotUnique):
|
||||||
|
message = _("The %(name)s resource already exists.")
|
||||||
|
code = 409
|
||||||
|
|
||||||
|
|
||||||
|
class AuthorizationFailure(GyanException):
|
||||||
|
message = _("%(client)s connection failed. %(message)s")
|
||||||
|
|
||||||
|
|
||||||
|
class UnsupportedObjectError(GyanException):
|
||||||
|
message = _('Unsupported object type %(objtype)s')
|
||||||
|
|
||||||
|
|
||||||
|
class IncompatibleObjectVersion(GyanException):
|
||||||
|
message = _('Version %(objver)s of %(objname)s is not supported')
|
||||||
|
|
||||||
|
|
||||||
|
class OrphanedObjectError(GyanException):
|
||||||
|
message = _('Cannot call %(method)s on orphaned %(objtype)s object')
|
||||||
|
|
||||||
|
|
||||||
|
class Invalid(GyanException):
|
||||||
|
message = _("Unacceptable parameters.")
|
||||||
|
code = 400
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidValue(Invalid):
|
||||||
|
message = _("Received value '%(value)s' is invalid for type %(type)s.")
|
||||||
|
|
||||||
|
|
||||||
|
class ValidationError(Invalid):
|
||||||
|
message = "%(detail)s"
|
||||||
|
|
||||||
|
|
||||||
|
class SchemaValidationError(ValidationError):
|
||||||
|
message = "%(detail)s"
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidUUID(Invalid):
|
||||||
|
message = _("Expected a uuid but received %(uuid)s.")
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidName(Invalid):
|
||||||
|
message = _("Expected a name but received %(uuid)s.")
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidDiscoveryURL(Invalid):
|
||||||
|
message = _("Received invalid discovery URL '%(discovery_url)s' for "
|
||||||
|
"discovery endpoint '%(discovery_endpoint)s'.")
|
||||||
|
|
||||||
|
|
||||||
|
class GetDiscoveryUrlFailed(GyanException):
|
||||||
|
message = _("Failed to get discovery url from '%(discovery_endpoint)s'.")
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidUuidOrName(Invalid):
|
||||||
|
message = _("Expected a name or uuid but received %(uuid)s.")
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidIdentity(Invalid):
|
||||||
|
message = _("Expected an uuid or int but received %(identity)s.")
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidCsr(Invalid):
|
||||||
|
message = _("Received invalid csr %(csr)s.")
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPNotFound(ResourceNotFound):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Conflict(GyanException):
|
||||||
|
message = _('Conflict.')
|
||||||
|
code = 409
|
||||||
|
|
||||||
|
|
||||||
|
class ConflictOptions(Conflict):
|
||||||
|
message = _('Conflicting options.')
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidState(Conflict):
|
||||||
|
message = _("Invalid resource state.")
|
||||||
|
|
||||||
|
|
||||||
|
# Cannot be templated as the error syntax varies.
|
||||||
|
# msg needs to be constructed when raised.
|
||||||
|
class InvalidParameterValue(Invalid):
|
||||||
|
message = _("%(err)s")
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidParamInVersion(Invalid):
|
||||||
|
message = _('Invalid param %(param)s because current request '
|
||||||
|
'version is %(req_version)s. %(param)s is only '
|
||||||
|
'supported from version %(min_version)s')
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidQuotaValue(Invalid):
|
||||||
|
message = _("Change would make usage less than 0 for the following "
|
||||||
|
"resources: %(unders)s")
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidQuotaMethodUsage(Invalid):
|
||||||
|
message = _("Wrong quota method %(method)s used on resource %(res)s")
|
||||||
|
|
||||||
|
|
||||||
|
class PatchError(Invalid):
|
||||||
|
message = _("Couldn't apply patch '%(patch)s'. Reason: %(reason)s")
|
||||||
|
|
||||||
|
|
||||||
|
class NotAuthorized(GyanException):
|
||||||
|
message = _("Not authorized.")
|
||||||
|
code = 403
|
||||||
|
|
||||||
|
|
||||||
|
class MLModelAlreadyExists(GyanException):
|
||||||
|
message = _("A ML Model with %(field)s %(value)s already exists.")
|
||||||
|
|
||||||
|
|
||||||
|
class MLModelNotFound(GyanException):
|
||||||
|
message = _("ML Model %(ml_model)s could not be found.")
|
||||||
|
|
||||||
|
class ConfigInvalid(GyanException):
|
||||||
|
message = _("Invalid configuration file. %(error_msg)s")
|
||||||
|
|
||||||
|
|
||||||
|
class PolicyNotAuthorized(NotAuthorized):
|
||||||
|
message = _("Policy doesn't allow %(action)s to be performed.")
|
||||||
|
|
||||||
|
|
||||||
|
class ComputeHostNotFound(HTTPNotFound):
|
||||||
|
message = _("Compute host %(compute_host)s could not be found.")
|
||||||
|
|
||||||
|
|
||||||
|
class GyanServiceNotFound(HTTPNotFound):
|
||||||
|
message = _("Gyan service %(binary)s on host %(host)s could not be found.")
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceProviderNotFound(HTTPNotFound):
|
||||||
|
message = _("Resource provider %(resource_provider)s could not be found.")
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceClassNotFound(HTTPNotFound):
|
||||||
|
message = _("Resource class %(resource_class)s could not be found.")
|
||||||
|
|
||||||
|
|
||||||
|
class ComputeHostAlreadyExists(ResourceExists):
|
||||||
|
message = _("A compute host with %(field)s %(value)s already exists.")
|
||||||
|
|
||||||
|
|
||||||
|
class GyanServiceAlreadyExists(ResourceExists):
|
||||||
|
message = _("Service %(binary)s on host %(host)s already exists.")
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceProviderAlreadyExists(ResourceExists):
|
||||||
|
message = _("A resource provider with %(field)s %(value)s already exists.")
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceClassAlreadyExists(ResourceExists):
|
||||||
|
message = _("A resource class with %(field)s %(value)s already exists.")
|
||||||
|
|
||||||
|
|
||||||
|
class UniqueConstraintViolated(ResourceExists):
|
||||||
|
message = _("A resource with %(fields)s violates unique constraint.")
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidStateException(GyanException):
|
||||||
|
message = _("Cannot %(action)s ml model %(id)s in %(actual_state)s state")
|
||||||
|
code = 409
|
||||||
|
|
||||||
|
|
||||||
|
class ServerInError(GyanException):
|
||||||
|
message = _('Went to status %(resource_status)s due to '
|
||||||
|
'"%(status_reason)s"')
|
||||||
|
|
||||||
|
|
||||||
|
class ServerUnknownStatus(GyanException):
|
||||||
|
message = _('%(result)s - Unknown status %(resource_status)s due to '
|
||||||
|
'"%(status_reason)s"')
|
||||||
|
|
||||||
|
|
||||||
|
class EntityNotFound(GyanException):
|
||||||
|
message = _("The %(entity)s (%(name)s) could not be found.")
|
||||||
|
|
||||||
|
|
||||||
|
class CommandError(GyanException):
|
||||||
|
message = _("The command: %(cmd)s failed on the system, due to %(error)s")
|
||||||
|
|
||||||
|
|
||||||
|
class NoValidHost(GyanException):
|
||||||
|
message = _("No valid host was found. %(reason)s")
|
||||||
|
|
||||||
|
|
||||||
|
class NotFound(GyanException):
|
||||||
|
message = _("Resource could not be found.")
|
||||||
|
code = 404
|
||||||
|
|
||||||
|
|
||||||
|
class SchedulerHostFilterNotFound(NotFound):
|
||||||
|
message = _("Scheduler Host Filter %(filter_name)s could not be found.")
|
||||||
|
|
||||||
|
|
||||||
|
class ClassNotFound(NotFound):
|
||||||
|
message = _("Class %(class_name)s could not be found: %(exception)s")
|
||||||
|
|
||||||
|
|
||||||
|
class ApiVersionsIntersect(GyanException):
|
||||||
|
message = _("Version of %(name)s %(min_ver)s %(max_ver)s intersects "
|
||||||
|
"with another versions.")
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionFailed(GyanException):
|
||||||
|
message = _("Failed to connect to remote host")
|
||||||
|
|
||||||
|
|
||||||
|
class SocketException(GyanException):
|
||||||
|
message = _("Socket exceptions")
|
||||||
|
|
||||||
|
|
||||||
|
class ResourcesUnavailable(GyanException):
|
||||||
|
message = _("Insufficient compute resources: %(reason)s.")
|
||||||
|
|
||||||
|
|
||||||
|
class FileNotFound(GyanException):
|
||||||
|
message = _("The expected file not exist")
|
||||||
|
|
||||||
|
|
||||||
|
class FailedParseStringToJson(GyanException):
|
||||||
|
message = _("Failed parse string to json: %(reason)s.")
|
||||||
|
|
||||||
|
|
||||||
|
class ServerNotUsable(GyanException):
|
||||||
|
message = _("gyan server not usable")
|
||||||
|
code = 404
|
||||||
|
|
||||||
|
|
||||||
|
class Base64Exception(Invalid):
|
||||||
|
msg_fmt = _("Invalid Base 64 file data")
|
24
gyan/common/i18n.py
Normal file
24
gyan/common/i18n.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
# It's based on oslo.i18n usage in OpenStack Keystone project and
|
||||||
|
# recommendations from
|
||||||
|
# https://docs.openstack.org/oslo.i18n/latest/user/usage.html
|
||||||
|
|
||||||
|
import oslo_i18n
|
||||||
|
|
||||||
|
|
||||||
|
_translators = oslo_i18n.TranslatorFactory(domain='gyan')
|
||||||
|
|
||||||
|
# The primary translation function using the well-known name "_"
|
||||||
|
_ = _translators.primary
|
89
gyan/common/keystone.py
Normal file
89
gyan/common/keystone.py
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
# 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 keystoneauth1.access import access as ka_access
|
||||||
|
from keystoneauth1.identity import access as ka_access_plugin
|
||||||
|
from keystoneauth1.identity import v3 as ka_v3
|
||||||
|
from keystoneauth1 import loading as ka_loading
|
||||||
|
from keystoneclient.v3 import client as kc_v3
|
||||||
|
from oslo_log import log as logging
|
||||||
|
|
||||||
|
from gyan.common import exception
|
||||||
|
import gyan.conf
|
||||||
|
from gyan.conf import keystone as ksconf
|
||||||
|
|
||||||
|
|
||||||
|
CONF = gyan.conf.CONF
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class KeystoneClientV3(object):
|
||||||
|
"""Keystone client wrapper so we can encapsulate logic in one place."""
|
||||||
|
|
||||||
|
def __init__(self, context):
|
||||||
|
self.context = context
|
||||||
|
self._client = None
|
||||||
|
self._session = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def auth_url(self):
|
||||||
|
# FIXME(pauloewerton): auth_url should be retrieved from keystone_auth
|
||||||
|
# section by default
|
||||||
|
url = CONF[ksconf.CFG_LEGACY_GROUP].www_authenticate_uri or \
|
||||||
|
CONF[ksconf.CFG_LEGACY_GROUP].auth_uri
|
||||||
|
return url.replace('v2.0', 'v3')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def auth_token(self):
|
||||||
|
return self.session.get_token()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def session(self):
|
||||||
|
if self._session:
|
||||||
|
return self._session
|
||||||
|
auth = self._get_auth()
|
||||||
|
session = self._get_session(auth)
|
||||||
|
self._session = session
|
||||||
|
return session
|
||||||
|
|
||||||
|
def _get_session(self, auth):
|
||||||
|
session = ka_loading.load_session_from_conf_options(
|
||||||
|
CONF, ksconf.CFG_GROUP, auth=auth)
|
||||||
|
return session
|
||||||
|
|
||||||
|
def _get_auth(self):
|
||||||
|
if self.context.auth_token_info:
|
||||||
|
access_info = ka_access.create(body=self.context.auth_token_info,
|
||||||
|
auth_token=self.context.auth_token)
|
||||||
|
auth = ka_access_plugin.AccessInfoPlugin(access_info)
|
||||||
|
elif self.context.auth_token:
|
||||||
|
auth = ka_v3.Token(auth_url=self.auth_url,
|
||||||
|
token=self.context.auth_token)
|
||||||
|
elif self.context.is_admin:
|
||||||
|
auth = ka_loading.load_auth_from_conf_options(CONF,
|
||||||
|
ksconf.CFG_GROUP)
|
||||||
|
else:
|
||||||
|
msg = ('Keystone API connection failed: no password, '
|
||||||
|
'trust_id or token found.')
|
||||||
|
LOG.error(msg)
|
||||||
|
raise exception.AuthorizationFailure(client='keystone',
|
||||||
|
message='reason %s' % msg)
|
||||||
|
|
||||||
|
return auth
|
||||||
|
|
||||||
|
@property
|
||||||
|
def client(self):
|
||||||
|
if self._client:
|
||||||
|
return self._client
|
||||||
|
client = kc_v3.Client(session=self.session)
|
||||||
|
self._client = client
|
||||||
|
return client
|
47
gyan/common/paths.py
Normal file
47
gyan/common/paths.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# 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 os
|
||||||
|
|
||||||
|
import gyan.conf
|
||||||
|
|
||||||
|
CONF = gyan.conf.CONF
|
||||||
|
|
||||||
|
|
||||||
|
def basedir_def(*args):
|
||||||
|
"""Return an uninterpolated path relative to $pybasedir."""
|
||||||
|
return os.path.join('$pybasedir', *args)
|
||||||
|
|
||||||
|
|
||||||
|
def bindir_def(*args):
|
||||||
|
"""Return an uninterpolated path relative to $bindir."""
|
||||||
|
return os.path.join('$bindir', *args)
|
||||||
|
|
||||||
|
|
||||||
|
def state_path_def(*args):
|
||||||
|
"""Return an uninterpolated path relative to $state_path."""
|
||||||
|
return os.path.join('$state_path', *args)
|
||||||
|
|
||||||
|
|
||||||
|
def basedir_rel(*args):
|
||||||
|
"""Return a path relative to $pybasedir."""
|
||||||
|
return os.path.join(CONF.pybasedir, *args)
|
||||||
|
|
||||||
|
|
||||||
|
def bindir_rel(*args):
|
||||||
|
"""Return a path relative to $bindir."""
|
||||||
|
return os.path.join(CONF.bindir, *args)
|
||||||
|
|
||||||
|
|
||||||
|
def state_path_rel(*args):
|
||||||
|
"""Return a path relative to $state_path."""
|
||||||
|
return os.path.join(CONF.state_path, *args)
|
24
gyan/common/policies/__init__.py
Normal file
24
gyan/common/policies/__init__.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# 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 itertools
|
||||||
|
|
||||||
|
from gyan.common.policies import host
|
||||||
|
from gyan.common.policies import base
|
||||||
|
from gyan.common.policies import ml_model
|
||||||
|
|
||||||
|
def list_rules():
|
||||||
|
return itertools.chain(
|
||||||
|
base.list_rules(),
|
||||||
|
host.list_rules(),
|
||||||
|
ml_model.list_rules()
|
||||||
|
)
|
41
gyan/common/policies/base.py
Normal file
41
gyan/common/policies/base.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# 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_policy import policy
|
||||||
|
|
||||||
|
ROLE_ADMIN = 'role:admin'
|
||||||
|
RULE_ADMIN_OR_OWNER = 'is_admin:True or project_id:%(project_id)s'
|
||||||
|
RULE_ADMIN_API = 'rule:context_is_admin'
|
||||||
|
RULE_DENY_EVERYBODY = 'rule:deny_everybody'
|
||||||
|
|
||||||
|
rules = [
|
||||||
|
policy.RuleDefault(
|
||||||
|
name='context_is_admin',
|
||||||
|
check_str=ROLE_ADMIN
|
||||||
|
),
|
||||||
|
policy.RuleDefault(
|
||||||
|
name='admin_or_owner',
|
||||||
|
check_str=RULE_ADMIN_OR_OWNER
|
||||||
|
),
|
||||||
|
policy.RuleDefault(
|
||||||
|
name='admin_api',
|
||||||
|
check_str=RULE_ADMIN_API
|
||||||
|
),
|
||||||
|
policy.RuleDefault(
|
||||||
|
name="deny_everybody",
|
||||||
|
check_str="!",
|
||||||
|
description="Default rule for deny everybody."),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def list_rules():
|
||||||
|
return rules
|
46
gyan/common/policies/host.py
Normal file
46
gyan/common/policies/host.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# 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_policy import policy
|
||||||
|
|
||||||
|
from gyan.common.policies import base
|
||||||
|
|
||||||
|
HOST = 'host:%s'
|
||||||
|
|
||||||
|
rules = [
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
name=HOST % 'get_all',
|
||||||
|
check_str=base.RULE_ADMIN_API,
|
||||||
|
description='List all compute hosts.',
|
||||||
|
operations=[
|
||||||
|
{
|
||||||
|
'path': '/v1/hosts',
|
||||||
|
'method': 'GET'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
name=HOST % 'get',
|
||||||
|
check_str=base.RULE_ADMIN_API,
|
||||||
|
description='Show the details of a specific compute host.',
|
||||||
|
operations=[
|
||||||
|
{
|
||||||
|
'path': '/v1/hosts/{host_ident}',
|
||||||
|
'method': 'GET'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def list_rules():
|
||||||
|
return rules
|
123
gyan/common/policies/ml_model.py
Normal file
123
gyan/common/policies/ml_model.py
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
# 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_policy import policy
|
||||||
|
|
||||||
|
from gyan.common.policies import base
|
||||||
|
|
||||||
|
ML_MODEL = 'ml_model:%s'
|
||||||
|
|
||||||
|
rules = [
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
name=ML_MODEL % 'create',
|
||||||
|
check_str=base.RULE_ADMIN_OR_OWNER,
|
||||||
|
description='Create a new ML Model.',
|
||||||
|
operations=[
|
||||||
|
{
|
||||||
|
'path': '/v1/ml_models',
|
||||||
|
'method': 'POST'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
name=ML_MODEL % 'delete',
|
||||||
|
check_str=base.RULE_ADMIN_OR_OWNER,
|
||||||
|
description='Delete a ML Model.',
|
||||||
|
operations=[
|
||||||
|
{
|
||||||
|
'path': '/v1/ml_models/{ml_model_ident}',
|
||||||
|
'method': 'DELETE'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
name=ML_MODEL % 'delete_all_projects',
|
||||||
|
check_str=base.RULE_ADMIN_API,
|
||||||
|
description='Delete a ml models from all projects.',
|
||||||
|
operations=[
|
||||||
|
{
|
||||||
|
'path': '/v1/ml_models/{ml_model_ident}',
|
||||||
|
'method': 'DELETE'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
name=ML_MODEL % 'delete_force',
|
||||||
|
check_str=base.RULE_ADMIN_API,
|
||||||
|
description='Forcibly delete a ML model.',
|
||||||
|
operations=[
|
||||||
|
{
|
||||||
|
'path': '/v1/ml_models/{ml_model_ident}',
|
||||||
|
'method': 'DELETE'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
name=ML_MODEL % 'get_one',
|
||||||
|
check_str=base.RULE_ADMIN_OR_OWNER,
|
||||||
|
description='Retrieve the details of a specific ml model.',
|
||||||
|
operations=[
|
||||||
|
{
|
||||||
|
'path': '/v1/ml_models/{ml_model_ident}',
|
||||||
|
'method': 'GET'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
name=ML_MODEL % 'get_all',
|
||||||
|
check_str=base.RULE_ADMIN_OR_OWNER,
|
||||||
|
description='Retrieve the details of all ml models.',
|
||||||
|
operations=[
|
||||||
|
{
|
||||||
|
'path': '/v1/ml_models',
|
||||||
|
'method': 'GET'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
name=ML_MODEL % 'get_all_all_projects',
|
||||||
|
check_str=base.RULE_ADMIN_API,
|
||||||
|
description='Retrieve the details of all ml models across projects.',
|
||||||
|
operations=[
|
||||||
|
{
|
||||||
|
'path': '/v1/ml_models',
|
||||||
|
'method': 'GET'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
name=ML_MODEL % 'update',
|
||||||
|
check_str=base.RULE_ADMIN_OR_OWNER,
|
||||||
|
description='Update a ML Model.',
|
||||||
|
operations=[
|
||||||
|
{
|
||||||
|
'path': '/v1/ml_models/{ml_model_ident}',
|
||||||
|
'method': 'PATCH'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
name=ML_MODEL % 'upload',
|
||||||
|
check_str=base.RULE_ADMIN_OR_OWNER,
|
||||||
|
description='Upload the trained ML Model',
|
||||||
|
operations=[
|
||||||
|
{
|
||||||
|
'path': '/v1/ml_models/{ml_model_ident}/upload',
|
||||||
|
'method': 'POST'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def list_rules():
|
||||||
|
return rules
|
155
gyan/common/policy.py
Normal file
155
gyan/common/policy.py
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
"""Policy Engine For Gyan."""
|
||||||
|
|
||||||
|
from oslo_log import log as logging
|
||||||
|
from oslo_policy import policy
|
||||||
|
from oslo_utils import excutils
|
||||||
|
|
||||||
|
from gyan.common import exception
|
||||||
|
from gyan.common import policies
|
||||||
|
import gyan.conf
|
||||||
|
|
||||||
|
_ENFORCER = None
|
||||||
|
CONF = gyan.conf.CONF
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# we can get a policy enforcer by this init.
|
||||||
|
# oslo policy support change policy rule dynamically.
|
||||||
|
# at present, policy.enforce will reload the policy rules when it checks
|
||||||
|
# the policy files have been touched.
|
||||||
|
def init(policy_file=None, rules=None,
|
||||||
|
default_rule=None, use_conf=True, overwrite=True):
|
||||||
|
"""Init an Enforcer class.
|
||||||
|
|
||||||
|
:param policy_file: Custom policy file to use, if none is
|
||||||
|
specified, ``conf.policy_file`` will be
|
||||||
|
used.
|
||||||
|
:param rules: Default dictionary / Rules to use. It will be
|
||||||
|
considered just in the first instantiation. If
|
||||||
|
:meth:`load_rules` with ``force_reload=True``,
|
||||||
|
:meth:`clear` or :meth:`set_rules` with
|
||||||
|
``overwrite=True`` is called this will be overwritten.
|
||||||
|
:param default_rule: Default rule to use, conf.default_rule will
|
||||||
|
be used if none is specified.
|
||||||
|
:param use_conf: Whether to load rules from cache or config file.
|
||||||
|
:param overwrite: Whether to overwrite existing rules when reload rules
|
||||||
|
from config file.
|
||||||
|
"""
|
||||||
|
global _ENFORCER
|
||||||
|
if not _ENFORCER:
|
||||||
|
# https://docs.openstack.org/oslo.policy/latest/user/usage.html
|
||||||
|
_ENFORCER = policy.Enforcer(CONF,
|
||||||
|
policy_file=policy_file,
|
||||||
|
rules=rules,
|
||||||
|
default_rule=default_rule,
|
||||||
|
use_conf=use_conf,
|
||||||
|
overwrite=overwrite)
|
||||||
|
register_rules(_ENFORCER)
|
||||||
|
return _ENFORCER
|
||||||
|
|
||||||
|
|
||||||
|
def register_rules(enforcer):
|
||||||
|
enforcer.register_defaults(policies.list_rules())
|
||||||
|
|
||||||
|
|
||||||
|
def enforce(context, rule=None, target=None,
|
||||||
|
do_raise=True, exc=None, *args, **kwargs):
|
||||||
|
|
||||||
|
"""Checks authorization of a rule against the target and credentials.
|
||||||
|
|
||||||
|
:param dict context: As much information about the user performing the
|
||||||
|
action as possible.
|
||||||
|
:param rule: The rule to evaluate.
|
||||||
|
:param dict target: As much information about the object being operated
|
||||||
|
on as possible.
|
||||||
|
:param do_raise: Whether to raise an exception or not if check
|
||||||
|
fails.
|
||||||
|
:param exc: Class of the exception to raise if the check fails.
|
||||||
|
Any remaining arguments passed to :meth:`enforce` (both
|
||||||
|
positional and keyword arguments) will be passed to
|
||||||
|
the exception class. If not specified,
|
||||||
|
:class:`PolicyNotAuthorized` will be used.
|
||||||
|
|
||||||
|
:return: ``False`` if the policy does not allow the action and `exc` is
|
||||||
|
not provided; otherwise, returns a value that evaluates to
|
||||||
|
``True``. Note: for rules using the "case" expression, this
|
||||||
|
``True`` value will be the specified string from the
|
||||||
|
expression.
|
||||||
|
"""
|
||||||
|
enforcer = init()
|
||||||
|
credentials = context.to_policy_values()
|
||||||
|
if not exc:
|
||||||
|
exc = exception.PolicyNotAuthorized
|
||||||
|
if target is None:
|
||||||
|
target = {'project_id': context.project_id,
|
||||||
|
'user_id': context.user_id}
|
||||||
|
return enforcer.enforce(rule, target, credentials,
|
||||||
|
do_raise=do_raise, exc=exc, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def authorize(context, action, target, do_raise=True, exc=None,
|
||||||
|
might_not_exist=False):
|
||||||
|
"""Verifies that the action is valid on the target in this context.
|
||||||
|
|
||||||
|
:param context: gyan context
|
||||||
|
:param action: string representing the action to be checked
|
||||||
|
this should be colon separated for clarity.
|
||||||
|
i.e. ``network:attach_external_network``
|
||||||
|
:param target: dictionary representing the object of the action
|
||||||
|
for object creation this should be a dictionary representing the
|
||||||
|
location of the object e.g. ``{'project_id': context.project_id}``
|
||||||
|
:param do_raise: if True (the default), raises PolicyNotAuthorized;
|
||||||
|
if False, returns False
|
||||||
|
:param exc: Class of the exception to raise if the check fails.
|
||||||
|
Any remaining arguments passed to :meth:`authorize` (both
|
||||||
|
positional and keyword arguments) will be passed to
|
||||||
|
the exception class. If not specified,
|
||||||
|
:class:`PolicyNotAuthorized` will be used.
|
||||||
|
:param might_not_exist: If True the policy check is skipped (and the
|
||||||
|
function returns True) if the specified policy does not exist.
|
||||||
|
Defaults to false.
|
||||||
|
|
||||||
|
:raises gyan.common.exception.PolicyNotAuthorized: if verification fails
|
||||||
|
and do_raise is True. Or if 'exc' is specified it will raise an
|
||||||
|
exception of that type.
|
||||||
|
|
||||||
|
:return: returns a non-False value (not necessarily "True") if
|
||||||
|
authorized, and the exact value False if not authorized and
|
||||||
|
do_raise is False.
|
||||||
|
"""
|
||||||
|
credentials = context.to_policy_values()
|
||||||
|
if not exc:
|
||||||
|
exc = exception.PolicyNotAuthorized
|
||||||
|
if might_not_exist and not (_ENFORCER.rules and action in _ENFORCER.rules):
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
result = _ENFORCER.enforce(action, target, credentials,
|
||||||
|
do_raise=do_raise, exc=exc, action=action)
|
||||||
|
except Exception:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.debug('Policy check for %(action)s failed with credentials '
|
||||||
|
'%(credentials)s',
|
||||||
|
{'action': action, 'credentials': credentials})
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def check_is_admin(context):
|
||||||
|
"""Whether or not user is admin according to policy setting.
|
||||||
|
|
||||||
|
"""
|
||||||
|
init()
|
||||||
|
target = {}
|
||||||
|
credentials = context.to_policy_values()
|
||||||
|
return _ENFORCER.enforce('context_is_admin', target, credentials)
|
22
gyan/common/privileged.py
Normal file
22
gyan/common/privileged.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# 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_privsep import capabilities as c
|
||||||
|
from oslo_privsep import priv_context
|
||||||
|
|
||||||
|
|
||||||
|
default = priv_context.PrivContext(
|
||||||
|
'gyan.common',
|
||||||
|
cfg_section='privsep',
|
||||||
|
pypath=__name__ + '.default',
|
||||||
|
capabilities=[c.CAP_SYS_ADMIN],
|
||||||
|
)
|
101
gyan/common/profiler.py
Normal file
101
gyan/common/profiler.py
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
###
|
||||||
|
# This code is taken from nova. Goal is minimal modification.
|
||||||
|
###
|
||||||
|
|
||||||
|
from oslo_log import log as logging
|
||||||
|
from oslo_utils import importutils
|
||||||
|
import webob.dec
|
||||||
|
|
||||||
|
from gyan.common import context
|
||||||
|
import gyan.conf
|
||||||
|
|
||||||
|
profiler = importutils.try_import("osprofiler.profiler")
|
||||||
|
profiler_initializer = importutils.try_import("osprofiler.initializer")
|
||||||
|
profiler_web = importutils.try_import("osprofiler.web")
|
||||||
|
|
||||||
|
|
||||||
|
CONF = gyan.conf.CONF
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class WsgiMiddleware(object):
|
||||||
|
|
||||||
|
def __init__(self, application, **kwargs):
|
||||||
|
self.application = application
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def factory(cls, global_conf, **local_conf):
|
||||||
|
if profiler_web:
|
||||||
|
return profiler_web.WsgiMiddleware.factory(global_conf,
|
||||||
|
**local_conf)
|
||||||
|
|
||||||
|
def filter_(app):
|
||||||
|
return cls(app, **local_conf)
|
||||||
|
|
||||||
|
return filter_
|
||||||
|
|
||||||
|
@webob.dec.wsgify
|
||||||
|
def __call__(self, request):
|
||||||
|
return request.get_response(self.application)
|
||||||
|
|
||||||
|
|
||||||
|
def setup(binary, host):
|
||||||
|
if profiler_initializer and CONF.profiler.enabled:
|
||||||
|
profiler_initializer.init_from_conf(
|
||||||
|
conf=CONF,
|
||||||
|
context=context.get_admin_context().to_dict(),
|
||||||
|
project="gyan",
|
||||||
|
service=binary,
|
||||||
|
host=host)
|
||||||
|
LOG.info('OSProfiler is enabled.')
|
||||||
|
|
||||||
|
|
||||||
|
def trace_cls(name, **kwargs):
|
||||||
|
"""Wrap the OSprofiler trace_cls.
|
||||||
|
|
||||||
|
Wrap the OSprofiler trace_cls decorator so that it will not try to
|
||||||
|
patch the class unless OSprofiler is present.
|
||||||
|
|
||||||
|
:param name: The name of action. For example, wsgi, rpc, db, ...
|
||||||
|
:param kwargs: Any other keyword args used by profiler.trace_cls
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(cls):
|
||||||
|
if profiler and 'profiler' in CONF:
|
||||||
|
trace_decorator = profiler.trace_cls(name, kwargs)
|
||||||
|
return trace_decorator(cls)
|
||||||
|
return cls
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def trace(name, **kwargs):
|
||||||
|
"""Wrap the OSprofiler trace.
|
||||||
|
|
||||||
|
Wrap the OSprofiler trace decorator so that it will not try to
|
||||||
|
patch the functions unless OSprofiler is present.
|
||||||
|
|
||||||
|
:param name: The name of action. For example, wsgi, rpc, db, ...
|
||||||
|
:param kwargs: Any other keyword args used by profiler.trace
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(f):
|
||||||
|
if profiler and 'profiler' in CONF:
|
||||||
|
trace_decorator = profiler.trace(name, kwargs)
|
||||||
|
return trace_decorator(f)
|
||||||
|
return f
|
||||||
|
|
||||||
|
return decorator
|
120
gyan/common/rpc.py
Normal file
120
gyan/common/rpc.py
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'init',
|
||||||
|
'set_defaults',
|
||||||
|
'add_extra_exmods',
|
||||||
|
'clear_extra_exmods',
|
||||||
|
'get_allowed_exmods',
|
||||||
|
'RequestContextSerializer',
|
||||||
|
'get_client',
|
||||||
|
]
|
||||||
|
|
||||||
|
import oslo_messaging as messaging
|
||||||
|
from oslo_serialization import jsonutils as json
|
||||||
|
from oslo_utils import importutils
|
||||||
|
|
||||||
|
from gyan.common import context as gyan_context
|
||||||
|
from gyan.common import exception
|
||||||
|
|
||||||
|
profiler = importutils.try_import("osprofiler.profiler")
|
||||||
|
|
||||||
|
TRANSPORT = None
|
||||||
|
ALLOWED_EXMODS = [
|
||||||
|
exception.__name__,
|
||||||
|
]
|
||||||
|
EXTRA_EXMODS = []
|
||||||
|
|
||||||
|
|
||||||
|
def init(conf):
|
||||||
|
global TRANSPORT
|
||||||
|
exmods = get_allowed_exmods()
|
||||||
|
TRANSPORT = messaging.get_rpc_transport(
|
||||||
|
conf, allowed_remote_exmods=exmods)
|
||||||
|
|
||||||
|
|
||||||
|
def set_defaults(control_exchange):
|
||||||
|
messaging.set_transport_defaults(control_exchange)
|
||||||
|
|
||||||
|
|
||||||
|
def add_extra_exmods(*args):
|
||||||
|
EXTRA_EXMODS.extend(args)
|
||||||
|
|
||||||
|
|
||||||
|
def clear_extra_exmods():
|
||||||
|
del EXTRA_EXMODS[:]
|
||||||
|
|
||||||
|
|
||||||
|
def get_allowed_exmods():
|
||||||
|
return ALLOWED_EXMODS + EXTRA_EXMODS
|
||||||
|
|
||||||
|
|
||||||
|
class JsonPayloadSerializer(messaging.NoOpSerializer):
|
||||||
|
@staticmethod
|
||||||
|
def serialize_entity(context, entity):
|
||||||
|
return json.to_primitive(entity, convert_instances=True)
|
||||||
|
|
||||||
|
|
||||||
|
class RequestContextSerializer(messaging.Serializer):
|
||||||
|
|
||||||
|
def __init__(self, base):
|
||||||
|
self._base = base
|
||||||
|
|
||||||
|
def serialize_entity(self, context, entity):
|
||||||
|
if not self._base:
|
||||||
|
return entity
|
||||||
|
return self._base.serialize_entity(context, entity)
|
||||||
|
|
||||||
|
def deserialize_entity(self, context, entity):
|
||||||
|
if not self._base:
|
||||||
|
return entity
|
||||||
|
return self._base.deserialize_entity(context, entity)
|
||||||
|
|
||||||
|
def serialize_context(self, context):
|
||||||
|
return context.to_dict()
|
||||||
|
|
||||||
|
def deserialize_context(self, context):
|
||||||
|
return gyan_context.RequestContext.from_dict(context)
|
||||||
|
|
||||||
|
|
||||||
|
class ProfilerRequestContextSerializer(RequestContextSerializer):
|
||||||
|
def serialize_context(self, context):
|
||||||
|
_context = super(ProfilerRequestContextSerializer,
|
||||||
|
self).serialize_context(context)
|
||||||
|
|
||||||
|
prof = profiler.get()
|
||||||
|
if prof:
|
||||||
|
trace_info = {
|
||||||
|
"hmac_key": prof.hmac_key,
|
||||||
|
"base_id": prof.get_base_id(),
|
||||||
|
"parent_id": prof.get_id()
|
||||||
|
}
|
||||||
|
_context.update({"trace_info": trace_info})
|
||||||
|
|
||||||
|
return _context
|
||||||
|
|
||||||
|
def deserialize_context(self, context):
|
||||||
|
trace_info = context.pop("trace_info", None)
|
||||||
|
if trace_info:
|
||||||
|
profiler.init(**trace_info)
|
||||||
|
|
||||||
|
return super(ProfilerRequestContextSerializer,
|
||||||
|
self).deserialize_context(context)
|
||||||
|
|
||||||
|
|
||||||
|
def get_client(target, serializer=None, timeout=None):
|
||||||
|
assert TRANSPORT is not None
|
||||||
|
return messaging.RPCClient(TRANSPORT,
|
||||||
|
target,
|
||||||
|
serializer=serializer,
|
||||||
|
timeout=timeout)
|
105
gyan/common/rpc_service.py
Normal file
105
gyan/common/rpc_service.py
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
"""Common RPC service and API tools for Gyan."""
|
||||||
|
|
||||||
|
import oslo_messaging as messaging
|
||||||
|
from oslo_messaging.rpc import dispatcher
|
||||||
|
from oslo_service import service
|
||||||
|
from oslo_utils import importutils
|
||||||
|
|
||||||
|
from gyan.common import context
|
||||||
|
from gyan.common import profiler
|
||||||
|
from gyan.common import rpc
|
||||||
|
import gyan.conf
|
||||||
|
from gyan.objects import base as objects_base
|
||||||
|
from gyan.servicegroup import gyan_service_periodic as servicegroup
|
||||||
|
|
||||||
|
osprofiler = importutils.try_import("osprofiler.profiler")
|
||||||
|
|
||||||
|
CONF = gyan.conf.CONF
|
||||||
|
|
||||||
|
|
||||||
|
def _init_serializer():
|
||||||
|
serializer = rpc.RequestContextSerializer(
|
||||||
|
objects_base.GyanObjectSerializer())
|
||||||
|
if osprofiler:
|
||||||
|
serializer = rpc.ProfilerRequestContextSerializer(serializer)
|
||||||
|
else:
|
||||||
|
serializer = rpc.RequestContextSerializer(serializer)
|
||||||
|
return serializer
|
||||||
|
|
||||||
|
|
||||||
|
class Service(service.Service):
|
||||||
|
|
||||||
|
def __init__(self, topic, server, endpoints, binary):
|
||||||
|
super(Service, self).__init__()
|
||||||
|
serializer = _init_serializer()
|
||||||
|
transport = messaging.get_rpc_transport(CONF)
|
||||||
|
access_policy = dispatcher.DefaultRPCAccessPolicy
|
||||||
|
# TODO(asalkeld) add support for version='x.y'
|
||||||
|
target = messaging.Target(topic=topic, server=server)
|
||||||
|
self.endpoints = endpoints
|
||||||
|
self._server = messaging.get_rpc_server(transport, target, endpoints,
|
||||||
|
executor='eventlet',
|
||||||
|
serializer=serializer,
|
||||||
|
access_policy=access_policy)
|
||||||
|
self.binary = binary
|
||||||
|
profiler.setup(binary, CONF.compute.host)
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
servicegroup.setup(CONF, self.binary, self.tg)
|
||||||
|
for endpoint in self.endpoints:
|
||||||
|
if hasattr(endpoint, 'init_containers'):
|
||||||
|
endpoint.init_containers(
|
||||||
|
context.get_admin_context(all_projects=True))
|
||||||
|
self.tg.add_dynamic_timer(
|
||||||
|
endpoint.run_periodic_tasks,
|
||||||
|
periodic_interval_max=CONF.periodic_interval_max,
|
||||||
|
context=context.get_admin_context(all_projects=True)
|
||||||
|
)
|
||||||
|
self._server.start()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
if self._server:
|
||||||
|
self._server.stop()
|
||||||
|
self._server.wait()
|
||||||
|
super(Service, self).stop()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls, topic, server, handlers, binary):
|
||||||
|
service_obj = cls(topic, server, handlers, binary)
|
||||||
|
return service_obj
|
||||||
|
|
||||||
|
|
||||||
|
class API(object):
|
||||||
|
def __init__(self, context=None, topic=None, server=None,
|
||||||
|
timeout=None):
|
||||||
|
serializer = _init_serializer()
|
||||||
|
self._context = context
|
||||||
|
if topic is None:
|
||||||
|
topic = ''
|
||||||
|
target = messaging.Target(topic=topic, server=server)
|
||||||
|
self._client = rpc.get_client(target,
|
||||||
|
serializer=serializer,
|
||||||
|
timeout=timeout)
|
||||||
|
|
||||||
|
def _call(self, server, method, *args, **kwargs):
|
||||||
|
cctxt = self._client.prepare(server=server)
|
||||||
|
return cctxt.call(self._context, method, *args, **kwargs)
|
||||||
|
|
||||||
|
def _cast(self, server, method, *args, **kwargs):
|
||||||
|
cctxt = self._client.prepare(server=server)
|
||||||
|
return cctxt.cast(self._context, method, *args, **kwargs)
|
||||||
|
|
||||||
|
def echo(self, message):
|
||||||
|
self._cast('echo', message=message)
|
92
gyan/common/service.py
Normal file
92
gyan/common/service.py
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
|
||||||
|
from oslo_concurrency import processutils
|
||||||
|
from oslo_log import log
|
||||||
|
from oslo_service import service
|
||||||
|
from oslo_service import wsgi
|
||||||
|
|
||||||
|
from gyan.api import app
|
||||||
|
from gyan.common import config
|
||||||
|
from gyan.common import exception
|
||||||
|
from gyan.common.i18n import _
|
||||||
|
import gyan.conf
|
||||||
|
|
||||||
|
CONF = gyan.conf.CONF
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_service(argv=None):
|
||||||
|
if argv is None:
|
||||||
|
argv = []
|
||||||
|
log.register_options(CONF)
|
||||||
|
config.parse_args(argv)
|
||||||
|
config.set_config_defaults()
|
||||||
|
log.setup(CONF, 'gyan')
|
||||||
|
# TODO(yuanying): Uncomment after objects are implemented
|
||||||
|
# objects.register_all()
|
||||||
|
|
||||||
|
|
||||||
|
def process_launcher():
|
||||||
|
return service.ProcessLauncher(CONF, restart_method='mutate')
|
||||||
|
|
||||||
|
|
||||||
|
class WSGIService(service.ServiceBase):
|
||||||
|
"""Provides ability to launch Gyan API from wsgi app."""
|
||||||
|
|
||||||
|
def __init__(self, name, use_ssl=False):
|
||||||
|
"""Initialize, but do not start the WSGI server.
|
||||||
|
|
||||||
|
:param name: The name of the WSGI server given to the loader.
|
||||||
|
:param use_ssl: Wraps the socket in an SSL context if True.
|
||||||
|
:returns: None
|
||||||
|
"""
|
||||||
|
self.name = name
|
||||||
|
self.app = app.load_app()
|
||||||
|
self.workers = (CONF.api.workers or processutils.get_worker_count())
|
||||||
|
if self.workers and self.workers < 1:
|
||||||
|
raise exception.ConfigInvalid(
|
||||||
|
_("api_workers value of %d is invalid, "
|
||||||
|
"must be greater than 0.") % self.workers)
|
||||||
|
|
||||||
|
self.server = wsgi.Server(CONF, name, self.app,
|
||||||
|
host=CONF.api.host_ip,
|
||||||
|
port=CONF.api.port,
|
||||||
|
use_ssl=use_ssl)
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
"""Start serving this service using loaded configuration.
|
||||||
|
|
||||||
|
:returns: None
|
||||||
|
"""
|
||||||
|
self.server.start()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""Stop serving this API.
|
||||||
|
|
||||||
|
:returns: None
|
||||||
|
"""
|
||||||
|
self.server.stop()
|
||||||
|
|
||||||
|
def wait(self):
|
||||||
|
"""Wait for the service to stop serving this API.
|
||||||
|
|
||||||
|
:returns: None
|
||||||
|
"""
|
||||||
|
self.server.wait()
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
"""Reset server greenpool size to default.
|
||||||
|
|
||||||
|
:returns: None
|
||||||
|
"""
|
||||||
|
self.server.reset()
|
63
gyan/common/short_id.py
Normal file
63
gyan/common/short_id.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
"""Utilities for creating short ID strings based on a random UUID.
|
||||||
|
The IDs each comprise 12 (lower-case) alphanumeric characters.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
from oslo_utils import uuidutils
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
from gyan.common.i18n import _
|
||||||
|
|
||||||
|
|
||||||
|
def _to_byte_string(value, num_bits):
|
||||||
|
"""Convert an integer to a big-endian string of bytes with padding.
|
||||||
|
|
||||||
|
Padding is added at the end (i.e. after the least-significant bit) if
|
||||||
|
required.
|
||||||
|
"""
|
||||||
|
|
||||||
|
shifts = six.moves.xrange(num_bits - 8, -8, -8)
|
||||||
|
byte_at = lambda off: (value >> off if off >= 0 else value << -off) & 0xff
|
||||||
|
return ''.join(six.int2byte(byte_at(offset)) for offset in shifts)
|
||||||
|
|
||||||
|
|
||||||
|
def get_id(source_uuid):
|
||||||
|
"""Derive a short (12 character) id from a random UUID.
|
||||||
|
|
||||||
|
The supplied UUID must be a version 4 UUID object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if isinstance(source_uuid, six.string_types):
|
||||||
|
source_uuid = uuid.UUID(source_uuid)
|
||||||
|
if source_uuid.version != 4:
|
||||||
|
raise ValueError(_('Invalid UUID version (%d)') % source_uuid.version)
|
||||||
|
|
||||||
|
# The "time" field of a v4 UUID contains 60 random bits
|
||||||
|
# (see RFC4122, Section 4.4)
|
||||||
|
random_bytes = _to_byte_string(source_uuid.time, 60)
|
||||||
|
# The first 12 bytes (= 60 bits) of base32-encoded output is our data
|
||||||
|
encoded = base64.b32encode(six.b(random_bytes))[:12]
|
||||||
|
|
||||||
|
if six.PY3:
|
||||||
|
return encoded.lower().decode('utf-8')
|
||||||
|
else:
|
||||||
|
return encoded.lower()
|
||||||
|
|
||||||
|
|
||||||
|
def generate_id():
|
||||||
|
"""Generate a short (12 character), random id."""
|
||||||
|
return uuidutils.generate_uuid()
|
25
gyan/common/singleton.py
Normal file
25
gyan/common/singleton.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
from oslo_concurrency import lockutils
|
||||||
|
|
||||||
|
|
||||||
|
class Singleton(type):
|
||||||
|
_instances = {}
|
||||||
|
_semaphores = lockutils.Semaphores()
|
||||||
|
|
||||||
|
def __call__(cls, *args, **kwargs):
|
||||||
|
with lockutils.lock('singleton_lock', semaphores=cls._semaphores):
|
||||||
|
if cls not in cls._instances:
|
||||||
|
cls._instances[cls] = super(
|
||||||
|
Singleton, cls).__call__(*args, **kwargs)
|
||||||
|
return cls._instances[cls]
|
255
gyan/common/utils.py
Normal file
255
gyan/common/utils.py
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
# It's based on oslo.i18n usage in OpenStack Keystone project and
|
||||||
|
# recommendations from
|
||||||
|
# https://docs.openstack.org/oslo.i18n/latest/user/usage.html
|
||||||
|
|
||||||
|
"""Utilities and helper functions."""
|
||||||
|
import base64
|
||||||
|
import binascii
|
||||||
|
import eventlet
|
||||||
|
import functools
|
||||||
|
import inspect
|
||||||
|
import json
|
||||||
|
import mimetypes
|
||||||
|
|
||||||
|
from oslo_concurrency import processutils
|
||||||
|
from oslo_context import context as common_context
|
||||||
|
from oslo_log import log as logging
|
||||||
|
from oslo_utils import excutils
|
||||||
|
from oslo_utils import strutils
|
||||||
|
import pecan
|
||||||
|
import six
|
||||||
|
|
||||||
|
from gyan.api import utils as api_utils
|
||||||
|
from gyan.common import consts
|
||||||
|
from gyan.common import exception
|
||||||
|
from gyan.common.i18n import _
|
||||||
|
from gyan.common import privileged
|
||||||
|
import gyan.conf
|
||||||
|
from gyan import objects
|
||||||
|
|
||||||
|
CONF = gyan.conf.CONF
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
VALID_STATES = {
|
||||||
|
'deploy': [consts.CREATED, consts.UNDEPLOYED],
|
||||||
|
'undeploy': [consts.DEPLOYED]
|
||||||
|
}
|
||||||
|
def safe_rstrip(value, chars=None):
|
||||||
|
"""Removes trailing characters from a string if that does not make it empty
|
||||||
|
|
||||||
|
:param value: A string value that will be stripped.
|
||||||
|
:param chars: Characters to remove.
|
||||||
|
:return: Stripped value.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not isinstance(value, six.string_types):
|
||||||
|
LOG.warning(
|
||||||
|
"Failed to remove trailing character. Returning original object. "
|
||||||
|
"Supplied object is not a string: %s.", value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
return value.rstrip(chars) or value
|
||||||
|
|
||||||
|
|
||||||
|
def _do_allow_certain_content_types(func, content_types_list):
|
||||||
|
# Allows you to bypass pecan's content-type restrictions
|
||||||
|
cfg = pecan.util._cfg(func)
|
||||||
|
cfg.setdefault('content_types', {})
|
||||||
|
cfg['content_types'].update((value, '')
|
||||||
|
for value in content_types_list)
|
||||||
|
return func
|
||||||
|
|
||||||
|
|
||||||
|
def allow_certain_content_types(*content_types_list):
|
||||||
|
def _wrapper(func):
|
||||||
|
return _do_allow_certain_content_types(func, content_types_list)
|
||||||
|
return _wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def allow_all_content_types(f):
|
||||||
|
return _do_allow_certain_content_types(f, mimetypes.types_map.values())
|
||||||
|
|
||||||
|
|
||||||
|
def spawn_n(func, *args, **kwargs):
|
||||||
|
"""Passthrough method for eventlet.spawn_n.
|
||||||
|
|
||||||
|
This utility exists so that it can be stubbed for testing without
|
||||||
|
interfering with the service spawns.
|
||||||
|
|
||||||
|
It will also grab the context from the threadlocal store and add it to
|
||||||
|
the store on the new thread. This allows for continuity in logging the
|
||||||
|
context when using this method to spawn a new thread.
|
||||||
|
"""
|
||||||
|
_context = common_context.get_current()
|
||||||
|
|
||||||
|
@functools.wraps(func)
|
||||||
|
def context_wrapper(*args, **kwargs):
|
||||||
|
# NOTE: If update_store is not called after spawn_n it won't be
|
||||||
|
# available for the logger to pull from threadlocal storage.
|
||||||
|
if _context is not None:
|
||||||
|
_context.update_store()
|
||||||
|
func(*args, **kwargs)
|
||||||
|
|
||||||
|
eventlet.spawn_n(context_wrapper, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def translate_exception(function):
|
||||||
|
"""Wraps a method to catch exceptions.
|
||||||
|
|
||||||
|
If the exception is not an instance of GyanException,
|
||||||
|
translate it into one.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@functools.wraps(function)
|
||||||
|
def decorated_function(self, context, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
return function(self, context, *args, **kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
if not isinstance(e, exception.GyanException):
|
||||||
|
LOG.exception("Unexpected error: %s", six.text_type(e))
|
||||||
|
e = exception.GyanException("Unexpected error: %s"
|
||||||
|
% six.text_type(e))
|
||||||
|
raise e
|
||||||
|
raise
|
||||||
|
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
|
||||||
|
def custom_execute(*cmd, **kwargs):
|
||||||
|
try:
|
||||||
|
return processutils.execute(*cmd, **kwargs)
|
||||||
|
except processutils.ProcessExecutionError as e:
|
||||||
|
sanitized_cmd = strutils.mask_password(' '.join(cmd))
|
||||||
|
raise exception.CommandError(cmd=sanitized_cmd,
|
||||||
|
error=six.text_type(e))
|
||||||
|
|
||||||
|
|
||||||
|
def is_all_projects(search_opts):
|
||||||
|
all_projects = search_opts.get('all_projects')
|
||||||
|
if all_projects:
|
||||||
|
try:
|
||||||
|
all_projects = strutils.bool_from_string(all_projects, True)
|
||||||
|
except ValueError:
|
||||||
|
bools = ', '.join(strutils.TRUE_STRINGS + strutils.FALSE_STRINGS)
|
||||||
|
raise exception.InvalidValue(_('Valid all_projects values are: %s')
|
||||||
|
% bools)
|
||||||
|
else:
|
||||||
|
all_projects = False
|
||||||
|
return all_projects
|
||||||
|
|
||||||
|
|
||||||
|
def get_ml_model(ml_model_ident):
|
||||||
|
ml_model = api_utils.get_resource('ML_Model', ml_model_ident)
|
||||||
|
if not ml_model:
|
||||||
|
pecan.abort(404, ('Not found; the ml model you requested '
|
||||||
|
'does not exist.'))
|
||||||
|
|
||||||
|
return ml_model
|
||||||
|
|
||||||
|
def validate_ml_model_state(ml_model, action):
|
||||||
|
if ml_model.status not in VALID_STATES[action]:
|
||||||
|
raise exception.InvalidStateException(
|
||||||
|
id=ml_model.uuid,
|
||||||
|
action=action,
|
||||||
|
actual_state=ml_model.status)
|
||||||
|
|
||||||
|
|
||||||
|
def get_wrapped_function(function):
|
||||||
|
"""Get the method at the bottom of a stack of decorators."""
|
||||||
|
if not hasattr(function, '__closure__') or not function.__closure__:
|
||||||
|
return function
|
||||||
|
|
||||||
|
def _get_wrapped_function(function):
|
||||||
|
if not hasattr(function, '__closure__') or not function.__closure__:
|
||||||
|
return None
|
||||||
|
|
||||||
|
for closure in function.__closure__:
|
||||||
|
func = closure.cell_contents
|
||||||
|
|
||||||
|
deeper_func = _get_wrapped_function(func)
|
||||||
|
if deeper_func:
|
||||||
|
return deeper_func
|
||||||
|
elif hasattr(closure.cell_contents, '__call__'):
|
||||||
|
return closure.cell_contents
|
||||||
|
|
||||||
|
return function
|
||||||
|
|
||||||
|
return _get_wrapped_function(function)
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_ml_model_event(prefix):
|
||||||
|
"""Warps a method to log the event taken on the ml_model, and result.
|
||||||
|
|
||||||
|
This decorator wraps a method to log the start and result of an event, as
|
||||||
|
part of an action taken on a ml_model.
|
||||||
|
"""
|
||||||
|
def helper(function):
|
||||||
|
|
||||||
|
@functools.wraps(function)
|
||||||
|
def decorated_function(self, context, *args, **kwargs):
|
||||||
|
wrapped_func = get_wrapped_function(function)
|
||||||
|
keyed_args = inspect.getcallargs(wrapped_func, self, context,
|
||||||
|
*args, **kwargs)
|
||||||
|
ml_model_uuid = keyed_args['ml_model'].uuid
|
||||||
|
|
||||||
|
event_name = '{0}_{1}'.format(prefix, function.__name__)
|
||||||
|
with EventReporter(context, event_name, ml_model_uuid):
|
||||||
|
return function(self, context, *args, **kwargs)
|
||||||
|
return decorated_function
|
||||||
|
return helper
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_exception():
|
||||||
|
def helper(function):
|
||||||
|
|
||||||
|
@functools.wraps(function)
|
||||||
|
def decorated_function(self, context, ml_model, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
return function(self, context, ml_model, *args, **kwargs)
|
||||||
|
except exception.DockerError as e:
|
||||||
|
with excutils.save_and_reraise_exception(reraise=False):
|
||||||
|
LOG.error("Error occurred while calling Docker API: %s",
|
||||||
|
six.text_type(e))
|
||||||
|
except Exception as e:
|
||||||
|
with excutils.save_and_reraise_exception(reraise=False):
|
||||||
|
LOG.exception("Unexpected exception: %s", six.text_type(e))
|
||||||
|
return decorated_function
|
||||||
|
return helper
|
||||||
|
|
||||||
|
|
||||||
|
def is_close(x, y, rel_tol=1e-06, abs_tol=0.0):
|
||||||
|
return abs(x - y) <= max(rel_tol * max(abs(x), abs(y)), abs_tol)
|
||||||
|
|
||||||
|
|
||||||
|
def is_less_than(x, y):
|
||||||
|
if isinstance(x, int) and isinstance(y, int):
|
||||||
|
return x < y
|
||||||
|
if isinstance(x, float) or isinstance(y, float):
|
||||||
|
return False if (x - y) >= 0 or is_close(x, y) else True
|
||||||
|
|
||||||
|
|
||||||
|
def encode_file_data(data):
|
||||||
|
if six.PY3 and isinstance(data, str):
|
||||||
|
data = data.encode('utf-8')
|
||||||
|
return base64.b64encode(data).decode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
def decode_file_data(data):
|
||||||
|
# Py3 raises binascii.Error instead of TypeError as in Py27
|
||||||
|
try:
|
||||||
|
return base64.b64decode(data)
|
||||||
|
except (TypeError, binascii.Error):
|
||||||
|
raise exception.Base64Exception()
|
33
gyan/common/yamlutils.py
Normal file
33
gyan/common/yamlutils.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# 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 yaml
|
||||||
|
|
||||||
|
|
||||||
|
def load(s):
|
||||||
|
try:
|
||||||
|
yml_dict = yaml.safe_load(s)
|
||||||
|
except yaml.YAMLError as exc:
|
||||||
|
msg = 'An error occurred during YAML parsing.'
|
||||||
|
if hasattr(exc, 'problem_mark'):
|
||||||
|
msg += ' Error position: (%s:%s)' % (exc.problem_mark.line + 1,
|
||||||
|
exc.problem_mark.column + 1)
|
||||||
|
raise ValueError(msg)
|
||||||
|
if not isinstance(yml_dict, dict) and not isinstance(yml_dict, list):
|
||||||
|
raise ValueError('The source is not a YAML mapping or list.')
|
||||||
|
if isinstance(yml_dict, dict) and len(yml_dict) < 1:
|
||||||
|
raise ValueError('Could not find any element in your YAML mapping.')
|
||||||
|
return yml_dict
|
||||||
|
|
||||||
|
|
||||||
|
def dump(s):
|
||||||
|
return yaml.safe_dump(s)
|
0
gyan/compute/__init__.py
Normal file
0
gyan/compute/__init__.py
Normal file
63
gyan/compute/api.py
Normal file
63
gyan/compute/api.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
"""Handles all requests relating to compute resources (e.g. ml_models,
|
||||||
|
and compute hosts on which they run)."""
|
||||||
|
|
||||||
|
from oslo_log import log as logging
|
||||||
|
|
||||||
|
from gyan.common import consts
|
||||||
|
from gyan.common import exception
|
||||||
|
from gyan.common.i18n import _
|
||||||
|
from gyan.common import profiler
|
||||||
|
from gyan.compute import rpcapi
|
||||||
|
import gyan.conf
|
||||||
|
from gyan import objects
|
||||||
|
|
||||||
|
|
||||||
|
CONF = gyan.conf.CONF
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@profiler.trace_cls("rpc")
|
||||||
|
class API(object):
|
||||||
|
"""API for interacting with the compute manager."""
|
||||||
|
|
||||||
|
def __init__(self, context):
|
||||||
|
self.rpcapi = rpcapi.API(context=context)
|
||||||
|
super(API, self).__init__()
|
||||||
|
|
||||||
|
def ml_model_create(self, context, new_ml_model, extra_spec):
|
||||||
|
try:
|
||||||
|
host_state = self._schedule_ml_model(context, ml_model,
|
||||||
|
extra_spec)
|
||||||
|
except exception.NoValidHost:
|
||||||
|
new_ml_model.status = consts.ERROR
|
||||||
|
new_ml_model.status_reason = _(
|
||||||
|
"There are not enough hosts available.")
|
||||||
|
new_ml_model.save(context)
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
new_ml_model.status = consts.ERROR
|
||||||
|
new_ml_model.status_reason = _("Unexpected exception occurred.")
|
||||||
|
new_ml_model.save(context)
|
||||||
|
raise
|
||||||
|
|
||||||
|
self.rpcapi.ml_model_create(context, host_state['host'],
|
||||||
|
new_ml_model)
|
||||||
|
|
||||||
|
def ml_model_delete(self, context, ml_model, *args):
|
||||||
|
self._record_action_start(context, ml_model, ml_model_actions.DELETE)
|
||||||
|
return self.rpcapi.ml_model_delete(context, ml_model, *args)
|
||||||
|
|
||||||
|
def ml_model_show(self, context, ml_model):
|
||||||
|
return self.rpcapi.ml_model_show(context, ml_model)
|
67
gyan/compute/compute_host_tracker.py
Normal file
67
gyan/compute/compute_host_tracker.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import collections
|
||||||
|
import copy
|
||||||
|
|
||||||
|
from oslo_log import log as logging
|
||||||
|
|
||||||
|
from gyan.common import exception
|
||||||
|
from gyan.common import utils
|
||||||
|
from gyan import objects
|
||||||
|
from gyan.objects import base as obj_base
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
COMPUTE_RESOURCE_SEMAPHORE = "compute_resources"
|
||||||
|
|
||||||
|
|
||||||
|
class ComputeHostTracker(object):
|
||||||
|
def __init__(self, host, ml_model_driver):
|
||||||
|
self.host = host
|
||||||
|
self.ml_model_driver = ml_model_driver
|
||||||
|
self.compute_host = None
|
||||||
|
self.tracked_ml_models = {}
|
||||||
|
self.old_resources = collections.defaultdict(objects.ComputeHost)
|
||||||
|
|
||||||
|
|
||||||
|
def update_available_resources(self, context):
|
||||||
|
# Check if the compute_host is already registered
|
||||||
|
host = self._get_compute_host(context)
|
||||||
|
if not host:
|
||||||
|
# If not, register it and pass the object to the driver
|
||||||
|
host = objects.ComputeHost(context)
|
||||||
|
host.hostname = self.host
|
||||||
|
host.type = self.ml_model_driver.__class__.__name__
|
||||||
|
host.status = "AVAILABLE"
|
||||||
|
host.create(context)
|
||||||
|
LOG.info('Host created for :%(host)s', {'host': self.host})
|
||||||
|
self.ml_model_driver.get_available_resources(host)
|
||||||
|
self.compute_host = host
|
||||||
|
return host
|
||||||
|
|
||||||
|
def _get_compute_host(self, context):
|
||||||
|
"""Returns compute host for the host"""
|
||||||
|
try:
|
||||||
|
return objects.ComputeHost.get_by_name(context, self.host)
|
||||||
|
except exception.ComputeHostNotFound:
|
||||||
|
LOG.warning("No compute host record for: %(host)s",
|
||||||
|
{'host': self.host})
|
||||||
|
|
||||||
|
|
||||||
|
def _set_ml_model_host(self, context, ml_model):
|
||||||
|
"""Tag the ml_model as belonging to this host.
|
||||||
|
|
||||||
|
This should be done while the COMPUTE_RESOURCES_SEMAPHORE is held so
|
||||||
|
the resource claim will not be lost if the audit process starts.
|
||||||
|
"""
|
||||||
|
ml_model.host = self.host
|
||||||
|
ml_model.save(context)
|
121
gyan/compute/manager.py
Normal file
121
gyan/compute/manager.py
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
# Copyright 2016 IBM Corp.
|
||||||
|
#
|
||||||
|
# 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 itertools
|
||||||
|
|
||||||
|
import six
|
||||||
|
import time
|
||||||
|
|
||||||
|
from oslo_log import log as logging
|
||||||
|
from oslo_service import periodic_task
|
||||||
|
from oslo_utils import excutils
|
||||||
|
from oslo_utils import timeutils
|
||||||
|
from oslo_utils import uuidutils
|
||||||
|
|
||||||
|
from gyan.common import consts
|
||||||
|
from gyan.common import context
|
||||||
|
from gyan.common import exception
|
||||||
|
from gyan.common.i18n import _
|
||||||
|
from gyan.common import utils
|
||||||
|
from gyan.common.utils import translate_exception
|
||||||
|
from gyan.common.utils import wrap_ml_model_event
|
||||||
|
from gyan.common.utils import wrap_exception
|
||||||
|
from gyan.compute import compute_host_tracker
|
||||||
|
import gyan.conf
|
||||||
|
from gyan.ml_model import driver
|
||||||
|
from gyan import objects
|
||||||
|
|
||||||
|
CONF = gyan.conf.CONF
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Manager(periodic_task.PeriodicTasks):
|
||||||
|
"""Manages the running ml_models."""
|
||||||
|
|
||||||
|
def __init__(self, ml_model_driver=None):
|
||||||
|
super(Manager, self).__init__(CONF)
|
||||||
|
self.driver = driver.load_ml_model_driver(ml_model_driver)
|
||||||
|
self.host = CONF.compute.host
|
||||||
|
self._resource_tracker = None
|
||||||
|
|
||||||
|
def ml_model_create(self, context, limits, requested_networks,
|
||||||
|
requested_volumes, ml_model, run, pci_requests=None):
|
||||||
|
@utils.synchronized(ml_model.uuid)
|
||||||
|
def do_ml_model_create():
|
||||||
|
created_ml_model = self._do_ml_model_create(
|
||||||
|
context, ml_model, requested_networks, requested_volumes,
|
||||||
|
pci_requests, limits)
|
||||||
|
if run:
|
||||||
|
self._do_ml_model_start(context, created_ml_model)
|
||||||
|
|
||||||
|
utils.spawn_n(do_ml_model_create)
|
||||||
|
|
||||||
|
@wrap_ml_model_event(prefix='compute')
|
||||||
|
def _do_ml_model_create(self, context, ml_model, requested_networks,
|
||||||
|
requested_volumes, pci_requests=None,
|
||||||
|
limits=None):
|
||||||
|
LOG.debug('Creating ml_model: %s', ml_model.uuid)
|
||||||
|
|
||||||
|
try:
|
||||||
|
rt = self._get_resource_tracker()
|
||||||
|
# As sriov port also need to claim, we need claim pci port before
|
||||||
|
# create sandbox.
|
||||||
|
with rt.ml_model_claim(context, ml_model, pci_requests, limits):
|
||||||
|
sandbox = None
|
||||||
|
if self.use_sandbox:
|
||||||
|
sandbox = self._create_sandbox(context, ml_model,
|
||||||
|
requested_networks)
|
||||||
|
|
||||||
|
created_ml_model = self._do_ml_model_create_base(
|
||||||
|
context, ml_model, requested_networks, requested_volumes,
|
||||||
|
sandbox, limits)
|
||||||
|
return created_ml_model
|
||||||
|
except exception.ResourcesUnavailable as e:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception("ML Model resource claim failed: %s",
|
||||||
|
six.text_type(e))
|
||||||
|
self._fail_ml_model(context, ml_model, six.text_type(e),
|
||||||
|
unset_host=True)
|
||||||
|
|
||||||
|
@wrap_ml_model_event(prefix='compute')
|
||||||
|
def _do_ml_model_start(self, context, ml_model):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@translate_exception
|
||||||
|
def ml_model_delete(self, context, ml_model, force=False):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@translate_exception
|
||||||
|
def ml_model_show(self, context, ml_model):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@translate_exception
|
||||||
|
def ml_model_start(self, context, ml_model):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@translate_exception
|
||||||
|
def ml_model_update(self, context, ml_model, patch):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@periodic_task.periodic_task(run_immediately=True)
|
||||||
|
def inventory_host(self, context):
|
||||||
|
rt = self._get_resource_tracker()
|
||||||
|
rt.update_available_resources(context)
|
||||||
|
|
||||||
|
def _get_resource_tracker(self):
|
||||||
|
if not self._resource_tracker:
|
||||||
|
rt = compute_host_tracker.ComputeHostTracker(self.host,
|
||||||
|
self.driver)
|
||||||
|
self._resource_tracker = rt
|
||||||
|
return self._resource_tracker
|
68
gyan/compute/rpcapi.py
Normal file
68
gyan/compute/rpcapi.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
# Copyright 2016 IBM Corp.
|
||||||
|
#
|
||||||
|
# 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 gyan.api import servicegroup
|
||||||
|
from gyan.common import exception
|
||||||
|
from gyan.common import profiler
|
||||||
|
from gyan.common import rpc_service
|
||||||
|
import gyan.conf
|
||||||
|
from gyan import objects
|
||||||
|
|
||||||
|
|
||||||
|
def check_ml_model_host(func):
|
||||||
|
"""Verify the state of ML Model host"""
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrap(self, context, ml_model, *args, **kwargs):
|
||||||
|
return func(self, context, ml_model, *args, **kwargs)
|
||||||
|
return wrap
|
||||||
|
|
||||||
|
|
||||||
|
@profiler.trace_cls("rpc")
|
||||||
|
class API(rpc_service.API):
|
||||||
|
"""Client side of the ml_model compute rpc API.
|
||||||
|
|
||||||
|
API version history:
|
||||||
|
|
||||||
|
* 1.0 - Initial version.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, transport=None, context=None, topic=None):
|
||||||
|
if topic is None:
|
||||||
|
gyan.conf.CONF.import_opt(
|
||||||
|
'topic', 'gyan.conf.compute', group='compute')
|
||||||
|
|
||||||
|
super(API, self).__init__(
|
||||||
|
context, gyan.conf.CONF.compute.topic, transport)
|
||||||
|
|
||||||
|
def ml_model_create(self, context, host, ml_model):
|
||||||
|
self._cast(host, 'ml_model_create',
|
||||||
|
ml_model=ml_model)
|
||||||
|
|
||||||
|
@check_ml_model_host
|
||||||
|
def ml_model_delete(self, context, ml_model, force):
|
||||||
|
return self._cast(ml_model.host, 'ml_model_delete',
|
||||||
|
ml_model=ml_model, force=force)
|
||||||
|
|
||||||
|
@check_ml_model_host
|
||||||
|
def ml_model_show(self, context, ml_model):
|
||||||
|
return self._call(ml_model.host, 'ml_model_show',
|
||||||
|
ml_model=ml_model)
|
||||||
|
|
||||||
|
|
||||||
|
@check_ml_model_host
|
||||||
|
def ml_model_update(self, context, ml_model, patch):
|
||||||
|
return self._call(ml_model.host, 'ml_model_update',
|
||||||
|
ml_model=ml_model, patch=patch)
|
41
gyan/conf/__init__.py
Normal file
41
gyan/conf/__init__.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
from gyan.conf import api
|
||||||
|
from gyan.conf import compute
|
||||||
|
from gyan.conf import ml_model_driver
|
||||||
|
from gyan.conf import database
|
||||||
|
from gyan.conf import keystone
|
||||||
|
from gyan.conf import path
|
||||||
|
from gyan.conf import profiler
|
||||||
|
from gyan.conf import scheduler
|
||||||
|
from gyan.conf import services
|
||||||
|
from gyan.conf import ssl
|
||||||
|
from gyan.conf import utils
|
||||||
|
from gyan.conf import gyan_client
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
api.register_opts(CONF)
|
||||||
|
compute.register_opts(CONF)
|
||||||
|
ml_model_driver.register_opts(CONF)
|
||||||
|
database.register_opts(CONF)
|
||||||
|
keystone.register_opts(CONF)
|
||||||
|
path.register_opts(CONF)
|
||||||
|
scheduler.register_opts(CONF)
|
||||||
|
services.register_opts(CONF)
|
||||||
|
gyan_client.register_opts(CONF)
|
||||||
|
ssl.register_opts(CONF)
|
||||||
|
profiler.register_opts(CONF)
|
||||||
|
utils.register_opts(CONF)
|
67
gyan/conf/api.py
Normal file
67
gyan/conf/api.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
|
||||||
|
api_service_opts = [
|
||||||
|
cfg.PortOpt('port',
|
||||||
|
default=8517,
|
||||||
|
help='The port for the gyan API server.'),
|
||||||
|
cfg.StrOpt('host',
|
||||||
|
default="localhost",
|
||||||
|
help='The port for the gyan API server.'),
|
||||||
|
cfg.IPOpt('host_ip',
|
||||||
|
default='127.0.0.1',
|
||||||
|
help="The listen IP for the gyan API server. "
|
||||||
|
"The default is ``$my_ip``, "
|
||||||
|
"the IP address of this host."),
|
||||||
|
cfg.BoolOpt('enable_ssl_api',
|
||||||
|
default=False,
|
||||||
|
help="Enable the integrated stand-alone API to service "
|
||||||
|
"requests via HTTPS instead of HTTP. If there is a "
|
||||||
|
"front-end service performing HTTPS offloading from "
|
||||||
|
"the service, this option should be False; note, you "
|
||||||
|
"will want to change public API endpoint to represent "
|
||||||
|
"SSL termination URL with 'public_endpoint' option."),
|
||||||
|
cfg.IntOpt('workers',
|
||||||
|
help="Number of workers for gyan-api service. "
|
||||||
|
"The default will be the number of CPUs available."),
|
||||||
|
cfg.IntOpt('max_limit',
|
||||||
|
default=1000,
|
||||||
|
help='The maximum number of items returned in a single '
|
||||||
|
'response from a collection resource.'),
|
||||||
|
cfg.StrOpt('api_paste_config',
|
||||||
|
default="api-paste.ini",
|
||||||
|
help="Configuration file for WSGI definition of API."),
|
||||||
|
cfg.BoolOpt('enable_image_validation',
|
||||||
|
default=True,
|
||||||
|
help="Enable image validation.")
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
api_group = cfg.OptGroup(name='api',
|
||||||
|
title='Options for the gyan-api service')
|
||||||
|
|
||||||
|
|
||||||
|
ALL_OPTS = (api_service_opts)
|
||||||
|
|
||||||
|
|
||||||
|
def register_opts(conf):
|
||||||
|
conf.register_group(api_group)
|
||||||
|
conf.register_opts(ALL_OPTS, api_group)
|
||||||
|
|
||||||
|
|
||||||
|
def list_opts():
|
||||||
|
return {
|
||||||
|
api_group: ALL_OPTS
|
||||||
|
}
|
40
gyan/conf/compute.py
Normal file
40
gyan/conf/compute.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
|
||||||
|
service_opts = [
|
||||||
|
cfg.StrOpt(
|
||||||
|
'topic',
|
||||||
|
default='gyan-compute',
|
||||||
|
help='The queue to add compute tasks to.'),
|
||||||
|
cfg.StrOpt(
|
||||||
|
'host',
|
||||||
|
default='localhost',
|
||||||
|
help='hostname'),
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
opt_group = cfg.OptGroup(
|
||||||
|
name='compute', title='Options for the gyan-compute service')
|
||||||
|
|
||||||
|
ALL_OPTS = (service_opts)
|
||||||
|
|
||||||
|
|
||||||
|
def register_opts(conf):
|
||||||
|
conf.register_group(opt_group)
|
||||||
|
conf.register_opts(ALL_OPTS, opt_group)
|
||||||
|
|
||||||
|
|
||||||
|
def list_opts():
|
||||||
|
return {opt_group: ALL_OPTS}
|
32
gyan/conf/database.py
Normal file
32
gyan/conf/database.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
|
||||||
|
sql_opts = [
|
||||||
|
cfg.StrOpt('mysql_engine',
|
||||||
|
default='InnoDB',
|
||||||
|
help='MySQL engine to use.')
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_OPTS = (sql_opts)
|
||||||
|
|
||||||
|
|
||||||
|
def register_opts(conf):
|
||||||
|
conf.register_opts(sql_opts, 'database')
|
||||||
|
|
||||||
|
|
||||||
|
def list_opts():
|
||||||
|
return {"DEFAULT": DEFAULT_OPTS}
|
51
gyan/conf/gyan_client.py
Normal file
51
gyan/conf/gyan_client.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
|
||||||
|
gyan_group = cfg.OptGroup(name='gyan_client',
|
||||||
|
title='Options for the Gyan client')
|
||||||
|
|
||||||
|
gyan_client_opts = [
|
||||||
|
cfg.StrOpt('region_name',
|
||||||
|
help='Region in Identity service catalog to use for '
|
||||||
|
'communication with the OpenStack service.'),
|
||||||
|
cfg.StrOpt('endpoint_type',
|
||||||
|
default='publicURL',
|
||||||
|
help='Type of endpoint in Identity service catalog to use '
|
||||||
|
'for communication with the OpenStack service.')]
|
||||||
|
|
||||||
|
|
||||||
|
common_security_opts = [
|
||||||
|
cfg.StrOpt('ca_file',
|
||||||
|
help='Optional CA cert file to use in SSL connections.'),
|
||||||
|
cfg.StrOpt('cert_file',
|
||||||
|
help='Optional PEM-formatted certificate chain file.'),
|
||||||
|
cfg.StrOpt('key_file',
|
||||||
|
help='Optional PEM-formatted file that contains the '
|
||||||
|
'private key.'),
|
||||||
|
cfg.BoolOpt('insecure',
|
||||||
|
default=False,
|
||||||
|
help="If set, then the server's certificate will not "
|
||||||
|
"be verified.")]
|
||||||
|
|
||||||
|
ALL_OPTS = (gyan_client_opts + common_security_opts)
|
||||||
|
|
||||||
|
|
||||||
|
def register_opts(conf):
|
||||||
|
conf.register_group(gyan_group)
|
||||||
|
conf.register_opts(gyan_client_opts, group=gyan_group)
|
||||||
|
|
||||||
|
|
||||||
|
def list_opts():
|
||||||
|
return {gyan_group: ALL_OPTS}
|
35
gyan/conf/keystone.py
Normal file
35
gyan/conf/keystone.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# 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 keystoneauth1 import loading as ka_loading
|
||||||
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
CFG_GROUP = 'keystone_auth'
|
||||||
|
CFG_LEGACY_GROUP = 'keystone_authtoken'
|
||||||
|
|
||||||
|
keystone_auth_group = cfg.OptGroup(name=CFG_GROUP,
|
||||||
|
title='Options for Keystone in Gyan')
|
||||||
|
|
||||||
|
|
||||||
|
def register_opts(conf):
|
||||||
|
conf.import_group(CFG_LEGACY_GROUP, 'keystonemiddleware.auth_token')
|
||||||
|
ka_loading.register_auth_conf_options(conf, CFG_GROUP)
|
||||||
|
ka_loading.register_session_conf_options(conf, CFG_GROUP)
|
||||||
|
conf.set_default('auth_type', default='password', group=CFG_GROUP)
|
||||||
|
|
||||||
|
|
||||||
|
def list_opts():
|
||||||
|
keystone_auth_opts = (ka_loading.get_auth_common_conf_options() +
|
||||||
|
ka_loading.get_auth_plugin_conf_options('password'))
|
||||||
|
return {
|
||||||
|
keystone_auth_group: keystone_auth_opts
|
||||||
|
}
|
47
gyan/conf/ml_model_driver.py
Normal file
47
gyan/conf/ml_model_driver.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
# implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
driver_opts = [
|
||||||
|
cfg.StrOpt('ml_model_driver',
|
||||||
|
default='gyan.ml_model.tensorflow.driver.TensorflowDriver',
|
||||||
|
help="""Defines which driver to use for controlling ml_model.
|
||||||
|
Possible values:
|
||||||
|
|
||||||
|
* ``ml_model.driver.TensorflowDriver``
|
||||||
|
|
||||||
|
Services which consume this:
|
||||||
|
|
||||||
|
* ``gyan-compute``
|
||||||
|
|
||||||
|
Interdependencies to other options:
|
||||||
|
|
||||||
|
* None
|
||||||
|
"""),
|
||||||
|
cfg.IntOpt('default_sleep_time', default=1,
|
||||||
|
help='Time to sleep (in seconds) during waiting for an event.'),
|
||||||
|
cfg.IntOpt('default_timeout', default=60 * 10,
|
||||||
|
help='Maximum time (in seconds) to wait for an event.')
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
ALL_OPTS = (driver_opts)
|
||||||
|
|
||||||
|
|
||||||
|
def register_opts(conf):
|
||||||
|
conf.register_opts(ALL_OPTS)
|
||||||
|
|
||||||
|
|
||||||
|
def list_opts():
|
||||||
|
return {"DEFAULT": ALL_OPTS}
|
76
gyan/conf/opts.py
Normal file
76
gyan/conf/opts.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
This is the single point of entry to generate the sample configuration
|
||||||
|
file for Gyan. It collects all the necessary info from the other modules
|
||||||
|
in this package. It is assumed that:
|
||||||
|
|
||||||
|
* every other module in this package has a 'list_opts' function which
|
||||||
|
return a dict where
|
||||||
|
* the keys are strings which are the group names
|
||||||
|
* the value of each key is a list of config options for that group
|
||||||
|
* the gyan.conf package doesn't have further packages with config options
|
||||||
|
* this module is only used in the context of sample file generation
|
||||||
|
"""
|
||||||
|
|
||||||
|
import collections
|
||||||
|
import importlib
|
||||||
|
import os
|
||||||
|
import pkgutil
|
||||||
|
|
||||||
|
LIST_OPTS_FUNC_NAME = "list_opts"
|
||||||
|
|
||||||
|
|
||||||
|
def _tupleize(dct):
|
||||||
|
"""Take the dict of options and convert to the 2-tuple format."""
|
||||||
|
return [(key, val) for key, val in dct.items()]
|
||||||
|
|
||||||
|
|
||||||
|
def list_opts():
|
||||||
|
opts = collections.defaultdict(list)
|
||||||
|
module_names = _list_module_names()
|
||||||
|
imported_modules = _import_modules(module_names)
|
||||||
|
_append_config_options(imported_modules, opts)
|
||||||
|
return _tupleize(opts)
|
||||||
|
|
||||||
|
|
||||||
|
def _list_module_names():
|
||||||
|
module_names = []
|
||||||
|
package_path = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
for _, modname, ispkg in pkgutil.iter_modules(path=[package_path]):
|
||||||
|
if modname == "opts" or ispkg:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
module_names.append(modname)
|
||||||
|
return module_names
|
||||||
|
|
||||||
|
|
||||||
|
def _import_modules(module_names):
|
||||||
|
imported_modules = []
|
||||||
|
for modname in module_names:
|
||||||
|
mod = importlib.import_module("gyan.conf." + modname)
|
||||||
|
if not hasattr(mod, LIST_OPTS_FUNC_NAME):
|
||||||
|
msg = "The module 'gyan.conf.%s' should have a '%s' "\
|
||||||
|
"function which returns the config options." % \
|
||||||
|
(modname, LIST_OPTS_FUNC_NAME)
|
||||||
|
raise AttributeError(msg)
|
||||||
|
else:
|
||||||
|
imported_modules.append(mod)
|
||||||
|
return imported_modules
|
||||||
|
|
||||||
|
|
||||||
|
def _append_config_options(imported_modules, config_options):
|
||||||
|
for mod in imported_modules:
|
||||||
|
configs = mod.list_opts()
|
||||||
|
for key, val in configs.items():
|
||||||
|
config_options[key].extend(val)
|
42
gyan/conf/path.py
Normal file
42
gyan/conf/path.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# 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 os
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
|
||||||
|
path_opts = [
|
||||||
|
cfg.StrOpt('pybasedir',
|
||||||
|
default=os.path.abspath(os.path.join(os.path.dirname(__file__),
|
||||||
|
'../')),
|
||||||
|
help='Directory where the gyan python module is installed.'),
|
||||||
|
cfg.StrOpt('bindir',
|
||||||
|
default='$pybasedir/bin',
|
||||||
|
help='Directory where gyan binaries are installed.'),
|
||||||
|
cfg.StrOpt('state_path',
|
||||||
|
default='$pybasedir',
|
||||||
|
help="Top-level directory for maintaining gyan's state."),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def state_path_def(*args):
|
||||||
|
"""Return an uninterpolated path relative to $state_path."""
|
||||||
|
return os.path.join('$state_path', *args)
|
||||||
|
|
||||||
|
|
||||||
|
def register_opts(conf):
|
||||||
|
conf.register_opts(path_opts)
|
||||||
|
|
||||||
|
|
||||||
|
def list_opts():
|
||||||
|
return {"DEFAULT": path_opts}
|
29
gyan/conf/profiler.py
Normal file
29
gyan/conf/profiler.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# 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_utils import importutils
|
||||||
|
|
||||||
|
|
||||||
|
profiler_opts = importutils.try_import('osprofiler.opts')
|
||||||
|
|
||||||
|
|
||||||
|
def register_opts(conf):
|
||||||
|
if profiler_opts:
|
||||||
|
profiler_opts.set_defaults(conf)
|
||||||
|
|
||||||
|
|
||||||
|
def list_opts():
|
||||||
|
if not profiler_opts:
|
||||||
|
return {}
|
||||||
|
return {
|
||||||
|
profiler_opts._profiler_opt_group: profiler_opts._PROFILER_OPTS
|
||||||
|
}
|
104
gyan/conf/scheduler.py
Normal file
104
gyan/conf/scheduler.py
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
# Copyright 2015 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.
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
|
||||||
|
scheduler_group = cfg.OptGroup(name="scheduler",
|
||||||
|
title="Scheduler configuration")
|
||||||
|
|
||||||
|
scheduler_opts = [
|
||||||
|
cfg.StrOpt("driver",
|
||||||
|
default="filter_scheduler",
|
||||||
|
choices=("chance_scheduler", "fake_scheduler",
|
||||||
|
"filter_scheduler"),
|
||||||
|
help="""
|
||||||
|
The class of the driver used by the scheduler.
|
||||||
|
|
||||||
|
The options are chosen from the entry points under the namespace
|
||||||
|
'gyan.scheduler.driver' in 'setup.cfg'.
|
||||||
|
|
||||||
|
Possible values:
|
||||||
|
|
||||||
|
* A string, where the string corresponds to the class name of a scheduler
|
||||||
|
driver. There are a number of options available:
|
||||||
|
** 'chance_scheduler', which simply picks a host at random
|
||||||
|
** A custom scheduler driver. In this case, you will be responsible for
|
||||||
|
creating and maintaining the entry point in your 'setup.cfg' file
|
||||||
|
"""),
|
||||||
|
cfg.MultiStrOpt("available_filters",
|
||||||
|
default=["gyan.scheduler.filters.all_filters"],
|
||||||
|
help="""
|
||||||
|
Filters that the scheduler can use.
|
||||||
|
|
||||||
|
An unordered list of the filter classes the gyan scheduler may apply. Only the
|
||||||
|
filters specified in the 'scheduler_enabled_filters' option will be used, but
|
||||||
|
any filter appearing in that option must also be included in this list.
|
||||||
|
|
||||||
|
By default, this is set to all filters that are included with gyan.
|
||||||
|
|
||||||
|
This option is only used by the FilterScheduler and its subclasses; if you use
|
||||||
|
a different scheduler, this option has no effect.
|
||||||
|
|
||||||
|
Possible values:
|
||||||
|
|
||||||
|
* A list of zero or more strings, where each string corresponds to the name of
|
||||||
|
a filter that may be used for selecting a host
|
||||||
|
|
||||||
|
Related options:
|
||||||
|
|
||||||
|
* scheduler_enabled_filters
|
||||||
|
"""),
|
||||||
|
cfg.ListOpt("enabled_filters",
|
||||||
|
default=[
|
||||||
|
"AvailabilityZoneFilter",
|
||||||
|
"CPUFilter",
|
||||||
|
"RamFilter",
|
||||||
|
"ComputeFilter",
|
||||||
|
"DiskFilter",
|
||||||
|
],
|
||||||
|
help="""
|
||||||
|
Filters that the scheduler will use.
|
||||||
|
|
||||||
|
An ordered list of filter class names that will be used for filtering
|
||||||
|
hosts. Ignore the word 'default' in the name of this option: these filters will
|
||||||
|
*always* be applied, and they will be applied in the order they are listed so
|
||||||
|
place your most restrictive filters first to make the filtering process more
|
||||||
|
efficient.
|
||||||
|
|
||||||
|
This option is only used by the FilterScheduler and its subclasses; if you use
|
||||||
|
a different scheduler, this option has no effect.
|
||||||
|
|
||||||
|
Possible values:
|
||||||
|
|
||||||
|
* A list of zero or more strings, where each string corresponds to the name of
|
||||||
|
a filter to be used for selecting a host
|
||||||
|
|
||||||
|
Related options:
|
||||||
|
|
||||||
|
* All of the filters in this option *must* be present in the
|
||||||
|
'scheduler_available_filters' option, or a SchedulerHostFilterNotFound
|
||||||
|
exception will be raised.
|
||||||
|
"""),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def register_opts(conf):
|
||||||
|
conf.register_group(scheduler_group)
|
||||||
|
conf.register_opts(scheduler_opts, group=scheduler_group)
|
||||||
|
|
||||||
|
|
||||||
|
def list_opts():
|
||||||
|
return {scheduler_group: scheduler_opts}
|
36
gyan/conf/services.py
Normal file
36
gyan/conf/services.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
|
||||||
|
periodic_opts = [
|
||||||
|
cfg.IntOpt('periodic_interval_max',
|
||||||
|
default=60,
|
||||||
|
help='Max interval size between periodic tasks execution in '
|
||||||
|
'seconds.'),
|
||||||
|
cfg.IntOpt('service_down_time',
|
||||||
|
default=180,
|
||||||
|
help='Max interval size between periodic tasks execution in '
|
||||||
|
'seconds.')
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
ALL_OPTS = (periodic_opts)
|
||||||
|
|
||||||
|
|
||||||
|
def register_opts(conf):
|
||||||
|
conf.register_opts(ALL_OPTS)
|
||||||
|
|
||||||
|
|
||||||
|
def list_opts():
|
||||||
|
return {"DEFAULT": ALL_OPTS}
|
27
gyan/conf/ssl.py
Normal file
27
gyan/conf/ssl.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_service import sslutils
|
||||||
|
|
||||||
|
|
||||||
|
def register_opts(conf):
|
||||||
|
sslutils.register_opts(conf)
|
||||||
|
|
||||||
|
|
||||||
|
def list_opts():
|
||||||
|
group_name, ssl_opts = sslutils.list_opts()[0]
|
||||||
|
ssl_group = cfg.OptGroup(name=group_name,
|
||||||
|
title='Options for the ssl')
|
||||||
|
return {
|
||||||
|
ssl_group: ssl_opts
|
||||||
|
}
|
31
gyan/conf/utils.py
Normal file
31
gyan/conf/utils.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
# use this file except in compliance with the License. You may obtain a copy
|
||||||
|
# of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
|
||||||
|
utils_opts = [
|
||||||
|
cfg.StrOpt('rootwrap_config',
|
||||||
|
default="/etc/gyan/rootwrap.conf",
|
||||||
|
help='Path to the rootwrap configuration file to use for '
|
||||||
|
'running commands as root.'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def register_opts(conf):
|
||||||
|
conf.register_opts(utils_opts)
|
||||||
|
|
||||||
|
|
||||||
|
def list_opts():
|
||||||
|
return {
|
||||||
|
"DEFAULT": utils_opts
|
||||||
|
}
|
21
gyan/db/__init__.py
Normal file
21
gyan/db/__init__.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# 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_db import options
|
||||||
|
|
||||||
|
from gyan.common import paths
|
||||||
|
import gyan.conf
|
||||||
|
|
||||||
|
_DEFAULT_SQL_CONNECTION = 'sqlite:///' + paths.state_path_def('gyan.sqlite')
|
||||||
|
|
||||||
|
options.set_defaults(gyan.conf.CONF)
|
||||||
|
options.set_defaults(gyan.conf.CONF, _DEFAULT_SQL_CONNECTION)
|
197
gyan/db/api.py
Normal file
197
gyan/db/api.py
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
# 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.
|
||||||
|
"""
|
||||||
|
Base API for Database
|
||||||
|
"""
|
||||||
|
|
||||||
|
from oslo_db import api as db_api
|
||||||
|
|
||||||
|
from gyan.common import profiler
|
||||||
|
import gyan.conf
|
||||||
|
|
||||||
|
"""Add the database backend mapping here"""
|
||||||
|
|
||||||
|
CONF = gyan.conf.CONF
|
||||||
|
_BACKEND_MAPPING = {'sqlalchemy': 'gyan.db.sqlalchemy.api'}
|
||||||
|
IMPL = db_api.DBAPI.from_config(CONF,
|
||||||
|
backend_mapping=_BACKEND_MAPPING,
|
||||||
|
lazy=True)
|
||||||
|
|
||||||
|
|
||||||
|
@profiler.trace("db")
|
||||||
|
def _get_dbdriver_instance():
|
||||||
|
"""Return a DB API instance."""
|
||||||
|
return IMPL
|
||||||
|
|
||||||
|
|
||||||
|
@profiler.trace("db")
|
||||||
|
def list_ml_models(context, filters=None, limit=None, marker=None,
|
||||||
|
sort_key=None, sort_dir=None):
|
||||||
|
"""List matching ML Models.
|
||||||
|
|
||||||
|
Return a list of the specified columns for all ml models that match
|
||||||
|
the specified filters.
|
||||||
|
|
||||||
|
:param context: The security context
|
||||||
|
:param filters: Filters to apply. Defaults to None.
|
||||||
|
:param limit: Maximum number of ml_models to return.
|
||||||
|
:param marker: the last item of the previous page; we return the next
|
||||||
|
result set.
|
||||||
|
:param sort_key: Attribute by which results should be sorted.
|
||||||
|
:param sort_dir: Direction in which results should be sorted.
|
||||||
|
(asc, desc)
|
||||||
|
:returns: A list of tuples of the specified columns.
|
||||||
|
"""
|
||||||
|
return _get_dbdriver_instance().list_ml_models(
|
||||||
|
context, filters, limit, marker, sort_key, sort_dir)
|
||||||
|
|
||||||
|
|
||||||
|
@profiler.trace("db")
|
||||||
|
def create_ml_model(context, values):
|
||||||
|
"""Create a new ML Model.
|
||||||
|
|
||||||
|
:param context: The security context
|
||||||
|
:param values: A dict containing several items used to identify
|
||||||
|
and track the ML Model
|
||||||
|
:returns: A ML Model.
|
||||||
|
"""
|
||||||
|
return _get_dbdriver_instance().create_ml_model(context, values)
|
||||||
|
|
||||||
|
|
||||||
|
@profiler.trace("db")
|
||||||
|
def get_ml_model_by_uuid(context, ml_model_uuid):
|
||||||
|
"""Return a ML Model.
|
||||||
|
|
||||||
|
:param context: The security context
|
||||||
|
:param ml_model_uuid: The uuid of a ml model.
|
||||||
|
:returns: A ML Model.
|
||||||
|
"""
|
||||||
|
return _get_dbdriver_instance().get_ml_model_by_uuid(
|
||||||
|
context, ml_model_uuid)
|
||||||
|
|
||||||
|
|
||||||
|
@profiler.trace("db")
|
||||||
|
def get_ml_model_by_name(context, ml_model_name):
|
||||||
|
"""Return a ML Model.
|
||||||
|
|
||||||
|
:param context: The security context
|
||||||
|
:param ml_model_name: The name of a ML Model.
|
||||||
|
:returns: A ML Model.
|
||||||
|
"""
|
||||||
|
return _get_dbdriver_instance().get_ml_model_by_name(
|
||||||
|
context, ml_model_name)
|
||||||
|
|
||||||
|
|
||||||
|
@profiler.trace("db")
|
||||||
|
def destroy_ml_model(context, ml_model_id):
|
||||||
|
"""Destroy a ml model and all associated interfaces.
|
||||||
|
|
||||||
|
:param context: Request context
|
||||||
|
:param ml_model_id: The id or uuid of a ml model.
|
||||||
|
"""
|
||||||
|
return _get_dbdriver_instance().destroy_ml_model(context, ml_model_id)
|
||||||
|
|
||||||
|
|
||||||
|
@profiler.trace("db")
|
||||||
|
def update_ml_model(context, ml_model_id, values):
|
||||||
|
"""Update properties of a ml model.
|
||||||
|
|
||||||
|
:param context: Request context
|
||||||
|
:param ml_model_id: The id or uuid of a ml model.
|
||||||
|
:param values: The properties to be updated
|
||||||
|
:returns: A ML Model.
|
||||||
|
:raises: MLModelNotFound
|
||||||
|
"""
|
||||||
|
return _get_dbdriver_instance().update_ml_model(
|
||||||
|
context, ml_model_id, values)
|
||||||
|
|
||||||
|
|
||||||
|
@profiler.trace("db")
|
||||||
|
def list_compute_hosts(context, filters=None, limit=None, marker=None,
|
||||||
|
sort_key=None, sort_dir=None):
|
||||||
|
"""List matching compute hosts.
|
||||||
|
|
||||||
|
Return a list of the specified columns for all compute hosts that match
|
||||||
|
the specified filters.
|
||||||
|
|
||||||
|
:param context: The security context
|
||||||
|
:param filters: Filters to apply. Defaults to None.
|
||||||
|
:param limit: Maximum number of compute nodes to return.
|
||||||
|
:param marker: the last item of the previous page; we return the next
|
||||||
|
result set.
|
||||||
|
:param sort_key: Attribute by which results should be sorted.
|
||||||
|
:param sort_dir: Direction in which results should be sorted.
|
||||||
|
(asc, desc)
|
||||||
|
:returns: A list of tuples of the specified columns.
|
||||||
|
"""
|
||||||
|
return _get_dbdriver_instance().list_compute_hosts(
|
||||||
|
context, filters, limit, marker, sort_key, sort_dir)
|
||||||
|
|
||||||
|
|
||||||
|
@profiler.trace("db")
|
||||||
|
def create_compute_host(context, values):
|
||||||
|
"""Create a new compute host.
|
||||||
|
|
||||||
|
:param context: The security context
|
||||||
|
:param values: A dict containing several items used to identify
|
||||||
|
and track the compute node, and several dicts which are
|
||||||
|
passed into the Drivers when managing this compute host.
|
||||||
|
:returns: A compute host.
|
||||||
|
"""
|
||||||
|
return _get_dbdriver_instance().create_compute_host(context, values)
|
||||||
|
|
||||||
|
|
||||||
|
@profiler.trace("db")
|
||||||
|
def get_compute_host(context, host_uuid):
|
||||||
|
"""Return a compute host.
|
||||||
|
|
||||||
|
:param context: The security context
|
||||||
|
:param node_uuid: The uuid of a compute node.
|
||||||
|
:returns: A compute node.
|
||||||
|
"""
|
||||||
|
return _get_dbdriver_instance().get_compute_host(context, host_uuid)
|
||||||
|
|
||||||
|
|
||||||
|
@profiler.trace("db")
|
||||||
|
def get_compute_host_by_hostname(context, hostname):
|
||||||
|
"""Return a compute node.
|
||||||
|
|
||||||
|
:param context: The security context
|
||||||
|
:param hostname: The hostname of a compute node.
|
||||||
|
:returns: A compute node.
|
||||||
|
"""
|
||||||
|
return _get_dbdriver_instance().get_compute_host_by_hostname(
|
||||||
|
context, hostname)
|
||||||
|
|
||||||
|
|
||||||
|
@profiler.trace("db")
|
||||||
|
def destroy_compute_host(context, host_uuid):
|
||||||
|
"""Destroy a compute node and all associated interfaces.
|
||||||
|
|
||||||
|
:param context: Request context
|
||||||
|
:param node_uuid: The uuid of a compute node.
|
||||||
|
"""
|
||||||
|
return _get_dbdriver_instance().destroy_compute_host(context, host_uuid)
|
||||||
|
|
||||||
|
|
||||||
|
@profiler.trace("db")
|
||||||
|
def update_compute_host(context, host_uuid, values):
|
||||||
|
"""Update properties of a compute node.
|
||||||
|
|
||||||
|
:param context: Request context
|
||||||
|
:param node_uuid: The uuid of a compute node.
|
||||||
|
:param values: The properties to be updated
|
||||||
|
:returns: A compute node.
|
||||||
|
:raises: ComputeNodeNotFound
|
||||||
|
"""
|
||||||
|
return _get_dbdriver_instance().update_compute_host(
|
||||||
|
context, host_uuid, values)
|
47
gyan/db/migration.py
Normal file
47
gyan/db/migration.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""Database setup and migration commands."""
|
||||||
|
|
||||||
|
from stevedore import driver
|
||||||
|
|
||||||
|
import gyan.conf
|
||||||
|
|
||||||
|
_IMPL = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_backend():
|
||||||
|
global _IMPL
|
||||||
|
if not _IMPL:
|
||||||
|
gyan.conf.CONF.import_opt('backend',
|
||||||
|
'oslo_db.options', group='database')
|
||||||
|
_IMPL = driver.DriverManager("gyan.database.migration_backend",
|
||||||
|
gyan.conf.CONF.database.backend).driver
|
||||||
|
return _IMPL
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade(version=None):
|
||||||
|
"""Migrate the database to `version` or the most recent version."""
|
||||||
|
return get_backend().upgrade(version)
|
||||||
|
|
||||||
|
|
||||||
|
def version():
|
||||||
|
return get_backend().version()
|
||||||
|
|
||||||
|
|
||||||
|
def stamp(version):
|
||||||
|
return get_backend().stamp(version)
|
||||||
|
|
||||||
|
|
||||||
|
def revision(message, autogenerate):
|
||||||
|
return get_backend().revision(message, autogenerate)
|
0
gyan/db/sqlalchemy/__init__.py
Normal file
0
gyan/db/sqlalchemy/__init__.py
Normal file
68
gyan/db/sqlalchemy/alembic.ini
Normal file
68
gyan/db/sqlalchemy/alembic.ini
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
# A generic, single database configuration.
|
||||||
|
|
||||||
|
[alembic]
|
||||||
|
# path to migration scripts
|
||||||
|
script_location = %(here)s/alembic
|
||||||
|
|
||||||
|
# template used to generate migration files
|
||||||
|
# file_template = %%(rev)s_%%(slug)s
|
||||||
|
|
||||||
|
# max length of characters to apply to the
|
||||||
|
# "slug" field
|
||||||
|
#truncate_slug_length = 40
|
||||||
|
|
||||||
|
# set to 'true' to run the environment during
|
||||||
|
# the 'revision' command, regardless of autogenerate
|
||||||
|
# revision_environment = false
|
||||||
|
|
||||||
|
# set to 'true' to allow .pyc and .pyo files without
|
||||||
|
# a source .py file to be detected as revisions in the
|
||||||
|
# versions/ directory
|
||||||
|
# sourceless = false
|
||||||
|
|
||||||
|
# version location specification; this defaults
|
||||||
|
# to alembic/versions. When using multiple version
|
||||||
|
# directories, initial revisions must be specified with --version-path
|
||||||
|
# version_locations = %(here)s/bar %(here)s/bat alembic/versions
|
||||||
|
|
||||||
|
# the output encoding used when revision files
|
||||||
|
# are written from script.py.mako
|
||||||
|
# output_encoding = utf-8
|
||||||
|
|
||||||
|
#sqlalchemy.url = driver://user:pass@localhost/dbname
|
||||||
|
|
||||||
|
|
||||||
|
# Logging configuration
|
||||||
|
[loggers]
|
||||||
|
keys = root,sqlalchemy,alembic
|
||||||
|
|
||||||
|
[handlers]
|
||||||
|
keys = console
|
||||||
|
|
||||||
|
[formatters]
|
||||||
|
keys = generic
|
||||||
|
|
||||||
|
[logger_root]
|
||||||
|
level = WARN
|
||||||
|
handlers = console
|
||||||
|
qualname =
|
||||||
|
|
||||||
|
[logger_sqlalchemy]
|
||||||
|
level = WARN
|
||||||
|
handlers =
|
||||||
|
qualname = sqlalchemy.engine
|
||||||
|
|
||||||
|
[logger_alembic]
|
||||||
|
level = INFO
|
||||||
|
handlers =
|
||||||
|
qualname = alembic
|
||||||
|
|
||||||
|
[handler_console]
|
||||||
|
class = StreamHandler
|
||||||
|
args = (sys.stderr,)
|
||||||
|
level = NOTSET
|
||||||
|
formatter = generic
|
||||||
|
|
||||||
|
[formatter_generic]
|
||||||
|
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||||
|
datefmt = %H:%M:%S
|
11
gyan/db/sqlalchemy/alembic/README
Normal file
11
gyan/db/sqlalchemy/alembic/README
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
Please see https://alembic.readthedocs.org/en/latest/index.html for general documentation
|
||||||
|
|
||||||
|
To create alembic migrations use:
|
||||||
|
$ gyan-db-manage revision --message "description of revision" --autogenerate
|
||||||
|
|
||||||
|
Stamp db with most recent migration version, without actually running migrations
|
||||||
|
$ gyan-db-manage stamp head
|
||||||
|
|
||||||
|
Upgrade can be performed by:
|
||||||
|
$ gyan-db-manage upgrade
|
||||||
|
$ gyan-db-manage upgrade head
|
58
gyan/db/sqlalchemy/alembic/env.py
Normal file
58
gyan/db/sqlalchemy/alembic/env.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
# 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 with_statement
|
||||||
|
from alembic import context
|
||||||
|
from logging.config import fileConfig
|
||||||
|
|
||||||
|
from gyan.db.sqlalchemy import api as sqla_api
|
||||||
|
from gyan.db.sqlalchemy import models
|
||||||
|
# this is the Alembic Config object, which provides
|
||||||
|
# access to the values within the .ini file in use.
|
||||||
|
config = context.config
|
||||||
|
|
||||||
|
# Interpret the config file for Python logging.
|
||||||
|
# This line sets up loggers basically.
|
||||||
|
fileConfig(config.config_file_name)
|
||||||
|
|
||||||
|
# add your model's MetaData object here
|
||||||
|
# for 'autogenerate' support
|
||||||
|
# from myapp import mymodel
|
||||||
|
# target_metadata = mymodel.Base.metadata
|
||||||
|
target_metadata = models.Base.metadata
|
||||||
|
|
||||||
|
# other values from the config, defined by the needs of env.py,
|
||||||
|
# can be acquired:
|
||||||
|
# my_important_option = config.get_main_option("my_important_option")
|
||||||
|
# ... etc.
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_online():
|
||||||
|
"""Run migrations in 'online' mode.
|
||||||
|
|
||||||
|
In this scenario we need to create an Engine
|
||||||
|
and associate a connection with the context.
|
||||||
|
|
||||||
|
"""
|
||||||
|
engine = sqla_api.get_engine()
|
||||||
|
with engine.connect() as connection:
|
||||||
|
context.configure(
|
||||||
|
connection=connection,
|
||||||
|
target_metadata=target_metadata,
|
||||||
|
render_as_batch=True
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
run_migrations_online()
|
20
gyan/db/sqlalchemy/alembic/script.py.mako
Normal file
20
gyan/db/sqlalchemy/alembic/script.py.mako
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
"""${message}
|
||||||
|
|
||||||
|
Revision ID: ${up_revision}
|
||||||
|
Revises: ${down_revision | comma,n}
|
||||||
|
Create Date: ${create_date}
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = ${repr(up_revision)}
|
||||||
|
down_revision = ${repr(down_revision)}
|
||||||
|
branch_labels = ${repr(branch_labels)}
|
||||||
|
depends_on = ${repr(depends_on)}
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
${imports if imports else ""}
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
${upgrades if upgrades else "pass"}
|
@ -0,0 +1,48 @@
|
|||||||
|
"""Add Model and Host tables
|
||||||
|
|
||||||
|
Revision ID: cebd81b206ca
|
||||||
|
Create Date: 2018-10-09 09:57:20.823110
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'cebd81b206ca'
|
||||||
|
down_revision = None
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table(
|
||||||
|
'compute_host',
|
||||||
|
sa.Column('id', sa.String(length=255), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('hostname', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('status', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('type', sa.String(length=255), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table(
|
||||||
|
'ml_model',
|
||||||
|
sa.Column('id', sa.String(length=255), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('host_id', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('name', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('project_id', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('user_id', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('status', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('status_reason', sa.Text, nullable=True),
|
||||||
|
sa.Column('url', sa.Text, nullable=True),
|
||||||
|
sa.Column('hints', sa.Text, nullable=True),
|
||||||
|
sa.Column('deployed', sa.Boolean(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.ForeignKeyConstraint(['host_id'], ['compute_host.id'])
|
||||||
|
)
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
306
gyan/db/sqlalchemy/api.py
Normal file
306
gyan/db/sqlalchemy/api.py
Normal file
@ -0,0 +1,306 @@
|
|||||||
|
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""SQLAlchemy storage backend."""
|
||||||
|
|
||||||
|
from oslo_db import exception as db_exc
|
||||||
|
from oslo_db.sqlalchemy import session as db_session
|
||||||
|
from oslo_db.sqlalchemy import utils as db_utils
|
||||||
|
from oslo_utils import importutils
|
||||||
|
from oslo_utils import strutils
|
||||||
|
from oslo_utils import timeutils
|
||||||
|
from oslo_utils import uuidutils
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.orm import contains_eager
|
||||||
|
from sqlalchemy.orm.exc import MultipleResultsFound
|
||||||
|
from sqlalchemy.orm.exc import NoResultFound
|
||||||
|
from sqlalchemy.sql.expression import desc
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
|
||||||
|
from gyan.common import consts
|
||||||
|
from gyan.common import exception
|
||||||
|
from gyan.common.i18n import _
|
||||||
|
import gyan.conf
|
||||||
|
from gyan.db.sqlalchemy import models
|
||||||
|
|
||||||
|
profiler_sqlalchemy = importutils.try_import('osprofiler.sqlalchemy')
|
||||||
|
|
||||||
|
CONF = gyan.conf.CONF
|
||||||
|
|
||||||
|
_FACADE = None
|
||||||
|
|
||||||
|
|
||||||
|
def _create_facade_lazily():
|
||||||
|
global _FACADE
|
||||||
|
if _FACADE is None:
|
||||||
|
_FACADE = db_session.enginefacade.get_legacy_facade()
|
||||||
|
if profiler_sqlalchemy:
|
||||||
|
if CONF.profiler.enabled and CONF.profiler.trace_sqlalchemy:
|
||||||
|
profiler_sqlalchemy.add_tracing(sa, _FACADE.get_engine(), "db")
|
||||||
|
return _FACADE
|
||||||
|
|
||||||
|
|
||||||
|
def get_engine():
|
||||||
|
facade = _create_facade_lazily()
|
||||||
|
return facade.get_engine()
|
||||||
|
|
||||||
|
|
||||||
|
def get_session(**kwargs):
|
||||||
|
facade = _create_facade_lazily()
|
||||||
|
return facade.get_session(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def get_backend():
|
||||||
|
"""The backend is this module itself."""
|
||||||
|
return Connection()
|
||||||
|
|
||||||
|
|
||||||
|
def model_query(model, *args, **kwargs):
|
||||||
|
"""Query helper for simpler session usage.
|
||||||
|
|
||||||
|
:param session: if present, the session to use
|
||||||
|
"""
|
||||||
|
|
||||||
|
session = kwargs.get('session') or get_session()
|
||||||
|
query = session.query(model, *args)
|
||||||
|
return query
|
||||||
|
|
||||||
|
|
||||||
|
def add_identity_filter(query, value):
|
||||||
|
"""Adds an identity filter to a query.
|
||||||
|
|
||||||
|
Filters results by ID, if supplied value is a valid integer.
|
||||||
|
Otherwise attempts to filter results by UUID.
|
||||||
|
|
||||||
|
:param query: Initial query to add filter to.
|
||||||
|
:param value: Value for filtering results by.
|
||||||
|
:return: Modified query.
|
||||||
|
"""
|
||||||
|
if strutils.is_int_like(value):
|
||||||
|
return query.filter_by(id=value)
|
||||||
|
elif uuidutils.is_uuid_like(value):
|
||||||
|
return query.filter_by(uuid=value)
|
||||||
|
else:
|
||||||
|
raise exception.InvalidIdentity(identity=value)
|
||||||
|
|
||||||
|
|
||||||
|
def _paginate_query(model, limit=None, marker=None, sort_key=None,
|
||||||
|
sort_dir=None, query=None, default_sort_key='id'):
|
||||||
|
if not query:
|
||||||
|
query = model_query(model)
|
||||||
|
sort_keys = [default_sort_key]
|
||||||
|
if sort_key and sort_key not in sort_keys:
|
||||||
|
sort_keys.insert(0, sort_key)
|
||||||
|
try:
|
||||||
|
query = db_utils.paginate_query(query, model, limit, sort_keys,
|
||||||
|
marker=marker, sort_dir=sort_dir)
|
||||||
|
except db_exc.InvalidSortKey:
|
||||||
|
raise exception.InvalidParameterValue(
|
||||||
|
_('The sort_key value "%(key)s" is an invalid field for sorting')
|
||||||
|
% {'key': sort_key})
|
||||||
|
return query.all()
|
||||||
|
|
||||||
|
|
||||||
|
class Connection(object):
|
||||||
|
"""SqlAlchemy connection."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _add_project_filters(self, context, query):
|
||||||
|
if context.is_admin and context.all_projects:
|
||||||
|
return query
|
||||||
|
|
||||||
|
if context.project_id:
|
||||||
|
query = query.filter_by(project_id=context.project_id)
|
||||||
|
else:
|
||||||
|
query = query.filter_by(user_id=context.user_id)
|
||||||
|
|
||||||
|
return query
|
||||||
|
|
||||||
|
def _add_filters(self, query, model, filters=None, filter_names=None):
|
||||||
|
"""Generic way to add filters to a Gyan model"""
|
||||||
|
if not filters:
|
||||||
|
return query
|
||||||
|
|
||||||
|
if not filter_names:
|
||||||
|
filter_names = []
|
||||||
|
|
||||||
|
for name in filter_names:
|
||||||
|
if name in filters:
|
||||||
|
value = filters[name]
|
||||||
|
if isinstance(value, list):
|
||||||
|
column = getattr(model, name)
|
||||||
|
query = query.filter(column.in_(value))
|
||||||
|
else:
|
||||||
|
column = getattr(model, name)
|
||||||
|
query = query.filter(column == value)
|
||||||
|
|
||||||
|
return query
|
||||||
|
|
||||||
|
def _add_compute_hosts_filters(self, query, filters):
|
||||||
|
filter_names = None
|
||||||
|
return self._add_filters(query, models.ComputeHost, filters=filters,
|
||||||
|
filter_names=filter_names)
|
||||||
|
|
||||||
|
def list_compute_hosts(self, context, filters=None, limit=None,
|
||||||
|
marker=None, sort_key=None, sort_dir=None):
|
||||||
|
query = model_query(models.ComputeHost)
|
||||||
|
query = self._add_compute_hosts_filters(query, filters)
|
||||||
|
return _paginate_query(models.ComputeHost, limit, marker,
|
||||||
|
sort_key, sort_dir, query,
|
||||||
|
default_sort_key='id')
|
||||||
|
|
||||||
|
def create_compute_host(self, context, values):
|
||||||
|
# ensure defaults are present for new compute hosts
|
||||||
|
if not values.get('id'):
|
||||||
|
values['id'] = uuidutils.generate_uuid()
|
||||||
|
|
||||||
|
compute_host = models.ComputeHost()
|
||||||
|
compute_host.update(values)
|
||||||
|
try:
|
||||||
|
compute_host.save()
|
||||||
|
except db_exc.DBDuplicateEntry:
|
||||||
|
raise exception.ComputeHostAlreadyExists(
|
||||||
|
field='UUID', value=values['uuid'])
|
||||||
|
return compute_host
|
||||||
|
|
||||||
|
def get_compute_host(self, context, host_uuid):
|
||||||
|
query = model_query(models.ComputeHost)
|
||||||
|
query = query.filter_by(id=host_uuid)
|
||||||
|
try:
|
||||||
|
return query.one()
|
||||||
|
except NoResultFound:
|
||||||
|
raise exception.ComputeHostNotFound(
|
||||||
|
compute_host=host_uuid)
|
||||||
|
|
||||||
|
def get_compute_host_by_hostname(self, context, hostname):
|
||||||
|
query = model_query(models.ComputeHost)
|
||||||
|
query = query.filter_by(hostname=hostname)
|
||||||
|
try:
|
||||||
|
return query.one()
|
||||||
|
except NoResultFound:
|
||||||
|
raise exception.ComputeHostNotFound(
|
||||||
|
compute_host=hostname)
|
||||||
|
except MultipleResultsFound:
|
||||||
|
raise exception.Conflict('Multiple compute hosts exist with same '
|
||||||
|
'hostname. Please use the uuid instead.')
|
||||||
|
|
||||||
|
def destroy_compute_host(self, context, host_uuid):
|
||||||
|
session = get_session()
|
||||||
|
with session.begin():
|
||||||
|
query = model_query(models.ComputeHost, session=session)
|
||||||
|
query = query.filter_by(uuid=host_uuid)
|
||||||
|
count = query.delete()
|
||||||
|
if count != 1:
|
||||||
|
raise exception.ComputeHostNotFound(
|
||||||
|
compute_host=host_uuid)
|
||||||
|
|
||||||
|
def update_compute_host(self, context, host_uuid, values):
|
||||||
|
if 'uuid' in values:
|
||||||
|
msg = _("Cannot overwrite UUID for an existing ComputeHost.")
|
||||||
|
raise exception.InvalidParameterValue(err=msg)
|
||||||
|
|
||||||
|
return self._do_update_compute_host(host_uuid, values)
|
||||||
|
|
||||||
|
def _do_update_compute_host(self, host_uuid, values):
|
||||||
|
session = get_session()
|
||||||
|
with session.begin():
|
||||||
|
query = model_query(models.ComputeHost, session=session)
|
||||||
|
query = query.filter_by(uuid=host_uuid)
|
||||||
|
try:
|
||||||
|
ref = query.with_lockmode('update').one()
|
||||||
|
except NoResultFound:
|
||||||
|
raise exception.ComputeHostNotFound(
|
||||||
|
compute_host=host_uuid)
|
||||||
|
|
||||||
|
ref.update(values)
|
||||||
|
return ref
|
||||||
|
|
||||||
|
def list_ml_models(self, context, filters=None, limit=None,
|
||||||
|
marker=None, sort_key=None, sort_dir=None):
|
||||||
|
query = model_query(models.Capsule)
|
||||||
|
query = self._add_project_filters(context, query)
|
||||||
|
query = self._add_ml_models_filters(query, filters)
|
||||||
|
return _paginate_query(models.Capsule, limit, marker,
|
||||||
|
sort_key, sort_dir, query)
|
||||||
|
|
||||||
|
def create_ml_model(self, context, values):
|
||||||
|
# ensure defaults are present for new ml_models
|
||||||
|
if not values.get('uuid'):
|
||||||
|
values['uuid'] = uuidutils.generate_uuid()
|
||||||
|
ml_model = models.ML_Model()
|
||||||
|
ml_model.update(values)
|
||||||
|
try:
|
||||||
|
ml_model.save()
|
||||||
|
except db_exc.DBDuplicateEntry:
|
||||||
|
raise exception.MLModelAlreadyExists(field='UUID',
|
||||||
|
value=values['uuid'])
|
||||||
|
return ml_model
|
||||||
|
|
||||||
|
def get_ml_model_by_uuid(self, context, ml_model_uuid):
|
||||||
|
query = model_query(models.ML_Model)
|
||||||
|
query = self._add_project_filters(context, query)
|
||||||
|
query = query.filter_by(uuid=ml_model_uuid)
|
||||||
|
try:
|
||||||
|
return query.one()
|
||||||
|
except NoResultFound:
|
||||||
|
raise exception.MLModelNotFound(ml_model=ml_model_uuid)
|
||||||
|
|
||||||
|
def get_ml_model_by_name(self, context, ml_model_name):
|
||||||
|
query = model_query(models.ML_Model)
|
||||||
|
query = self._add_project_filters(context, query)
|
||||||
|
query = query.filter_by(meta_name=ml_model_name)
|
||||||
|
try:
|
||||||
|
return query.one()
|
||||||
|
except NoResultFound:
|
||||||
|
raise exception.MLModelNotFound(ml_model=ml_model_name)
|
||||||
|
except MultipleResultsFound:
|
||||||
|
raise exception.Conflict('Multiple ml_models exist with same '
|
||||||
|
'name. Please use the ml_model uuid '
|
||||||
|
'instead.')
|
||||||
|
|
||||||
|
def destroy_ml_model(self, context, ml_model_id):
|
||||||
|
session = get_session()
|
||||||
|
with session.begin():
|
||||||
|
query = model_query(models.ML_Model, session=session)
|
||||||
|
query = add_identity_filter(query, ml_model_id)
|
||||||
|
count = query.delete()
|
||||||
|
if count != 1:
|
||||||
|
raise exception.MLModelNotFound(ml_model_id)
|
||||||
|
|
||||||
|
def update_ml_model(self, context, ml_model_id, values):
|
||||||
|
if 'uuid' in values:
|
||||||
|
msg = _("Cannot overwrite UUID for an existing ML Model.")
|
||||||
|
raise exception.InvalidParameterValue(err=msg)
|
||||||
|
|
||||||
|
return self._do_update_ml_model_id(ml_model_id, values)
|
||||||
|
|
||||||
|
def _do_update_ml_model_id(self, ml_model_id, values):
|
||||||
|
session = get_session()
|
||||||
|
with session.begin():
|
||||||
|
query = model_query(models.ML_Model, session=session)
|
||||||
|
query = add_identity_filter(query, ml_model_id)
|
||||||
|
try:
|
||||||
|
ref = query.with_lockmode('update').one()
|
||||||
|
except NoResultFound:
|
||||||
|
raise exception.MLModelNotFound(ml_model=ml_model_id)
|
||||||
|
|
||||||
|
ref.update(values)
|
||||||
|
return ref
|
||||||
|
|
||||||
|
def _add_ml_models_filters(self, query, filters):
|
||||||
|
filter_names = ['uuid', 'project_id', 'user_id']
|
||||||
|
return self._add_filters(query, models.ML_Model, filters=filters,
|
||||||
|
filter_names=filter_names)
|
111
gyan/db/sqlalchemy/migration.py
Normal file
111
gyan/db/sqlalchemy/migration.py
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
# 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 os
|
||||||
|
|
||||||
|
import alembic
|
||||||
|
from alembic import config as alembic_config
|
||||||
|
import alembic.migration as alembic_migration
|
||||||
|
from oslo_db import exception as db_exc
|
||||||
|
from oslo_db.sqlalchemy import enginefacade
|
||||||
|
from oslo_db.sqlalchemy.migration_cli import manager
|
||||||
|
|
||||||
|
from gyan.db.sqlalchemy import models
|
||||||
|
|
||||||
|
import gyan.conf
|
||||||
|
|
||||||
|
_MANAGER = None
|
||||||
|
|
||||||
|
|
||||||
|
def _alembic_config():
|
||||||
|
path = os.path.join(os.path.dirname(__file__), 'alembic.ini')
|
||||||
|
config = alembic_config.Config(path)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def get_manager():
|
||||||
|
global _MANAGER
|
||||||
|
if not _MANAGER:
|
||||||
|
alembic_path = os.path.abspath(
|
||||||
|
os.path.join(os.path.dirname(__file__), 'alembic.ini'))
|
||||||
|
migrate_path = os.path.abspath(
|
||||||
|
os.path.join(os.path.dirname(__file__), 'alembic'))
|
||||||
|
migration_config = {'alembic_ini_path': alembic_path,
|
||||||
|
'alembic_repo_path': migrate_path,
|
||||||
|
'db_url': gyan.conf.CONF.database.connection}
|
||||||
|
_MANAGER = manager.MigrationManager(migration_config)
|
||||||
|
|
||||||
|
return _MANAGER
|
||||||
|
|
||||||
|
|
||||||
|
def version(config=None, engine=None):
|
||||||
|
"""Current database version.
|
||||||
|
|
||||||
|
:returns: Database version
|
||||||
|
:rtype: string
|
||||||
|
"""
|
||||||
|
if engine is None:
|
||||||
|
engine = enginefacade.get_legacy_facade().get_engine()
|
||||||
|
with engine.connect() as conn:
|
||||||
|
context = alembic_migration.MigrationContext.configure(conn)
|
||||||
|
return context.get_current_revision()
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade(version):
|
||||||
|
"""Used for upgrading database.
|
||||||
|
|
||||||
|
:param version: Desired database version
|
||||||
|
:type version: string
|
||||||
|
"""
|
||||||
|
version = version or 'head'
|
||||||
|
|
||||||
|
get_manager().upgrade(version)
|
||||||
|
|
||||||
|
|
||||||
|
def stamp(revision, config=None):
|
||||||
|
"""Stamps database with provided revision.
|
||||||
|
|
||||||
|
Don't run any migrations.
|
||||||
|
|
||||||
|
:param revision: Should match one from repository or head - to stamp
|
||||||
|
database with most recent revision
|
||||||
|
:type revision: string
|
||||||
|
"""
|
||||||
|
config = config or _alembic_config()
|
||||||
|
return alembic.command.stamp(config, revision=revision)
|
||||||
|
|
||||||
|
|
||||||
|
def create_schema(config=None, engine=None):
|
||||||
|
"""Create database schema from models description.
|
||||||
|
|
||||||
|
Can be used for initial installation instead of upgrade('head').
|
||||||
|
"""
|
||||||
|
if engine is None:
|
||||||
|
engine = enginefacade.get_legacy_facade().get_engine()
|
||||||
|
|
||||||
|
if version(engine=engine) is not None:
|
||||||
|
raise db_exc.DBMigrationError("DB schema is already under version"
|
||||||
|
" control. Use upgrade() instead")
|
||||||
|
models.Base.metadata.create_all(engine)
|
||||||
|
stamp('head', config=config)
|
||||||
|
|
||||||
|
|
||||||
|
def revision(message=None, autogenerate=False):
|
||||||
|
"""Creates template for migration.
|
||||||
|
|
||||||
|
:param message: Text that will be used for migration title
|
||||||
|
:type message: string
|
||||||
|
:param autogenerate: If True - generates diff based on current database
|
||||||
|
state
|
||||||
|
:type autogenerate: bool
|
||||||
|
"""
|
||||||
|
return get_manager().revision(message=message, autogenerate=autogenerate)
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user