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:
parent
356c12db2e
commit
4a7c6db9b2
|
@ -0,0 +1 @@
|
|||
../client-common
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
@ -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()
|
|
@ -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
|
|
@ -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
|
|
@ -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;
|
|
@ -0,0 +1,305 @@
|
|||
# PVC Provisioner API architecture
|
||||
|
||||
The PVC Provisioner API is a standalone client application for PVC. It interfaces directly with the Zookeeper database to manage state, and with the Patroni PostgreSQL database to store configuration details.
|
||||
|
||||
The Provisioner is built using Flask and is packaged in the Debian package `pvc-client-provisioner`. The Provisioner depends on the common client functions of the `pvc-client-common` package as does the CLI client.
|
||||
|
||||
Details of the Provisioner API interface can be found in [the manual](/manuals/provisioner).
|
||||
|
||||
## Purpose
|
||||
|
||||
The purpose of the Provisioner API is to provide a convenient way for administrators to automate the creation of new virtual machines on the PVC cluster.
|
||||
|
||||
The Provisioner allows the administrator to create "templates", a unified set of configurations, which VMs can then use. These templates configure the VM resources (memory, disk, metadata), VM networks, and VM disks separately, allowing the administrator to specify very granular and dynamic configurations for new virtual machines.
|
||||
|
||||
Upon triggering a new VM creation, the provisioner also has facilities to create new virtual machines in three main ways:
|
||||
|
||||
1. Via cloning an existing RBD disk image, then performing optional post-clone actions on the volume(s).
|
||||
2. Via booting an installer ISO image, stored as an RBD disk image.
|
||||
3. Via custom provisioning scripts provided by the administrator.
|
||||
|
||||
The first option allows administrators to quickly create new virtual machines based on an existing image, either uploaded by the administrator or created from an existing virtual machine.
|
||||
|
||||
The second option allows administrators to install arbitrary operating systems via ISO images, which are uploaded by the administrator. Usually, auto-configuring/kickstarted ISOs are ideal for this purpose.
|
||||
|
||||
The third method provides extreme flexibility in setting up Unix-based virtual machines, as standard, arbitrary Python scripts can be provided by the administrator, allowing the system to automatically install and configure the VM exactly to the specifications they want. Furthermore, PVC includes integrated support for `cloud-init` inside VMs, for maximum flexibility in post-install configurations.
|
||||
|
||||
## System Templates
|
||||
|
||||
The PVC Provisioner has three categories of templates to specify the resources allocated to the virtual machine. They are: System Templates, Network Templates, and Disk Templates.
|
||||
|
||||
### System Templates
|
||||
|
||||
System templates specify the basic resources of the virtual machine: vCPUs, memory, and configuration metadata (e.g. serial/VNC/Spice consoles, migration methods, additional devices, etc.). PVC VMs use the Libvirt XML configuration format, so these templates specify the required values in the created VM configuration file. When querying details, the API will return JSON representations of the configuration, which are used here for examples.
|
||||
|
||||
vCPU and memory configurations are specified explicitly. For instance, a template might be called `small_webserver` and specify 2 `vcpus` and 2GB (always specified in MB) of `memory`:
|
||||
|
||||
```
|
||||
"small_webserver": {
|
||||
"vcpus": 2,
|
||||
"memory": 2048
|
||||
}
|
||||
```
|
||||
|
||||
Additional, non-default configuration values can also be specified. For instance, one can specify the `console_type` and additional values for this:
|
||||
|
||||
```
|
||||
"serial_server": {
|
||||
"vcpus": 1,
|
||||
"memory": 1024,
|
||||
"console_type": "serial",
|
||||
"serial_device": "auto",
|
||||
"serial_logfile": "/var/log/libvirt/VMNAME.xml"
|
||||
}
|
||||
```
|
||||
|
||||
The serial logfile can also be "auto" - this enables the PVC `vm log` functionality. The literal string `VMNAME` in this value will be replaced with the virtual machine name.
|
||||
|
||||
Configuration for a VNC console is similar:
|
||||
|
||||
```
|
||||
"vnc_server": {
|
||||
"vcpus": 4,
|
||||
"memory": 4096,
|
||||
"console_type": "vnc",
|
||||
"vnc_port": "auto",
|
||||
"vnc_listen": "0.0.0.0"
|
||||
}
|
||||
```
|
||||
|
||||
Additional PVC metadata can be configured in these templates as well. For example:
|
||||
|
||||
```
|
||||
"limited_server": {
|
||||
"vcpus": 1,
|
||||
"memory": 1024,
|
||||
"pvc_node_limit": "pvchv1,pvchv2",
|
||||
"pvc_node_selector": "vms",
|
||||
"pvc_node_autostart": "True"
|
||||
}
|
||||
```
|
||||
|
||||
### Network Templates
|
||||
|
||||
Network template specify which PVC networks the virtual machine is active in, as well as the method used to calculate MAC addresses for VM interfaces. Networks are specified by their VNI ID or description within PVC.
|
||||
|
||||
For example, a system with a single interface and autogenerated MAC address:
|
||||
|
||||
```
|
||||
"single_net_auto": {
|
||||
"networks": {
|
||||
"client-net-01"
|
||||
},
|
||||
"macaddr": "auto"
|
||||
}
|
||||
```
|
||||
|
||||
In some cases, it may be useful for the administrator to specify a static MAC address pattern for a set of VMs, for instance if they must get consistent DHCP reservations between rebuilds. The `macaddr` field can contain templated MAC address values, in the format `AA:AA:AA:XX:XX:YZ`. In this format, `A` represents the OUI portion (usually the KVM default of `52:54:00`), `X` represents a static prefix specified by the administrator, `Y` represents the VM number/ID, which is autofilled by PVC based on the VM name (or set to 0 for numberless VM names), and `Z` represents the incremental interface ID within the VM. Therefore, to configure a static MAC address, the template could be:
|
||||
|
||||
```
|
||||
"double_net_templated_mac": {
|
||||
"networks": {
|
||||
"5927",
|
||||
"5928"
|
||||
},
|
||||
"macaddr": "52:54:00:00:01:YZ"
|
||||
}
|
||||
```
|
||||
|
||||
Note the literal `Y` and `Z` characters in the value. This will expand to the following MAC addresses for a VM called `web3`, which would have VM number/ID `3`:
|
||||
|
||||
* Network `5927`: `52:54:00:00:01:30`
|
||||
* Network `5928`: `52:54:00:00:01:31`
|
||||
|
||||
Similarly, a VM called `accounting`, which would have the implied VM number/ID `0`, would expand to:
|
||||
|
||||
* Network `5927`: `52:54:00:00:01:00`
|
||||
* Network `5928`: `52:54:00:00:01:01`
|
||||
|
||||
Note that these automated values do not overflow; therefore, PVC does not support templated MAC addresses for >9 numbered VMs (e.g. web1-web9) within a single template, or for >10 networks within each VM. For such cases, static MAC addresses are far less useful anyways and the administrator must consider this. Also note that assigning the same static MAC template to overlapping numbered VMs (e.g. web1-web3 and mail1-mail3) will result in MAC address conflicts within a given client network and must be avoided.
|
||||
|
||||
### Disk Templates
|
||||
|
||||
Disk templates specify the disk layout, including filesystem and mountpoint for scripted deployments, for the VM. Disks are specified by their virtual disk name within the VM, and sizes are always specified in GB. For a basic, unmanaged VM with a single disk, the template may be as simple as:
|
||||
|
||||
```
|
||||
"single_disk": {
|
||||
"vda": {
|
||||
"size": 20
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For a scripted VM, two additional values should be specified: the filesystem, which must be a valid filesystem usable by the VM, and the mountpoint:
|
||||
|
||||
```
|
||||
"scripted_single_disk": {
|
||||
"vda": {
|
||||
"size": 20,
|
||||
"filesystem": "ext4",
|
||||
"mountpoint": "/"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note that these values are technically optional: if unspecified, PVC will not create a filesystem on the device nor attempt to mount it during the scripted configuration steps. This allows administrators to attach unallocated block devices to scripted VMs as well as the main filesystem(s) that the OS will be installed on.
|
||||
|
||||
More complicated disk templates are also possible by specifying incrementing `vdX` devices in the VM, for example:
|
||||
|
||||
```
|
||||
scripted_multi_disk_srv": {
|
||||
"vda": {
|
||||
"size": 4,
|
||||
"filesystem": "ext4",
|
||||
"mountpoint": "/"
|
||||
},
|
||||
"vdb": {
|
||||
"size": 8,
|
||||
"filesystem": "ext4",
|
||||
"mountpoint": "/var"
|
||||
},
|
||||
"vdc": {
|
||||
"size": 40,
|
||||
"filesystem": "xfs",
|
||||
"mountpoint": "/srv"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## System Definitions
|
||||
|
||||
At the next level above configuraton templates, system definitions provide a way to group templates together to provide standard definitions for classes of virtual machines. This definition can then be specified, or autodetected, instead of manually specifying the 3 resource templates on VM creation, as well as specify additional provisioning metadata including the install method and provisioning script template, if applicable.
|
||||
|
||||
It is generally a good idea to make use of system definitions, rather than manually specifying all values at install time, in order to reduce the possibility of administrative mistakes in provisioning new VMs. They are however optional: all the required configuration information may be specified explicitly by the administrator when creating a new VM, instead of using a definition.
|
||||
|
||||
The `autostart` option specifies to PVC whether the VM should be automatically started after the provisioning sequence completes. It defaults to "True", and this can be avoided by setting this value to "False", requiring the administrator to manually start the VM using PVC commands afterwards.
|
||||
|
||||
For example, here are several VM definitions using some of the example system templates above:
|
||||
|
||||
```
|
||||
"webX": {
|
||||
"templates": {
|
||||
"system": "small_webserver",
|
||||
"network": "double_net_templated_mac",
|
||||
"disk": "scripted_single_disk"
|
||||
},
|
||||
"provisioner": {
|
||||
"method": "script",
|
||||
"script": {
|
||||
"name": "basic-pvc-debian",
|
||||
"arguments": {
|
||||
"keyword_argument": "my_value",
|
||||
"another_argument": "some_value"
|
||||
}
|
||||
}
|
||||
},
|
||||
"autostart": "False"
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
"windows-10": {
|
||||
"templates": {
|
||||
"system": "vnc_server",
|
||||
"network": "single_net_auto",
|
||||
"disk": "single_disk"
|
||||
},
|
||||
"provisioner": {
|
||||
"method": "iso",
|
||||
"iso": "installers/windows-10-installer-201910"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
"cloned_mail": {
|
||||
"templates": {
|
||||
"system": "limited_server",
|
||||
"network": "single_net_auto",
|
||||
"disk": "scripted_multi_disk_srv"
|
||||
}
|
||||
"provisioner": {
|
||||
"method": "clone",
|
||||
"clone": {
|
||||
"source_disks": {
|
||||
"vda": "templates/mailX_root",
|
||||
"vdb": "templates/mailX_var"
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Scripted installs
|
||||
|
||||
Scripted installs specify the `script` `method` in their `provisioner` metadata section. The second value, named `script`, specifies the provisioner script name which must exist, as well as any additional arguments the administrator may wish to pass to the script functions. Provisioner scripts are explained in detail in a subsequent section.
|
||||
|
||||
### ISO installs
|
||||
|
||||
ISO installs specify the `iso` `method` in their `provisioner` metadata section. The second value, named `iso`, specifies the RBD image containing the ISO which must exist having been previously uploaded by the administrator. The VM is booted immediately after basic configuration, and control is passed to the ISO to perform any installation steps; no other configuration occurrs from the PVC side.
|
||||
|
||||
### Clone installs
|
||||
|
||||
Clone installs specify the`clone` `method` in their `provisioner` metadata section. The second value, named `clone`, specifies the target virtual devices and their corresponding source RBD images, as well as the provisioner script to run after cloning.
|
||||
|
||||
Within the `clone` section, the `source_disks` section specifies a list of disks to clone as well as the target device. These target devices must align with disks from the Disk template, to map the source volumes to the new volumes for the VM. For example, if the Disk template specifies `vda` as a disk with `mountpoint` `/` (the `size` and `filesystem` will be ignored), and the `source_disks` value for `vda` maps to the RBD image `templates/root`, the provisioner will clone the RBD image `templates/root` to a new volume for the VM named, for example, `vms/VMNAME_vda`. If there are additional disks specified in the Disk template that are not specified in the `source_disks` list, they will be created as normal.
|
||||
|
||||
PVC performs no actions to a clone deployment aside from creating the additional disks mentioned above, if applicable. All configuration of the clone is the responsibility of the administrator. The cloud-init support from the `script` install method can be useful in this case to create a "golden image" that will then use cloud-init to configure itself on first boot.
|
||||
|
||||
## Provisioning scripts
|
||||
|
||||
The PVC provisioner provides a scripting framework in order to automate VM installation. This is generally the most useful with UNIX-like systems which can be installed over the network via shell scripts. For instance, the script might install a Debian VM using `debootstrap`.
|
||||
|
||||
Provisioner scripts are written in Python 3 and are called in a standardized way during the provisioning sequence. A single function called `install` is called during the provisioning sequence, performing OS installation, after which the system is booted.
|
||||
|
||||
The flow of the provisioning sequence is as follows:
|
||||
|
||||
1. The provisioner creates the required disks.
|
||||
1. The provisioner creates a temporary directory on the local system (often the primary hypervisor, but the provisioner may be run in a dedicated virtual machine).
|
||||
1. The provisioner maps the VM's RBD volumes on the local system.
|
||||
1. The provisioner mounts the RBD volumes at their `mountpoint` under the temporary directory, along with several temporary virtual filesystems bind-mounted from the local system.
|
||||
1. The provisioner calls the `install` function of the provisioner script and waits for it to finish execution.
|
||||
1. The provisioner creates any cloud-init configuration files specified.
|
||||
1. The provisioner unmounts the RBD volumes and temporary virtual filesystems (cleanup).
|
||||
1. The provisioner unmaps the RBD volumes from the local system (cleanup).
|
||||
1. The provisioner defines the new VM in PVC and, optionally, starts it.
|
||||
|
||||
*A WARNING*: It's important to remember that these provisioning scripts will run with the same privileges as the provisioner API daemon (usually root) on the system running the daemon. THIS MAY POSE A SECURITY RISK. However, the intent is that administrators of the cluster are the only ones allowed to specify these scripts, and that they check them thoroughly when adding them to the system as well as limit access to the provisioning API to trusted sources. If neither of these conditions are possible, for instance if arbitrary users must specify custom scripts without administrator oversight, then the PVC provisoner may not be ideal, and administrators are encouraged to implement their own custom provisioning engine.
|
||||
|
||||
### `install` function
|
||||
|
||||
The `install` function is the main entrypoing for a provisioning script, and is the only part of the script that is explicitly called. The provisioner calls this function after setting up the temporary install directory and mounting the volumes. Thus, this script can then perform any sort of tasks required in the VM to install it, and then finishes.
|
||||
|
||||
This function is passed a number of keyword arguments that it can then use during installation, described below, as well as any keyword arguments passed via optional arguments to the script.
|
||||
|
||||
###### `vm_name`
|
||||
|
||||
The `vm_name` keyword argument contains the full name of the new VM.
|
||||
|
||||
###### `vm_id`
|
||||
|
||||
The `vm_id` keyword argument contains the VM identifier (the last numeral of the VM name, or `0` for a VM that does not end in a numeral).
|
||||
|
||||
###### `temporary_directory`
|
||||
|
||||
The `temporary_directory` keyword argument contains the path to the temporary directory on which the new VM's disks are mounted. The function *must* perform any installation steps to/under this directory.
|
||||
|
||||
###### `disks`
|
||||
|
||||
The `disks` keyword argument contains a Python list of the configured disks, as dictionaries of values as specified in the Disk template. The function *may* use these values as appropriate, for instance to specify an `/etc/fstab`.
|
||||
|
||||
###### `networks`
|
||||
|
||||
The `networks` keyword argument contains a Python list of the configured networks, as dictionaries of values as specified in the Network template. The function *may* use these values as appropriate, for instance to write an `/etc/network/interfaces` file.
|
||||
|
||||
## Cloud-Init
|
||||
|
||||
PVC contains full support for cloud-init, a tool to automatically configure VMs on first boot from a defined set of metadata. The PVC provisioner includes a cloud-init metadata server that the administrator can use to provide information to running VMs.
|
||||
|
||||
### Configuring Cloud-Init in VMs
|
||||
|
||||
The PVC provisioner sequence makes no special considerations for cloud-init; the administrator must handle the installation of the cloud-init packages as well as any tweaks to the cloud.cfg file in the installation script. The provisioner does however listen on the standard EC2 interface at `http://169.254.169.254/latest/` from within the VM to provision user data.
|
||||
|
||||
### Configuring user-data
|
||||
|
||||
The PVC provisioner supports managing cloud-init user-data from within it. This data will be delivered to VMs based on the configuration options.
|
Loading…
Reference in New Issue