Initial provisioner configuration

Features all the components of creating various templates and scripts
for the provisioner, as well as VM profiles combining these.
This commit is contained in:
2019-12-02 20:24:38 -05:00
parent 356c12db2e
commit 4a7c6db9b2
8 changed files with 2095 additions and 0 deletions

View File

@ -0,0 +1 @@
../client-common

View File

@ -0,0 +1,168 @@
#!/usr/bin/env python3
# provisioing_script.py - PVC Provisioner example script
# 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/>.
#
###############################################################################
# This script provides an example of a PVC provisioner script. It will install
# a Debian system, of the release specified in the keyword argument `deb_release`
# and from the mirror specified in the keyword argument `deb_mirror`, and
# including the packages specified in the keyword argument `deb_packages` (a list
# of strings, which is then joined together as a CSV and passed to debootstrap),
# to the configured disks, configure fstab, and install GRUB. Any later config
# should be done within the VM, for instance via cloud-init.
# This script can thus be used as an example or reference implementation of a
# PVC provisioner script and expanded upon as required.
# This script will run under root privileges as the provisioner does. Be careful
# with that.
import os
# Installation function - performs a debootstrap install of a Debian system
# Note that the only arguments are keyword arguments.
def install(**kwargs):
# The provisioner has already mounted the disks on kwargs['temporary_directory'].
# by this point, so we can get right to running the debootstrap after setting
# some nicer variable names; you don't necessarily have to do this.
vm_name = kwargs['vm_name']
vm_id = kwargs['vm_id']
temporary_directory = kwargs['temporary_directory']
disks = kwargs['disks']
networks = kwargs['networks']
# Our own required arguments. We should, though are not required to, handle
# failures of these gracefully, should administrators forget to specify them.
try:
deb_release = kwargs['deb_release']
except:
deb_release = "stable"
try:
deb_mirror = kwargs['deb_mirror']
except:
deb_mirror = "http://ftp.debian.org/debian"
try:
deb_packages = kwargs['deb_packages']
except:
deb_packages = ["linux-image-amd64", "grub-pc", "cloud-init", "python3-cffi-backend"]
# We need to know our root disk
root_disk = None
for disk in disks:
if disk['mountpoint'] == '/':
root_disk = disk
if not root_disk:
return
print(root_disk)
# Ensure we have debootstrap intalled on the provisioner system; this is a
# good idea to include if you plan to use anything that is not part of the
# base Debian host system, just in case the provisioner host is not properly
# configured already.
os.system(
"apt-get install -y debootstrap"
)
# Perform a deboostrap installation
os.system(
"debootstrap --include={pkgs} {suite} {target} {mirror}".format(
suite=deb_release,
target=temporary_directory,
mirror=deb_mirror,
pkgs=','.join(deb_packages)
)
)
# Bind mount the devfs
os.system(
"mount --bind /dev {}/dev".format(
temporary_directory
)
)
# Create an fstab entry for each disk
fstab_file = "{}/etc/fstab".format(temporary_directory)
for disk in disks:
# We assume SSD-based/-like storage, and dislike atimes
options = "defaults,discard,noatime,nodiratime"
# The root and var volumes have specific values
if disk['mountpoint'] == "/":
dump = 0
cpass = 1
elif disk['mountpoint'] == '/var':
dump = 0
cpass = 2
else:
dump = 0
cpass = 0
# Append the fstab line
with open(fstab_file, 'a') as fh:
fh.write("/dev/{disk} {mountpoint} {filesystem} {options} {dump} {cpass}\n".format(
disk=disk['name'],
mountpoint=disk['mountpoint'],
filesystem=disk['filesystem'],
options=options,
dump=dump,
cpass=cpass
))
# Write the GRUB configuration
grubcfg_file = "{}/etc/default/grub".format(temporary_directory)
with open(grubcfg_file, 'w') as fh:
fh.write("""# Written by the PVC provisioner
GRUB_DEFAULT=0
GRUB_TIMEOUT=1
GRUB_DISTRIBUTOR="PVC Virtual Machine"
GRUB_CMDLINE_LINUX_DEFAULT="root=/dev/{root_disk} console=tty0 console=ttyS0,115200n8"
GRUB_CMDLINE_LINUX=""
GRUB_TERMINAL=console
GRUB_SERIAL_COMMAND="serial --speed=115200 --unit=0 --word=8 --parity=no --stop=1"
GRUB_DISABLE_LINUX_UUID=false
""".format(root_disk=root_disk['name']))
# Chroot and install GRUB so we can boot, then exit the chroot
# EXITING THE CHROOT IS VERY IMPORTANT OR THE FOLLOWING STAGES OF THE PROVISIONER
# WILL FAIL IN UNEXPECTED WAYS! Keep this in mind when using chroot in your scripts.
real_root = os.open("/", os.O_RDONLY)
os.chroot(temporary_directory)
fake_root = os.open("/", os.O_RDONLY)
os.fchdir(fake_root)
os.system(
"grub-install /dev/rbd/{}".format(root_disk['volume'])
)
os.system(
"update-grub"
)
# Restore our original root
os.fchdir(real_root)
os.chroot(".")
os.fchdir(real_root)
os.close(fake_root)
os.close(real_root)
# Unmount the bound devfs
os.system(
"umount {}/dev".format(
temporary_directory
)
)
# Everything else is done via cloud-init

View File

@ -0,0 +1,606 @@
#!/usr/bin/env python3
# pvcapi.py - PVC HTTP API functions
# 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 psycopg2
import psycopg2.extras
import os
import re
import client_lib.common as pvc_common
import client_lib.vm as pvc_vm
import client_lib.network as pvc_network
import client_lib.ceph as pvc_ceph
#
# Common functions
#
# Database connections
def open_database(config):
conn = psycopg2.connect(
host=config['database_host'],
port=config['database_port'],
dbname=config['database_name'],
user=config['database_user'],
password=config['database_password']
)
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
return conn, cur
def close_database(conn, cur, failed=False):
if not failed:
conn.commit()
cur.close()
conn.close()
#
# Template List functions
#
def list_template(limit, table, is_fuzzy=True):
if limit:
if is_fuzzy:
# Handle fuzzy vs. non-fuzzy limits
if not re.match('\^.*', limit):
limit = '%' + limit
else:
limit = limit[1:]
if not re.match('.*\$', limit):
limit = limit + '%'
else:
limit = limit[:-1]
args = (limit, )
query = "SELECT * FROM {} WHERE name LIKE %s;".format(table)
else:
args = ()
query = "SELECT * FROM {};".format(table)
conn, cur = open_database(config)
cur.execute(query, args)
data = cur.fetchall()
if table == 'network_template':
for template_id, template_data in enumerate(data):
# Fetch list of VNIs from network table
query = "SELECT vni FROM network WHERE network_template = %s;"
args = (template_data['id'],)
cur.execute(query, args)
vnis = cur.fetchall()
data[template_id]['networks'] = vnis
if table == 'storage_template':
for template_id, template_data in enumerate(data):
# Fetch list of VNIs from network table
query = "SELECT * FROM storage WHERE storage_template = %s;"
args = (template_data['id'],)
cur.execute(query, args)
disks = cur.fetchall()
data[template_id]['disks'] = disks
close_database(conn, cur)
return data
def list_template_system(limit, is_fuzzy=True):
"""
Obtain a list of system templates.
"""
data = list_template(limit, 'system_template', is_fuzzy)
return data
def list_template_network(limit, is_fuzzy=True):
"""
Obtain a list of network templates.
"""
data = list_template(limit, 'network_template', is_fuzzy)
return data
def list_template_network_vnis(name):
"""
Obtain a list of network template VNIs.
"""
data = list_template(name, 'network_template', is_fuzzy=False)[0]
networks = data['networks']
return networks
def list_template_storage(limit, is_fuzzy=True):
"""
Obtain a list of storage templates.
"""
data = list_template(limit, 'storage_template', is_fuzzy)
return data
def list_template_storage_disks(name):
"""
Obtain a list of storage template disks.
"""
data = list_template(name, 'storage_template', is_fuzzy=False)[0]
disks = data['disks']
return disks
def template_list(limit):
system_templates = list_template_system(limit)
network_templates = list_template_network(limit)
storage_templates = list_template_storage(limit)
return { "system_templates": system_templates, "network_templates": network_templates, "storage_templates": storage_templates }
#
# Template Create functions
#
def create_template_system(name, vcpu_count, vram_mb, serial=False, vnc=False, vnc_bind=None):
if list_template_system(name, is_fuzzy=False):
retmsg = { "message": "The system template {} already exists".format(name) }
retcode = 400
return flask.jsonify(retmsg), retcode
query = "INSERT INTO system_template (name, vcpu_count, vram_mb, serial, vnc, vnc_bind) VALUES (%s, %s, %s, %s, %s, %s);"
args = (name, vcpu_count, vram_mb, serial, vnc, vnc_bind)
conn, cur = open_database(config)
try:
cur.execute(query, args)
retmsg = { "name": name }
retcode = 200
except psycopg2.IntegrityError as e:
retmsg = { "message": "Failed to create entry {}".format(name), "error": e }
retcode = 400
close_database(conn, cur)
return flask.jsonify(retmsg), retcode
def create_template_network(name, mac_template=None):
if list_template_network(name, is_fuzzy=False):
retmsg = { "message": "The network template {} already exists".format(name) }
retcode = 400
return flask.jsonify(retmsg), retcode
conn, cur = open_database(config)
try:
query = "INSERT INTO network_template (name, mac_template) VALUES (%s, %s);"
args = (name, mac_template)
cur.execute(query, args)
retmsg = { "name": name }
retcode = 200
except psycopg2.IntegrityError as e:
retmsg = { "message": "Failed to create entry {}".format(name), "error": e }
retcode = 400
close_database(conn, cur)
return flask.jsonify(retmsg), retcode
def create_template_network_element(name, network):
if not list_template_network(name, is_fuzzy=False):
retmsg = { "message": "The network template {} does not exist".format(name) }
retcode = 400
return flask.jsonify(retmsg), retcode
networks = list_template_network_vnis(name)
found_vni = False
for network in networks:
if network['vni'] == vni:
found_vni = True
if found_vni:
retmsg = { "message": "The VNI {} in network template {} already exists".format(vni, name) }
retcode = 400
return flask.jsonify(retmsg), retcode
conn, cur = open_database(config)
try:
query = "SELECT id FROM network_template WHERE name = %s;"
args = (name,)
cur.execute(query, args)
template_id = cur.fetchone()['id']
query = "INSERT INTO network (network_template, vni) VALUES (%s, %s);"
args = (template_id, network)
cur.execute(query, args)
retmsg = { "name": name, "vni": network }
retcode = 200
except psycopg2.IntegrityError as e:
retmsg = { "message": "Failed to create entry {}".format(network), "error": e }
retcode = 400
close_database(conn, cur)
return flask.jsonify(retmsg), retcode
def create_template_storage(name):
if list_template_storage(name, is_fuzzy=False):
retmsg = { "message": "The storage template {} already exists".format(name) }
retcode = 400
return flask.jsonify(retmsg), retcode
conn, cur = open_database(config)
try:
query = "INSERT INTO storage_template (name) VALUES (%s);"
args = (name,)
cur.execute(query, args)
retmsg = { "name": name }
retcode = 200
except psycopg2.IntegrityError as e:
retmsg = { "message": "Failed to create entry {}".format(name), "error": e }
retcode = 400
close_database(conn, cur)
return flask.jsonify(retmsg), retcode
def create_template_storage_element(name, disk_id, disk_size_gb, mountpoint=None, filesystem=None):
if not list_template_storage(name, is_fuzzy=False):
retmsg = { "message": "The storage template {} does not exist".format(name) }
retcode = 400
return flask.jsonify(retmsg), retcode
disks = list_template_storage_disks(name)
found_disk = False
for disk in disks:
if disk['disk_id'] == disk_id:
found_disk = True
if found_disk:
retmsg = { "message": "The disk {} in storage template {} already exists".format(disk_id, name) }
retcode = 400
return flask.jsonify(retmsg), retcode
if mountpoint and not filesystem:
retmsg = { "message": "A filesystem must be specified along with a mountpoint." }
retcode = 400
return flask.jsonify(retmsg), retcode
conn, cur = open_database(config)
try:
query = "SELECT id FROM storage_template WHERE name = %s;"
args = (name,)
cur.execute(query, args)
template_id = cur.fetchone()['id']
query = "INSERT INTO storage (storage_template, disk_id, disk_size_gb, mountpoint, filesystem) VALUES (%s, %s, %s, %s, %s);"
args = (template_id, disk_id, disk_size_gb, mountpoint, filesystem)
cur.execute(query, args)
retmsg = { "name": name, "disk_id": disk_id }
retcode = 200
except psycopg2.IntegrityError as e:
retmsg = { "message": "Failed to create entry {}".format(disk_id), "error": e }
retcode = 400
close_database(conn, cur)
return flask.jsonify(retmsg), retcode
def delete_template_system(name):
if not list_template_system(name, is_fuzzy=False):
retmsg = { "message": "The system template {} does not exist".format(name) }
retcode = 400
return flask.jsonify(retmsg), retcode
conn, cur = open_database(config)
try:
query = "DELETE FROM system_template WHERE name = %s;"
args = (name,)
cur.execute(query, args)
retmsg = { "name": name }
retcode = 200
except psycopg2.IntegrityError as e:
retmsg = { "message": "Failed to delete entry {}".format(name), "error": e }
retcode = 400
close_database(conn, cur)
return flask.jsonify(retmsg), retcode
def delete_template_network(name):
if not list_template_network(name, is_fuzzy=False):
retmsg = { "message": "The network template {} does not exist".format(name) }
retcode = 400
return flask.jsonify(retmsg), retcode
conn, cur = open_database(config)
try:
query = "SELECT id FROM network_template WHERE name = %s;"
args = (name,)
cur.execute(query, args)
template_id = cur.fetchone()['id']
query = "DELETE FROM network WHERE network_template = %s;"
args = (template_id,)
cur.execute(query, args)
query = "DELETE FROM network_template WHERE name = %s;"
args = (name,)
cur.execute(query, args)
retmsg = { "name": name }
retcode = 200
except psycopg2.IntegrityError as e:
retmsg = { "message": "Failed to delete entry {}".format(name), "error": e }
retcode = 400
close_database(conn, cur)
return flask.jsonify(retmsg), retcode
def delete_template_network_element(name, vni):
if not list_template_network(name, is_fuzzy=False):
retmsg = { "message": "The network template {} does not exist".format(name) }
retcode = 400
return flask.jsonify(retmsg), retcode
networks = list_template_network_vnis(name)
found_vni = False
for network in networks:
if network['vni'] == vni:
found_vni = True
if not found_vni:
retmsg = { "message": "The VNI {} in network template {} does not exist".format(vni, name) }
retcode = 400
return flask.jsonify(retmsg), retcode
conn, cur = open_database(config)
try:
query = "SELECT id FROM network_template WHERE name = %s;"
args = (name,)
cur.execute(query, args)
template_id = cur.fetchone()['id']
query = "DELETE FROM network WHERE network_template = %s and vni = %s;"
args = (template_id, vni)
cur.execute(query, args)
retmsg = { "name": name, "vni": vni }
retcode = 200
except psycopg2.IntegrityError as e:
retmsg = { "message": "Failed to delete entry {}".format(name), "error": e }
retcode = 400
close_database(conn, cur)
return flask.jsonify(retmsg), retcode
def delete_template_storage(name):
if not list_template_storage(name, is_fuzzy=False):
retmsg = { "message": "The storage template {} does not exist".format(name) }
retcode = 400
return flask.jsonify(retmsg), retcode
conn, cur = open_database(config)
try:
query = "SELECT id FROM storage_template WHERE name = %s;"
args = (name,)
cur.execute(query, args)
template_id = cur.fetchone()['id']
query = "DELETE FROM storage WHERE storage_template = %s;"
args = (template_id,)
cur.execute(query, args)
query = "DELETE FROM storage_template WHERE name = %s;"
args = (name,)
cur.execute(query, args)
retmsg = { "name": name }
retcode = 200
except psycopg2.IntegrityError as e:
retmsg = { "message": "Failed to delete entry {}".format(name), "error": e }
retcode = 400
close_database(conn, cur)
return flask.jsonify(retmsg), retcode
def delete_template_storage_element(name, disk_id):
if not list_template_storage(name, is_fuzzy=False):
retmsg = { "message": "The storage template {} does not exist".format(name) }
retcode = 400
return flask.jsonify(retmsg), retcode
disks = list_template_storage_disks(name)
found_disk = False
for disk in disks:
if disk['disk_id'] == disk_id:
found_disk = True
if not found_disk:
retmsg = { "message": "The disk {} in storage template {} does not exist".format(disk_id, name) }
retcode = 400
return flask.jsonify(retmsg), retcode
conn, cur = open_database(config)
try:
query = "SELECT id FROM storage_template WHERE name = %s;"
args = (name,)
cur.execute(query, args)
template_id = cur.fetchone()['id']
query = "DELETE FROM storage WHERE storage_template = %s and disk_id = %s;"
args = (template_id, disk_id)
cur.execute(query, args)
retmsg = { "name": name, "disk_id": disk_id }
retcode = 200
except psycopg2.IntegrityError as e:
retmsg = { "message": "Failed to delete entry {}".format(name), "error": e }
retcode = 400
close_database(conn, cur)
return flask.jsonify(retmsg), retcode
#
# Script functions
#
def list_script(limit, is_fuzzy=True):
if limit:
if is_fuzzy:
# Handle fuzzy vs. non-fuzzy limits
if not re.match('\^.*', limit):
limit = '%' + limit
else:
limit = limit[1:]
if not re.match('.*\$', limit):
limit = limit + '%'
else:
limit = limit[:-1]
query = "SELECT * FROM {} WHERE name LIKE %s;".format('script')
args = (limit, )
else:
query = "SELECT * FROM {};".format('script')
args = ()
conn, cur = open_database(config)
cur.execute(query, args)
data = cur.fetchall()
close_database(conn, cur)
return data
def create_script(name, script):
if list_script(name, is_fuzzy=False):
retmsg = { "message": "The script {} already exists".format(name) }
retcode = 400
return flask.jsonify(retmsg), retcode
conn, cur = open_database(config)
try:
query = "INSERT INTO script (name, script) VALUES (%s, %s);"
args = (name, script)
cur.execute(query, args)
retmsg = { "name": name }
retcode = 200
except psycopg2.IntegrityError as e:
retmsg = { "message": "Failed to create entry {}".format(name), "error": e }
retcode = 400
close_database(conn, cur)
return flask.jsonify(retmsg), retcode
def delete_script(name):
if not list_script(name, is_fuzzy=False):
retmsg = { "message": "The script {} does not exist".format(name) }
retcode = 400
return flask.jsonify(retmsg), retcode
conn, cur = open_database(config)
try:
query = "DELETE FROM script WHERE name = %s;"
args = (name,)
cur.execute(query, args)
retmsg = { "name": name }
retcode = 200
except psycopg2.IntegrityError as e:
retmsg = { "message": "Failed to delete entry {}".format(name), "error": e }
retcode = 400
close_database(conn, cur)
return flask.jsonify(retmsg), retcode
#
# Profile functions
#
def list_profile(limit, is_fuzzy=True):
if limit:
if is_fuzzy:
# Handle fuzzy vs. non-fuzzy limits
if not re.match('\^.*', limit):
limit = '%' + limit
else:
limit = limit[1:]
if not re.match('.*\$', limit):
limit = limit + '%'
else:
limit = limit[:-1]
query = "SELECT * FROM {} WHERE name LIKE %s;".format('profile')
args = (limit, )
else:
query = "SELECT * FROM {};".format('profile')
args = ()
conn, cur = open_database(config)
cur.execute(query, args)
orig_data = cur.fetchall()
data = list()
for profile in orig_data:
profile_data = dict()
profile_data['name'] = profile['name']
for etype in 'system_template', 'network_template', 'storage_template', 'script':
query = 'SELECT name from {} WHERE id = %s'.format(etype)
args = (profile[etype],)
cur.execute(query, args)
name = cur.fetchone()['name']
profile_data[etype] = name
data.append(profile_data)
close_database(conn, cur)
return data
def create_profile(name, system_template, network_template, storage_template, script):
if list_profile(name, is_fuzzy=False):
retmsg = { "message": "The profile {} already exists".format(name) }
retcode = 400
return flask.jsonify(retmsg), retcode
system_templates = list_template_system(None)
system_template_id = None
for template in system_templates:
if template['name'] == system_template:
system_template_id = template['id']
if not system_template_id:
retmsg = { "message": "The system template {} for profile {} does not exist".format(system_template, name) }
retcode = 400
return flask.jsonify(retmsg), retcode
network_templates = list_template_network(None)
network_template_id = None
for template in network_templates:
if template['name'] == network_template:
network_template_id = template['id']
if not network_template_id:
retmsg = { "message": "The network template {} for profile {} does not exist".format(network_template, name) }
retcode = 400
return flask.jsonify(retmsg), retcode
storage_templates = list_template_storage(None)
storage_template_id = None
for template in storage_templates:
if template['name'] == storage_template:
storage_template_id = template['id']
if not storage_template_id:
retmsg = { "message": "The storage template {} for profile {} does not exist".format(storage_template, name) }
retcode = 400
return flask.jsonify(retmsg), retcode
scripts = list_script(None)
script_id = None
for scr in scripts:
if scr['name'] == script:
script_id = scr['id']
if not script_id:
retmsg = { "message": "The script {} for profile {} does not exist".format(script, name) }
retcode = 400
return flask.jsonify(retmsg), retcode
conn, cur = open_database(config)
try:
query = "INSERT INTO profile (name, system_template, network_template, storage_template, script) VALUES (%s, %s, %s, %s, %s);"
args = (name, system_template_id, network_template_id, storage_template_id, script_id)
cur.execute(query, args)
retmsg = { "name": name }
retcode = 200
except psycopg2.IntegrityError as e:
retmsg = { "message": "Failed to create entry {}".format(name), "error": e }
retcode = 400
close_database(conn, cur)
return flask.jsonify(retmsg), retcode
def delete_profile(name):
if not list_profile(name, is_fuzzy=False):
retmsg = { "message": "The profile {} does not exist".format(name) }
retcode = 400
return flask.jsonify(retmsg), retcode
conn, cur = open_database(config)
try:
query = "DELETE FROM profile WHERE name = %s;"
args = (name,)
cur.execute(query, args)
retmsg = { "name": name }
retcode = 200
except psycopg2.IntegrityError as e:
retmsg = { "message": "Failed to delete entry {}".format(name), "error": e }
retcode = 400
close_database(conn, cur)
return flask.jsonify(retmsg), retcode
#
# Job functions
#
def create_vm(vm_name, profile_name):
pass

View File

@ -0,0 +1,932 @@
#!/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()

View File

@ -0,0 +1,55 @@
---
# pvc-provisioner client configuration file example
#
# This configuration file specifies details for the PVC provisioner client
# running on this machine. Default values are not supported; the values in
# this sample configuration are considered defaults and can be used as-is.
#
# Copy this example to /etc/pvc/pvc-provisioner.yaml and edit to your needs.
#
# Alternatively, you may combine this configuration (anything under the
# `provisioner` section) with a PVC API configuration in a single file, and
# create links between them. By default, the only difference is the
# provisioner header and the listen port specifically.
pvc:
# debug: Enable/disable API debug mode
debug: True
# provisioner: Configuration of the Provisioner API listener
provisioner:
# listen_address: IP address(es) to listen on; use 0.0.0.0 for all interfaces
listen_address: "127.0.0.1"
# listen_port: TCP port to listen on, usually 7375
listen_port: "7375"
# authentication: Authentication and security settings
authentication:
# enabled: Enable or disable authentication (True/False)
enabled: False
# secret_key: Per-cluster secret key for API cookies; generate with uuidgen or pwgen
secret_key: ""
# tokens: a list of authentication tokens; leave as an empty list to disable authentication
tokens:
# description: token description for management
- description: "testing"
# token: random token for authentication; generate with uuidgen or pwgen
token: ""
# ssl: SSL configuration
ssl:
# enabled: Enabled or disable SSL operation (True/False)
enabled: False
# cert_file: SSL certificate file
cert_file: ""
# key_file: SSL certificate key file
key_file: ""
# database: Backend database configuration
database:
# host: PostgreSQL hostname, invariably 'localhost
host: 10.100.0.252
# port: PostgreSQL port, invariably 'localhost'
port: 5432
# name: PostgreSQL database name, invariably 'pvcprov'
name: pvcprov
# user: PostgreSQL username, invariable 'pvcprov'
user: pvcprov
# pass: PostgreSQL user password, randomly generated
pass: pvcprov

View File

@ -0,0 +1,16 @@
# Parallel Virtual Cluster Provisioner client daemon unit file
[Unit]
Description = Parallel Virtual Cluster Provisioner client daemon
After = network-online.target
[Service]
Type = simple
WorkingDirectory = /usr/share/pvc
Environment = PYTHONUNBUFFERED=true
Environment = PVC_CONFIG_FILE=/etc/pvc/pvc-provisioner.yaml
ExecStart = /usr/share/pvc/pvc-provisioner.py
Restart = on-failure
[Install]
WantedBy = multi-user.target

View File

@ -0,0 +1,12 @@
create database pvcprov owner pvcprov;
\c pvcprov
create table system_template (id SERIAL PRIMARY KEY, name TEXT NOT NULL UNIQUE, vcpu_count INT NOT NULL, vram_mb INT NOT NULL, serial BOOL NOT NULL, vnc BOOL NOT NULL, vnc_bind TEXT);
create table network_template (id SERIAL PRIMARY KEY, name TEXT NOT NULL UNIQUE, mac_template TEXT);
create table network (id SERIAL PRIMARY KEY, network_template INT REFERENCES network_template(id), vni INT NOT NULL);
create table storage_template (id SERIAL PRIMARY KEY, name TEXT NOT NULL UNIQUE);
create table storage (id SERIAL PRIMARY KEY, storage_template INT REFERENCES storage_template(id), disk_id TEXT NOT NULL, disk_size_gb INT NOT NULL, mountpoint TEXT, filesystem TEXT);
create table script (id SERIAL PRIMARY KEY, name TEXT NOT NULL UNIQUE, script TEXT NOT NULL);
create table profile (id SERIAL PRIMARY KEY, name TEXT NOT NULL UNIQUE, system_template INT REFERENCES system_template(id), network_template INT REFERENCES network_template(id), storage_template INT REFERENCES storage_template(id), script INT REFERENCES script(id));
grant all privileges on database pvcprov to pvcprov;
grant all privileges on all tables in schema public to pvcprov;
grant all privileges on all sequences in schema public to pvcprov;