2019-12-02 20:24:38 -05:00
#!/usr/bin/env python3
# pvc-provisioner.py - PVC Provisioner API interface
# Part of the Parallel Virtual Cluster (PVC) system
#
# Copyright (C) 2018-2019 Joshua M. Boniface <joshua@boniface.me>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
###############################################################################
import flask
import json
import yaml
import os
import uu
import gevent . pywsgi
2019-12-03 23:39:13 -05:00
import celery as Celery
2019-12-02 20:24:38 -05:00
import provisioner_lib . provisioner as pvcprovisioner
# Parse the configuration file
try :
pvc_config_file = os . environ [ ' PVC_CONFIG_FILE ' ]
except :
print ( ' Error: The " PVC_CONFIG_FILE " environment variable must be set before starting pvc-provisioner. ' )
exit ( 1 )
print ( ' Starting PVC Provisioner daemon ' )
# Read in the config
try :
with open ( pvc_config_file , ' r ' ) as cfgfile :
o_config = yaml . load ( cfgfile )
except Exception as e :
print ( ' Failed to parse configuration file: {} ' . format ( e ) )
exit ( 1 )
try :
# Create the config object
config = {
' debug ' : o_config [ ' pvc ' ] [ ' debug ' ] ,
2019-12-03 23:39:13 -05:00
' coordinators ' : o_config [ ' pvc ' ] [ ' coordinators ' ] ,
2019-12-02 20:24:38 -05:00
' listen_address ' : o_config [ ' pvc ' ] [ ' provisioner ' ] [ ' listen_address ' ] ,
' listen_port ' : int ( o_config [ ' pvc ' ] [ ' provisioner ' ] [ ' listen_port ' ] ) ,
' auth_enabled ' : o_config [ ' pvc ' ] [ ' provisioner ' ] [ ' authentication ' ] [ ' enabled ' ] ,
' auth_secret_key ' : o_config [ ' pvc ' ] [ ' provisioner ' ] [ ' authentication ' ] [ ' secret_key ' ] ,
' auth_tokens ' : o_config [ ' pvc ' ] [ ' provisioner ' ] [ ' authentication ' ] [ ' tokens ' ] ,
' ssl_enabled ' : o_config [ ' pvc ' ] [ ' provisioner ' ] [ ' ssl ' ] [ ' enabled ' ] ,
' ssl_key_file ' : o_config [ ' pvc ' ] [ ' provisioner ' ] [ ' ssl ' ] [ ' key_file ' ] ,
' ssl_cert_file ' : o_config [ ' pvc ' ] [ ' provisioner ' ] [ ' ssl ' ] [ ' cert_file ' ] ,
' database_host ' : o_config [ ' pvc ' ] [ ' provisioner ' ] [ ' database ' ] [ ' host ' ] ,
' database_port ' : int ( o_config [ ' pvc ' ] [ ' provisioner ' ] [ ' database ' ] [ ' port ' ] ) ,
' database_name ' : o_config [ ' pvc ' ] [ ' provisioner ' ] [ ' database ' ] [ ' name ' ] ,
' database_user ' : o_config [ ' pvc ' ] [ ' provisioner ' ] [ ' database ' ] [ ' user ' ] ,
2019-12-03 23:39:13 -05:00
' database_password ' : o_config [ ' pvc ' ] [ ' provisioner ' ] [ ' database ' ] [ ' pass ' ] ,
' queue_host ' : o_config [ ' pvc ' ] [ ' provisioner ' ] [ ' queue ' ] [ ' host ' ] ,
' queue_port ' : o_config [ ' pvc ' ] [ ' provisioner ' ] [ ' queue ' ] [ ' port ' ] ,
' queue_path ' : o_config [ ' pvc ' ] [ ' provisioner ' ] [ ' queue ' ] [ ' path ' ] ,
2019-12-08 23:05:17 -05:00
' storage_hosts ' : o_config [ ' pvc ' ] [ ' cluster ' ] [ ' storage_hosts ' ] ,
' storage_domain ' : o_config [ ' pvc ' ] [ ' cluster ' ] [ ' storage_domain ' ] ,
' ceph_monitor_port ' : o_config [ ' pvc ' ] [ ' cluster ' ] [ ' ceph_monitor_port ' ] ,
' ceph_storage_secret_uuid ' : o_config [ ' pvc ' ] [ ' cluster ' ] [ ' ceph_storage_secret_uuid ' ]
2019-12-02 20:24:38 -05:00
}
2019-12-08 23:05:17 -05:00
if not config [ ' storage_hosts ' ] :
config [ ' storage_hosts ' ] = config [ ' coordinators ' ]
2019-12-02 20:24:38 -05:00
# Set the config object in the pvcapi namespace
pvcprovisioner . config = config
except Exception as e :
print ( ' {} ' . format ( e ) )
exit ( 1 )
# Try to connect to the database or fail
try :
print ( ' Verifying connectivity to database ' )
conn , cur = pvcprovisioner . open_database ( config )
pvcprovisioner . close_database ( conn , cur )
except Exception as e :
print ( ' {} ' . format ( e ) )
exit ( 1 )
api = flask . Flask ( __name__ )
2019-12-03 23:39:13 -05:00
api . config [ ' CELERY_BROKER_URL ' ] = ' redis:// {} : {} {} ' . format ( config [ ' queue_host ' ] , config [ ' queue_port ' ] , config [ ' queue_path ' ] )
api . config [ ' CELERY_RESULT_BACKEND ' ] = ' redis:// {} : {} {} ' . format ( config [ ' queue_host ' ] , config [ ' queue_port ' ] , config [ ' queue_path ' ] )
2019-12-02 20:24:38 -05:00
if config [ ' debug ' ] :
api . config [ ' DEBUG ' ] = True
if config [ ' auth_enabled ' ] :
api . config [ " SECRET_KEY " ] = config [ ' auth_secret_key ' ]
2019-12-03 23:39:13 -05:00
print ( api . name )
celery = Celery . Celery ( api . name , broker = api . config [ ' CELERY_BROKER_URL ' ] )
celery . conf . update ( api . config )
#
# Job functions
#
@celery.task ( bind = True )
def create_vm ( self , vm_name , profile_name ) :
return pvcprovisioner . create_vm ( self , vm_name , profile_name )
2019-12-02 20:24:38 -05:00
# Authentication decorator function
def authenticator ( function ) :
def authenticate ( * args , * * kwargs ) :
# No authentication required
if not config [ ' auth_enabled ' ] :
return function ( * args , * * kwargs )
# Session-based authentication
if ' token ' in flask . session :
return function ( * args , * * kwargs )
# Key header-based authentication
if ' X-Api-Key ' in flask . request . headers :
if any ( token for token in secret_tokens if flask . request . headers . get ( ' X-Api-Key ' ) == token ) :
return function ( * args , * * kwargs )
else :
return " X-Api-Key Authentication failed \n " , 401
# All authentications failed
return " X-Api-Key Authentication required \n " , 401
authenticate . __name__ = function . __name__
return authenticate
@api.route ( ' /api/v1 ' , methods = [ ' GET ' ] )
def api_root ( ) :
return flask . jsonify ( { " message " : " PVC Provisioner API version 1 " } ) , 209
@api.route ( ' /api/v1/auth/login ' , methods = [ ' GET ' , ' POST ' ] )
def api_auth_login ( ) :
# Just return a 200 if auth is disabled
if not config [ ' auth_enabled ' ] :
return flask . jsonify ( { " message " : " Authentication is disabled. " } ) , 200
if flask . request . method == ' GET ' :
return '''
< form method = " post " >
< p >
Enter your authentication token :
< input type = text name = token style = ' width:24em ' >
< input type = submit value = Login >
< / p >
< / form >
'''
if flask . request . method == ' POST ' :
if any ( token for token in config [ ' auth_tokens ' ] if flask . request . values [ ' token ' ] in token [ ' token ' ] ) :
flask . session [ ' token ' ] = flask . request . form [ ' token ' ]
return flask . redirect ( flask . url_for ( ' api_root ' ) )
else :
return flask . jsonify ( { " message " : " Authentication failed " } ) , 401
@api.route ( ' /api/v1/auth/logout ' , methods = [ ' GET ' , ' POST ' ] )
def api_auth_logout ( ) :
# Just return a 200 if auth is disabled
if not config [ ' auth_enabled ' ] :
return flask . jsonify ( { " message " : " Authentication is disabled. " } ) , 200
# remove the username from the session if it's there
flask . session . pop ( ' token ' , None )
return flask . redirect ( flask . url_for ( ' api_root ' ) )
#
# Template endpoints
#
@api.route ( ' /api/v1/template ' , methods = [ ' GET ' ] )
@authenticator
def api_template_root ( ) :
"""
/ template - Manage provisioning templates for VM creation .
GET : List all templates in the provisioning system .
? limit : Specify a limit to queries . Fuzzy by default ; use ^ and $ to force exact matches .
"""
# Get name limit
if ' limit ' in flask . request . values :
limit = flask . request . values [ ' limit ' ]
else :
limit = None
return flask . jsonify ( pvcprovisioner . template_list ( limit ) ) , 200
@api.route ( ' /api/v1/template/system ' , methods = [ ' GET ' , ' POST ' ] )
@authenticator
def api_template_system_root ( ) :
"""
/ template / system - Manage system provisioning templates for VM creation .
GET : List all system templates in the provisioning system .
? limit : Specify a limit to queries . Fuzzy by default ; use ^ and $ to force exact matches .
* type : text
* optional : true
* requires : N / A
POST : Add new system template .
? name : The name of the template .
* type : text
* optional : false
* requires : N / A
? vcpus : The number of VCPUs .
* type : integer
* optional : false
* requires : N / A
? vram : The amount of RAM in MB .
* type : integer , Megabytes ( MB )
* optional : false
* requires : N / A
? serial : Enable serial console .
* type : boolean
* optional : false
* requires : N / A
? vnc : True / False , enable VNC console .
* type : boolean
* optional : false
* requires : N / A
? vnc_bind : Address to bind VNC to .
* default : ' 127.0.0.1 '
* type : IP Address ( or ' 0.0.0.0 ' wildcard )
* optional : true
* requires : vnc = True
2019-12-08 23:05:17 -05:00
? node_limit : CSV list of node ( s ) to limit VM operation to
* type : CSV of valid PVC nodes
* optional : true
* requires : N / A
? node_selector : Selector to use for node migrations after initial provisioning
* type : Valid PVC node selector
* optional : true
* requires : N / A
? start_with_node : Whether to start limited node with the parent node
* default : false
* type : boolean
* optional : true
* requires : N / A
2019-12-02 20:24:38 -05:00
"""
if flask . request . method == ' GET ' :
# Get name limit
if ' limit ' in flask . request . values :
limit = flask . request . values [ ' limit ' ]
else :
limit = None
return flask . jsonify ( pvcprovisioner . list_template_system ( limit ) ) , 200
if flask . request . method == ' POST ' :
# Get name data
if ' name ' in flask . request . values :
name = flask . request . values [ ' name ' ]
else :
return flask . jsonify ( { " message " : " A name must be specified. " } ) , 400
# Get vcpus data
if ' vcpus ' in flask . request . values :
try :
vcpu_count = int ( flask . request . values [ ' vcpus ' ] )
except :
return flask . jsonify ( { " message " : " A vcpus value must be an integer. " } ) , 400
else :
return flask . jsonify ( { " message " : " A vcpus value must be specified. " } ) , 400
# Get vram data
if ' vram ' in flask . request . values :
try :
vram_mb = int ( flask . request . values [ ' vram ' ] )
except :
return flask . jsonify ( { " message " : " A vram integer value in Megabytes must be specified. " } ) , 400
else :
return flask . jsonify ( { " message " : " A vram integer value in Megabytes must be specified. " } ) , 400
# Get serial configuration
if ' serial ' in flask . request . values and flask . request . values [ ' serial ' ] :
serial = True
else :
serial = False
# Get VNC configuration
if ' vnc ' in flask . request . values and flask . request . values [ ' vnc ' ] :
vnc = True
if ' vnc_bind ' in flask . request . values :
vnc_bind = flask . request . values [ ' vnc_bind_address ' ]
else :
vnc_bind = None
else :
vnc = False
vnc_bind = None
2019-12-08 23:05:17 -05:00
if ' node_limit ' in flask . request . values :
node_limit = flask . request . values [ ' node_limit ' ]
else :
node_limit = None
if ' node_selector ' in flask . request . values :
node_selector = flask . request . values [ ' node_selector ' ]
else :
node_selector = None
if ' start_with_node ' in flask . request . values and flask . request . values [ ' start_with_node ' ] :
start_with_node = True
else :
start_with_node = False
return pvcprovisioner . create_template_system ( name , vcpu_count , vram_mb , serial , vnc , vnc_bind , node_limit , node_selector , start_with_node )
2019-12-02 20:24:38 -05:00
@api.route ( ' /api/v1/template/system/<template> ' , methods = [ ' GET ' , ' POST ' , ' DELETE ' ] )
@authenticator
def api_template_system_element ( template ) :
"""
/ template / system / < template > - Manage system provisioning template < template > .
GET : Show details of system template < template > .
POST : Add new system template with name < template > .
? vcpus : The number of VCPUs .
* type : integer
* optional : false
* requires : N / A
? vram : The amount of RAM in MB .
* type : integer , Megabytes ( MB )
* optional : false
* requires : N / A
? serial : Enable serial console .
* type : boolean
* optional : false
* requires : N / A
? vnc : True / False , enable VNC console .
* type : boolean
* optional : false
* requires : N / A
? vnc_bind : Address to bind VNC to .
* default : ' 127.0.0.1 '
* type : IP Address ( or ' 0.0.0.0 ' wildcard )
* optional : true
* requires : vnc = True
DELETE : Remove system template < template > .
"""
if flask . request . method == ' GET ' :
return flask . jsonify ( pvcprovisioner . list_template_system ( template , is_fuzzy = False ) ) , 200
if flask . request . method == ' POST ' :
# Get vcpus data
if ' vcpus ' in flask . request . values :
try :
vcpu_count = int ( flask . request . values [ ' vcpus ' ] )
except :
return flask . jsonify ( { " message " : " A vcpus value must be an integer. " } ) , 400
else :
return flask . jsonify ( { " message " : " A vcpus value must be specified. " } ) , 400
# Get vram data
if ' vram ' in flask . request . values :
try :
vram_mb = int ( flask . request . values [ ' vram ' ] )
except :
return flask . jsonify ( { " message " : " A vram integer value in Megabytes must be specified. " } ) , 400
else :
return flask . jsonify ( { " message " : " A vram integer value in Megabytes must be specified. " } ) , 400
# Get serial configuration
if ' serial ' in flask . request . values and flask . request . values [ ' serial ' ] :
serial = True
else :
serial = False
# Get VNC configuration
if ' vnc ' in flask . request . values and flask . request . values [ ' vnc ' ] :
vnc = True
if ' vnc_bind ' in flask . request . values :
vnc_bind = flask . request . values [ ' vnc_bind_address ' ]
else :
vnc_bind = None
else :
vnc = False
vnc_bind = None
return pvcprovisioner . create_template_system ( template , vcpu_count , vram_mb , serial , vnc , vnc_bind )
if flask . request . method == ' DELETE ' :
return pvcprovisioner . delete_template_system ( template )
@api.route ( ' /api/v1/template/network ' , methods = [ ' GET ' , ' POST ' ] )
@authenticator
def api_template_network_root ( ) :
"""
/ template / network - Manage network provisioning templates for VM creation .
GET : List all network templates in the provisioning system .
? limit : Specify a limit to queries . Fuzzy by default ; use ^ and $ to force exact matches .
* type : text
* optional : true
* requires : N / A
POST : Add new network template .
? name : The name of the template .
* type : text
* optional : false
* requires : N / A
? mac_template : The MAC address template for the template .
2019-12-08 23:05:17 -05:00
* type : MAC address template
2019-12-02 20:24:38 -05:00
* optional : true
* requires : N / A
2019-12-08 23:05:17 -05:00
The MAC address template should use the following conventions :
* use { prefix } to represent the Libvirt MAC prefix , always " 52:54:00 "
* use { vmid } to represent the hex value ( < 16 ) of the host ' s ID (e.g. server4 has ID 4, server has ID 0)
* use { netid } to represent the hex value ( < 16 ) of the network ' s sequential integer ID (first is 0, etc.)
Example : " {prefix} :ff:ff: {vmid} {netid} "
2019-12-02 20:24:38 -05:00
"""
if flask . request . method == ' GET ' :
# Get name limit
if ' limit ' in flask . request . values :
limit = flask . request . values [ ' limit ' ]
else :
limit = None
return flask . jsonify ( pvcprovisioner . list_template_network ( limit ) ) , 200
if flask . request . method == ' POST ' :
# Get name data
if ' name ' in flask . request . values :
name = flask . request . values [ ' name ' ]
else :
return flask . jsonify ( { " message " : " A name must be specified. " } ) , 400
if ' mac_template ' in flask . request . values :
mac_template = flask . request . values [ ' mac_template ' ]
else :
mac_template = None
return pvcprovisioner . create_template_network ( name , mac_template )
@api.route ( ' /api/v1/template/network/<template> ' , methods = [ ' GET ' , ' POST ' , ' DELETE ' ] )
@authenticator
def api_template_network_element ( template ) :
"""
/ template / network / < template > - Manage network provisioning template < template > .
GET : Show details of network template < template > .
POST : Add new network template with name < template > .
? mac_template : The MAC address template for the template .
* type : text
* optional : true
* requires : N / A
DELETE : Remove network template < template > .
"""
if flask . request . method == ' GET ' :
return flask . jsonify ( pvcprovisioner . list_template_network ( template , is_fuzzy = False ) ) , 200
if flask . request . method == ' POST ' :
if ' mac_template ' in flask . request . values :
mac_template = flask . request . values [ ' mac_template ' ]
else :
mac_template = None
return pvcprovisioner . create_template_network ( template , mac_template )
if flask . request . method == ' DELETE ' :
return pvcprovisioner . delete_template_network ( template )
@api.route ( ' /api/v1/template/network/<template>/net ' , methods = [ ' GET ' , ' POST ' , ' DELETE ' ] )
@authenticator
def api_template_network_net_root ( template ) :
"""
/ template / network / < template > / net - Manage network VNIs in network provisioning template < template > .
GET : Show details of network template < template > .
POST : Add new network VNI to network template < template > .
? vni : The network VNI .
* type : integer
* optional : false
* requires : N / A
DELETE : Remove network VNI from network template < template > .
? vni : The network VNI .
* type : integer
* optional : false
* requires : N / A
"""
if flask . request . method == ' GET ' :
return flask . jsonify ( pvcprovisioner . list_template_network ( template , is_fuzzy = False ) ) , 200
if flask . request . method == ' POST ' :
if ' vni ' in flask . request . values :
vni = flask . request . values [ ' vni ' ]
else :
return flask . jsonify ( { " message " : " A VNI must be specified. " } ) , 400
return pvcprovisioner . create_template_network_element ( template , vni )
if flask . request . method == ' DELETE ' :
if ' vni ' in flask . request . values :
vni = flask . request . values [ ' vni ' ]
else :
return flask . jsonify ( { " message " : " A VNI must be specified. " } ) , 400
return pvcprovisioner . delete_template_network_element ( template , vni )
@api.route ( ' /api/v1/template/network/<template>/net/<vni> ' , methods = [ ' GET ' , ' POST ' , ' DELETE ' ] )
@authenticator
def api_template_network_net_element ( template , vni ) :
"""
/ template / network / < template > / net / < vni > - Manage network VNI < vni > in network provisioning template < template > .
GET : Show details of network template < template > .
POST : Add new network VNI < vni > to network template < template > .
DELETE : Remove network VNI < vni > from network template < template > .
"""
if flask . request . method == ' GET ' :
networks = pvcprovisioner . list_template_network_vnis ( template )
for network in networks :
if int ( network [ ' vni ' ] ) == int ( vni ) :
return flask . jsonify ( network ) , 200
return flask . jsonify ( { " message " : " Found no network with VNI {} in network template {} " . format ( vni , template ) } ) , 404
if flask . request . method == ' POST ' :
return pvcprovisioner . create_template_network_element ( template , vni )
if flask . request . method == ' DELETE ' :
return pvcprovisioner . delete_template_network_element ( template , vni )
@api.route ( ' /api/v1/template/storage ' , methods = [ ' GET ' , ' POST ' ] )
@authenticator
def api_template_storage_root ( ) :
"""
/ template / storage - Manage storage provisioning templates for VM creation .
GET : List all storage templates in the provisioning system .
? limit : Specify a limit to queries . Fuzzy by default ; use ^ and $ to force exact matches .
* type : text
* optional : true
* requires : N / A
POST : Add new storage template .
? name : The name of the template .
* type : text
* optional : false
* requires : N / A
"""
if flask . request . method == ' GET ' :
# Get name limit
if ' limit ' in flask . request . values :
limit = flask . request . values [ ' limit ' ]
else :
limit = None
return flask . jsonify ( pvcprovisioner . list_template_storage ( limit ) ) , 200
if flask . request . method == ' POST ' :
# Get name data
if ' name ' in flask . request . values :
name = flask . request . values [ ' name ' ]
else :
return flask . jsonify ( { " message " : " A name must be specified. " } ) , 400
return pvcprovisioner . create_template_storage ( name )
@api.route ( ' /api/v1/template/storage/<template> ' , methods = [ ' GET ' , ' POST ' , ' DELETE ' ] )
@authenticator
def api_template_storage_element ( template ) :
"""
/ template / storage / < template > - Manage storage provisioning template < template > .
GET : Show details of storage template .
POST : Add new storage template .
DELETE : Remove storage template .
"""
if flask . request . method == ' GET ' :
return flask . jsonify ( pvcprovisioner . list_template_storage ( template , is_fuzzy = False ) ) , 200
if flask . request . method == ' POST ' :
return pvcprovisioner . create_template_storage ( template )
if flask . request . method == ' DELETE ' :
return pvcprovisioner . delete_template_storage ( template )
if ' disk ' in flask . request . values :
disks = list ( )
for disk in flask . request . values . getlist ( ' disk ' ) :
disk_data = disk . split ( ' , ' )
disks . append ( disk_data )
else :
return flask . jsonify ( { " message " : " A disk must be specified. " } ) , 400
@api.route ( ' /api/v1/template/storage/<template>/disk ' , methods = [ ' GET ' , ' POST ' , ' DELETE ' ] )
@authenticator
def api_template_storage_disk_root ( template ) :
"""
/ template / storage / < template > / disk - Manage disks in storage provisioning template < template > .
GET : Show details of storage template < template > .
POST : Add new disk to storage template < template > .
? disk_id : The identifier of the disk .
* type : Disk identifier in ' sdX ' or ' vdX ' format , unique within template
* optional : false
* requires : N / A
2019-12-03 23:39:13 -05:00
? pool : The storage pool in which to store the disk .
* type : Storage Pool name
* optional : false
* requires : N / A
2019-12-02 20:24:38 -05:00
? disk_size : The disk size in GB .
* type : integer , Gigabytes ( GB )
* optional : false
* requires : N / A
? filesystem : The Linux guest filesystem for the disk
* default : unformatted filesystem
* type : Valid Linux filesystem
* optional : true
* requires : N / A
2019-12-07 02:16:13 -05:00
? filesystem_arg : Argument for the guest filesystem
* type : Valid mkfs . < filesystem > argument , multiple
* optional : true
* requires : N / A
2019-12-02 20:24:38 -05:00
? mountpoint : The Linux guest mountpoint for the disk
* default : unmounted in guest
* type : Valid Linux mountpoint ( e . g . ' / ' , ' /var ' , etc . )
* optional : true
* requires : ? filesystem
DELETE : Remove disk from storage template < template > .
? disk_id : The identifier of the disk .
* type : Disk identifier in ' sdX ' or ' vdX ' format
* optional : false
* requires : N / A
"""
if flask . request . method == ' GET ' :
return flask . jsonify ( pvcprovisioner . list_template_storage ( template , is_fuzzy = False ) ) , 200
if flask . request . method == ' POST ' :
if ' disk_id ' in flask . request . values :
disk_id = flask . request . values [ ' disk_id ' ]
else :
return flask . jsonify ( { " message " : " A disk ID in sdX/vdX format must be specified. " } ) , 400
2019-12-03 23:39:13 -05:00
if ' pool ' in flask . request . values :
pool = flask . request . values [ ' pool ' ]
else :
return flask . jsonify ( { " message " : " A pool name must be specified. " } ) , 400
2019-12-02 20:24:38 -05:00
if ' disk_size ' in flask . request . values :
disk_size = flask . request . values [ ' disk_size ' ]
else :
return flask . jsonify ( { " message " : " A disk size in GB must be specified. " } ) , 400
if ' filesystem ' in flask . request . values :
filesystem = flask . request . values [ ' filesystem ' ]
else :
filesystem = None
2019-12-07 02:16:13 -05:00
if ' filesystem_arg ' in flask . request . values :
filesystem_args = flask . request . values . getlist ( ' filesystem_arg ' )
else :
filesystem_args = None
2019-12-02 20:24:38 -05:00
if ' mountpoint ' in flask . request . values :
mountpoint = flask . request . values [ ' mountpoint ' ]
else :
mountpoint = None
2019-12-07 02:16:13 -05:00
return pvcprovisioner . create_template_storage_element ( template , pool , disk_id , disk_size , filesystem , filesystem_args , mountpoint )
2019-12-02 20:24:38 -05:00
if flask . request . method == ' DELETE ' :
if ' disk_id ' in flask . request . values :
disk_id = flask . request . values [ ' disk_id ' ]
else :
return flask . jsonify ( { " message " : " A disk ID in sdX/vdX format must be specified. " } ) , 400
return pvcprovisioner . delete_template_storage_element ( template , disk_id )
@api.route ( ' /api/v1/template/storage/<template>/disk/<disk_id> ' , methods = [ ' GET ' , ' POST ' , ' DELETE ' ] )
@authenticator
def api_template_storage_disk_element ( template , disk_id ) :
"""
/ template / storage / < template > / disk / < disk_id > - Manage disk < disk_id > in storage provisioning template < template > .
GET : Show details of disk < disk_id > storage template < template > .
POST : Add new storage VNI < vni > to storage template < template > .
2019-12-03 23:39:13 -05:00
? pool : The storage pool in which to store the disk .
* type : Storage Pool name
* optional : false
* requires : N / A
2019-12-02 20:24:38 -05:00
? disk_size : The disk size in GB .
* type : integer , Gigabytes ( GB )
* optional : false
* requires : N / A
? filesystem : The Linux guest filesystem for the disk
* default : unformatted filesystem
* type : Valid Linux filesystem
* optional : true
* requires : N / A
? mountpoint : The Linux guest mountpoint for the disk
* default : unmounted in guest
* type : Valid Linux mountpoint ( e . g . ' / ' , ' /var ' , etc . )
* optional : true
* requires : ? filesystem
DELETE : Remove storage VNI < vni > from storage template < template > .
"""
if flask . request . method == ' GET ' :
disks = pvcprovisioner . list_template_storage_disks ( template )
for disk in disks :
if disk [ ' disk_id ' ] == disk_id :
return flask . jsonify ( disk ) , 200
return flask . jsonify ( { " message " : " Found no disk with ID {} in storage template {} " . format ( disk_id , template ) } ) , 404
if flask . request . method == ' POST ' :
2019-12-03 23:39:13 -05:00
if ' pool ' in flask . request . values :
pool = flask . request . values [ ' pool ' ]
else :
return flask . jsonify ( { " message " : " A pool name must be specified. " } ) , 400
2019-12-02 20:24:38 -05:00
if ' disk_size ' in flask . request . values :
disk_size = flask . request . values [ ' disk_size ' ]
else :
return flask . jsonify ( { " message " : " A disk size in GB must be specified. " } ) , 400
if ' filesystem ' in flask . request . values :
filesystem = flask . request . values [ ' filesystem ' ]
else :
filesystem = None
2019-12-07 02:16:13 -05:00
if ' filesystem_arg ' in flask . request . values :
filesystem_args = flask . request . values . getlist ( ' filesystem_arg ' )
else :
filesystem_args = None
2019-12-02 20:24:38 -05:00
if ' mountpoint ' in flask . request . values :
mountpoint = flask . request . values [ ' mountpoint ' ]
else :
mountpoint = None
2019-12-07 02:16:13 -05:00
return pvcprovisioner . create_template_storage_element ( template , pool , disk_id , disk_size , filesystem , filesystem_args , mountpoint )
2019-12-02 20:24:38 -05:00
if flask . request . method == ' DELETE ' :
return pvcprovisioner . delete_template_storage_element ( template , disk_id )
#
# Script endpoints
#
@api.route ( ' /api/v1/script ' , methods = [ ' GET ' , ' POST ' ] )
@authenticator
def api_script_root ( ) :
"""
/ script - Manage provisioning scripts for VM creation .
GET : List all scripts in the provisioning system .
? limit : Specify a limit to queries . Fuzzy by default ; use ^ and $ to force exact matches .
* type : text
* optional : true
* requires : N / A
POST : Add new provisioning script .
? name : The name of the script .
* type : text
* optional : false
* requires : N / A
? data : The raw text of the script .
* type : text ( freeform )
* optional : false
* requires : N / A
"""
if flask . request . method == ' GET ' :
# Get name limit
if ' limit ' in flask . request . values :
limit = flask . request . values [ ' limit ' ]
else :
limit = None
return flask . jsonify ( pvcprovisioner . list_script ( limit ) ) , 200
if flask . request . method == ' POST ' :
# Get name data
if ' name ' in flask . request . values :
name = flask . request . values [ ' name ' ]
else :
return flask . jsonify ( { " message " : " A name must be specified. " } ) , 400
# Get script data
if ' data ' in flask . request . values :
data = flask . request . values [ ' data ' ]
else :
return flask . jsonify ( { " message " : " Script data must be specified. " } ) , 400
return pvcprovisioner . create_script ( name , data )
@api.route ( ' /api/v1/script/<script> ' , methods = [ ' GET ' , ' POST ' , ' DELETE ' ] )
@authenticator
def api_script_element ( script ) :
"""
/ script / < script > - Manage provisioning script < script > .
GET : Show details of provisioning script .
POST : Add new provisioning script .
? data : The raw text of the script .
* type : text ( freeform )
* optional : false
* requires : N / A
DELETE : Remove provisioning script .
"""
if flask . request . method == ' GET ' :
return flask . jsonify ( pvcprovisioner . list_script ( script , is_fuzzy = False ) ) , 200
if flask . request . method == ' POST ' :
# Get script data
if ' data ' in flask . request . values :
data = flask . request . values [ ' data ' ]
else :
return flask . jsonify ( { " message " : " Script data must be specified. " } ) , 400
return pvcprovisioner . create_script ( script , data )
if flask . request . method == ' DELETE ' :
return pvcprovisioner . delete_script ( script )
#
# Profile endpoints
#
@api.route ( ' /api/v1/profile ' , methods = [ ' GET ' , ' POST ' ] )
@authenticator
def api_profile_root ( ) :
"""
/ profile - Manage VM profiles for VM creation .
GET : List all VM profiles in the provisioning system .
? limit : Specify a limit to queries . Fuzzy by default ; use ^ and $ to force exact matches .
* type : text
* optional : true
* requires : N / A
POST : Add new VM profile .
? name : The name of the profile .
* type : text
* optional : false
* requires : N / A
? system_template : The name of the system template .
* type : text
* optional : false
* requires : N / A
? network_template : The name of the network template .
* type : text
* optional : false
* requires : N / A
? storage_template : The name of the disk template .
* type : text
* optional : false
* requires : N / A
? script : The name of the provisioning script .
* type : text
* optional : false
* requires : N / A
2019-12-03 23:39:13 -05:00
? arg : An arbitrary key = value argument for use by the provisioning script .
* type : key - value pair , multiple
* optional : true
* requires : N / A
2019-12-02 20:24:38 -05:00
"""
if flask . request . method == ' GET ' :
# Get name limit
if ' limit ' in flask . request . values :
limit = flask . request . values [ ' limit ' ]
else :
limit = None
return flask . jsonify ( pvcprovisioner . list_profile ( limit ) ) , 200
if flask . request . method == ' POST ' :
# Get name data
if ' name ' in flask . request . values :
name = flask . request . values [ ' name ' ]
else :
return flask . jsonify ( { " message " : " A name must be specified. " } ) , 400
# Get system_template data
if ' system_template ' in flask . request . values :
system_template = flask . request . values [ ' system_template ' ]
else :
return flask . jsonify ( { " message " : " A system template must be specified. " } ) , 400
# Get network_template data
if ' network_template ' in flask . request . values :
network_template = flask . request . values [ ' network_template ' ]
else :
return flask . jsonify ( { " message " : " A network template must be specified. " } ) , 400
# Get storage_template data
if ' storage_template ' in flask . request . values :
storage_template = flask . request . values [ ' storage_template ' ]
else :
return flask . jsonify ( { " message " : " A disk template must be specified. " } ) , 400
# Get script data
if ' script ' in flask . request . values :
script = flask . request . values [ ' script ' ]
else :
return flask . jsonify ( { " message " : " A script must be specified. " } ) , 400
2019-12-03 23:39:13 -05:00
if ' arg ' in flask . request . values :
arguments = flask . request . values . getlist ( ' arg ' )
else :
arguments = None
return pvcprovisioner . create_profile ( name , system_template , network_template , storage_template , script , arguments )
2019-12-02 20:24:38 -05:00
@api.route ( ' /api/v1/profile/<profile> ' , methods = [ ' GET ' , ' POST ' , ' DELETE ' ] )
@authenticator
def api_profile_element ( profile ) :
"""
/ profile / < profile > - Manage VM profile < profile > .
GET : Show details of VM profile .
POST : Add new VM profile .
? system_template : The name of the system template .
* type : text
* optional : false
* requires : N / A
? network_template : The name of the network template .
* type : text
* optional : false
* requires : N / A
? storage_template : The name of the disk template .
* type : text
* optional : false
* requires : N / A
? script : The name of the provisioning script .
* type : text
* optional : false
* requires : N / A
DELETE : Remove VM profile .
"""
if flask . request . method == ' GET ' :
return flask . jsonify ( pvcprovisioner . list_profile ( profile , is_fuzzy = False ) ) , 200
if flask . request . method == ' POST ' :
# Get system_template data
if ' system_template ' in flask . request . values :
system_template = flask . request . values [ ' system_template ' ]
else :
return flask . jsonify ( { " message " : " A system template must be specified. " } ) , 400
# Get network_template data
if ' network_template ' in flask . request . values :
network_template = flask . request . values [ ' network_template ' ]
else :
return flask . jsonify ( { " message " : " A network template must be specified. " } ) , 400
# Get storage_template data
if ' storage_template ' in flask . request . values :
storage_template = flask . request . values [ ' storage_template ' ]
else :
return flask . jsonify ( { " message " : " A disk template must be specified. " } ) , 400
# Get script data
if ' script ' in flask . request . values :
script = flask . request . values [ ' script ' ]
else :
return flask . jsonify ( { " message " : " A script must be specified. " } ) , 400
return pvcprovisioner . create_profile ( profile , system_template , network_template , storage_template , script )
if flask . request . method == ' DELETE ' :
return pvcprovisioner . delete_profile ( profile )
#
# Provisioning endpoints
#
@api.route ( ' /api/v1/create ' , methods = [ ' POST ' ] )
@authenticator
def api_create_root ( ) :
"""
/ create - Create new VM on the cluster .
POST : Create new VM .
2019-12-03 23:39:13 -05:00
? name : The name of the VM .
* type : text
* optional : false
* requires : N / A
? profile : The profile name of the VM .
* type : text
* optional : flase
* requires : N / A
2019-12-02 20:24:38 -05:00
"""
2019-12-03 23:39:13 -05:00
if ' name ' in flask . request . values :
name = flask . request . values [ ' name ' ]
else :
return flask . jsonify ( { " message " : " A VM name must be specified. " } ) , 400
if ' profile ' in flask . request . values :
profile = flask . request . values [ ' profile ' ]
else :
return flask . jsonify ( { " message " : " A VM profile must be specified. " } ) , 400
print ( " starting task " )
task = create_vm . delay ( name , profile )
print ( task . id )
return flask . jsonify ( { " task_id " : task . id } ) , 202 , { ' Location ' : flask . url_for ( ' api_status_root ' , task_id = task . id ) }
2019-12-02 20:24:38 -05:00
2019-12-03 23:39:13 -05:00
@api.route ( ' /api/v1/status/<task_id> ' , methods = [ ' GET ' ] )
@authenticator
def api_status_root ( task_id ) :
"""
/ status - Report on VM creation status .
2019-12-02 20:24:38 -05:00
2019-12-03 23:39:13 -05:00
GET : Get status of the VM provisioning .
? task : The task ID returned from the ' /create ' endpoint .
* type : text
* optional : flase
* requires : N / A
"""
task = create_vm . AsyncResult ( task_id )
if task . state == ' PENDING ' :
# job did not start yet
response = {
' state ' : task . state ,
' current ' : 0 ,
' total ' : 1 ,
' status ' : ' Pending... '
}
elif task . state != ' FAILURE ' :
# job is still running
response = {
' state ' : task . state ,
' current ' : task . info . get ( ' current ' , 0 ) ,
' total ' : task . info . get ( ' total ' , 1 ) ,
' status ' : task . info . get ( ' status ' , ' ' )
}
if ' result ' in task . info :
response [ ' result ' ] = task . info [ ' result ' ]
else :
# something went wrong in the background job
response = {
' state ' : task . state ,
' current ' : 1 ,
' total ' : 1 ,
' status ' : str ( task . info ) , # this is the exception raised
}
return flask . jsonify ( response )
2019-12-02 20:24:38 -05:00
#
# Entrypoint
#
2019-12-03 23:39:13 -05:00
if __name__ == ' __main__ ' :
2019-12-09 10:33:44 -05:00
# Start main API
2019-12-03 23:39:13 -05:00
if config [ ' debug ' ] :
# Run in Flask standard mode
api . run ( config [ ' listen_address ' ] , config [ ' listen_port ' ] )
2019-12-02 20:24:38 -05:00
else :
2019-12-03 23:39:13 -05:00
if config [ ' ssl_enabled ' ] :
# Run the WSGI server with SSL
http_server = gevent . pywsgi . WSGIServer (
( config [ ' listen_address ' ] , config [ ' listen_port ' ] ) ,
api ,
keyfile = config [ ' ssl_key_file ' ] ,
certfile = config [ ' ssl_cert_file ' ]
)
else :
# Run the ?WSGI server without SSL
http_server = gevent . pywsgi . WSGIServer (
( config [ ' listen_address ' ] , config [ ' listen_port ' ] ) ,
api
)
print ( ' Starting PyWSGI server at {} : {} with SSL= {} , Authentication= {} ' . format ( config [ ' listen_address ' ] , config [ ' listen_port ' ] , config [ ' ssl_enabled ' ] , config [ ' auth_enabled ' ] ) )
http_server . serve_forever ( )