#!/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 # # 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 . # ############################################################################### import flask import json import yaml import os import uu import distutils.util import threading import time import gevent.pywsgi import celery as Celery import provisioner_lib.provisioner as pvc_provisioner import client_lib.common as pvc_common import client_lib.vm as pvc_vm import client_lib.network as pvc_network # 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'], 'coordinators': o_config['pvc']['coordinators'], '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'], 'queue_host': o_config['pvc']['provisioner']['queue']['host'], 'queue_port': o_config['pvc']['provisioner']['queue']['port'], 'queue_path': o_config['pvc']['provisioner']['queue']['path'], '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'] } if not config['storage_hosts']: config['storage_hosts'] = config['coordinators'] # Set the config object in the pvcapi namespace pvc_provisioner.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 = pvc_provisioner.open_database(config) pvc_provisioner.close_database(conn, cur) except Exception as e: print('{}'.format(e)) exit(1) # Primary provisioning API prapi = flask.Flask(__name__) prapi.config['CELERY_BROKER_URL'] = 'redis://{}:{}{}'.format(config['queue_host'], config['queue_port'], config['queue_path']) prapi.config['CELERY_RESULT_BACKEND'] = 'redis://{}:{}{}'.format(config['queue_host'], config['queue_port'], config['queue_path']) if config['debug']: prapi.config['DEBUG'] = True if config['auth_enabled']: prapi.config["SECRET_KEY"] = config['auth_secret_key'] celery = Celery.Celery(prapi.name, broker=prapi.config['CELERY_BROKER_URL']) celery.conf.update(prapi.config) # Metadata API mdapi = flask.Flask(__name__) if config['debug']: mdapi.config['DEBUG'] = True # # Job functions # @celery.task(bind=True) def create_vm(self, vm_name, profile_name): return pvc_provisioner.create_vm(self, vm_name, profile_name) # 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 # # Provisioning API # @prapi.route('/api/v1', methods=['GET']) def api_root(): return flask.jsonify({"message": "PVC Provisioner API version 1"}), 209 @prapi.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 '''

Enter your authentication token:

''' 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 @prapi.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 # @prapi.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(pvc_provisioner.template_list(limit)), 200 @prapi.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 ?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 """ 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(pvc_provisioner.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 bool(distutils.util.strtobool(flask.request.values['serial'])): serial = True else: serial = False # Get VNC configuration if 'vnc' in flask.request.values and bool(distutils.util.strtobool(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 # Get metadata 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 bool(distutils.util.strtobool(flask.request.values['start_with_node'])): start_with_node = True else: start_with_node = False return pvc_provisioner.create_template_system(name, vcpu_count, vram_mb, serial, vnc, vnc_bind, node_limit, node_selector, start_with_node) @prapi.route('/api/v1/template/system/