Features all the components of creating various templates and scripts for the provisioner, as well as VM profiles combining these.
933 lines
32 KiB
Python
Executable File
933 lines
32 KiB
Python
Executable File
#!/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
|
|
|
|
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'],
|
|
'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'],
|
|
'database_password': o_config['pvc']['provisioner']['database']['pass']
|
|
}
|
|
|
|
# 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__)
|
|
|
|
if config['debug']:
|
|
api.config['DEBUG'] = True
|
|
|
|
if config['auth_enabled']:
|
|
api.config["SECRET_KEY"] = config['auth_secret_key']
|
|
|
|
# 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
|
|
"""
|
|
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
|
|
|
|
return pvcprovisioner.create_template_system(name, vcpu_count, vram_mb, serial, vnc, vnc_bind)
|
|
|
|
@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.
|
|
* type: text
|
|
* optional: true
|
|
* 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_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
|
|
?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 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
|
|
|
|
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
|
|
|
|
if 'mountpoint' in flask.request.values:
|
|
mountpoint = flask.request.values['mountpoint']
|
|
else:
|
|
mountpoint = None
|
|
|
|
return pvcprovisioner.create_template_storage_element(template, disk_id, disk_size, filesystem, mountpoint)
|
|
|
|
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>.
|
|
?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':
|
|
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
|
|
|
|
if 'mountpoint' in flask.request.values:
|
|
mountpoint = flask.request.values['mountpoint']
|
|
else:
|
|
mountpoint = None
|
|
|
|
return pvcprovisioner.create_template_storage_element(template, disk_id, disk_size, mountpoint, filesystem)
|
|
|
|
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
|
|
"""
|
|
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
|
|
|
|
return pvcprovisioner.create_profile(name, system_template, network_template, storage_template, script)
|
|
|
|
@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.
|
|
"""
|
|
pass
|
|
|
|
|
|
|
|
#
|
|
# Entrypoint
|
|
#
|
|
if config['debug']:
|
|
# Run in Flask standard mode
|
|
api.run(config['listen_address'], config['listen_port'])
|
|
else:
|
|
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()
|