Finish the provisioner and metadata server

This commit is contained in:
Joshua Boniface 2019-12-12 19:41:23 -05:00
parent 88924497c2
commit 708de48065
6 changed files with 577 additions and 26 deletions

View File

@ -113,31 +113,64 @@ def install(**kwargs):
# Append the fstab line
with open(fstab_file, 'a') as fh:
fh.write("/dev/{disk} {mountpoint} {filesystem} {options} {dump} {cpass}\n".format(
data = "/dev/{disk} {mountpoint} {filesystem} {options} {dump} {cpass}\n".format(
disk=disk['disk_id'],
mountpoint=disk['mountpoint'],
filesystem=disk['filesystem'],
options=options,
dump=dump,
cpass=cpass
))
)
fh.write(data)
# Write the hostname
hostname_file = "{}/etc/hostname".format(temporary_directory)
with open(hostname_file, 'w') as fh:
fh.write("{}".format(vm_name))
# Write a DHCP stanza for ens2
# Fix the cloud-init.target since it's broken
cloudinit_target_file = "{}/etc/systemd/system/cloud-init.target".format(temporary_directory)
with open(cloudinit_target_file, 'w') as fh:
data = """[Install]
WantedBy=multi-user.target
[Unit]
Description=Cloud-init target
After=multi-user.target
"""
fh.write(data)
# NOTE: Due to device ordering within the Libvirt XML configuration, the first Ethernet interface
# will always be on PCI bus ID 2, hence the name "ens2".
# Write a DHCP stanza for ens2
ens2_network_file = "{}/etc/network/interfaces.d/ens2".format(temporary_directory)
with open(ens2_network_file, 'w') as fh:
fh.write("auto ens2\niface ens2 inet dhcp\n")
data = """auto ens2
iface ens2 inet dhcp
"""
fh.write(data)
# Write the DHCP config for ens2
dhclient_file = "{}/etc/dhcp/dhclient.conf".format(temporary_directory)
with open(dhclient_file, 'w') as fh:
data = """# DHCP client configuration
# Created by vminstall for host web1.i.bonilan.net
option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;
interface "ens2" {
send host-name = "web1";
send fqdn.fqdn = "web1";
request subnet-mask, broadcast-address, time-offset, routers,
domain-name, domain-name-servers, domain-search, host-name,
dhcp6.name-servers, dhcp6.domain-search, dhcp6.fqdn, dhcp6.sntp-servers,
netbios-name-servers, netbios-scope, interface-mtu,
rfc3442-classless-static-routes, ntp-servers;
}
"""
fh.write(data)
# 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
data = """# Written by the PVC provisioner
GRUB_DEFAULT=0
GRUB_TIMEOUT=1
GRUB_DISTRIBUTOR="PVC Virtual Machine"
@ -146,25 +179,39 @@ 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['disk_id']))
""".format(root_disk=root_disk['disk_id'])
fh.write(data)
# Chroot and install GRUB so we can boot, then exit the chroot
# Chroot, do some in-root tasks, 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)
# Install and update GRUB
os.system(
"grub-install --force /dev/rbd/{}/{}_{}".format(root_disk['pool'], vm_name, root_disk['disk_id'])
)
os.system(
"update-grub"
)
# Set a really dumb root password [TEMPORARY]
os.system(
"echo root:test123 | chpasswd"
)
# Restore our original root
# Enable cloud-init target on (first) boot
# NOTE: Your user-data should handle this and disable it once done, or things get messy.
# That cloud-init won't run without this hack seems like a bug... but even the official
# Debian cloud images are affected, so who knows.
os.system(
"systemctl enable cloud-init.target"
)
# Restore our original root/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.
os.fchdir(real_root)
os.chroot(".")
os.fchdir(real_root)
@ -182,4 +229,4 @@ GRUB_DISABLE_LINUX_UUID=false
del fake_root
del real_root
# Everything else is done via cloud-init
# Everything else is done via cloud-init user-data

View File

@ -0,0 +1,23 @@
#cloud-config
# Example user-data file to set up an alternate /var/home, a first user and some SSH keys, and some packages
bootcmd:
- "mv /home /var/"
- "locale-gen"
package_update: true
packages:
- openssh-server
- sudo
users:
- name: deploy
gecos: Deploy User
homedir: /var/home/deploy
sudo: "ALL=(ALL) NOPASSWD: ALL"
groups: adm, sudo
lock_passwd: true
ssh_authorized_keys:
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBRBGPzlbh5xYD6k8DMZdPNEwemZzKSSpWGOuU72ehfN joshua@bonifacelabs.net 2017-04
usercmd:
- "groupmod -g 200 deploy"
- "usermod -u 200 deploy"
- "userdel debian"
- "systemctl disable cloud-init.target"

View File

@ -1,6 +1,6 @@
#!/usr/bin/env python3
# pvcapi.py - PVC HTTP API functions
# provisioner.py - PVC Provisioner functions
# Part of the Parallel Virtual Cluster (PVC) system
#
# Copyright (C) 2018-2019 Joshua M. Boniface <joshua@boniface.me>
@ -165,12 +165,20 @@ def list_template_storage_disks(name):
disks = data['disks']
return disks
def list_template_userdata(limit, is_fuzzy=True):
"""
Obtain a list of userdata templates.
"""
data = list_template(limit, 'userdata_template', is_fuzzy)
return data
def template_list(limit):
system_templates = list_template_system(limit)
network_templates = list_template_network(limit)
storage_templates = list_template_storage(limit)
userdata_templates = list_template_userdata(limit)
return { "system_templates": system_templates, "network_templates": network_templates, "storage_templates": storage_templates }
return { "system_templates": system_templates, "network_templates": network_templates, "storage_templates": storage_templates, "userdata_templates": userdata_templates }
#
# Template Create functions
@ -304,6 +312,49 @@ def create_template_storage_element(name, pool, disk_id, disk_size_gb, filesyste
close_database(conn, cur)
return flask.jsonify(retmsg), retcode
def create_template_userdata(name, userdata):
if list_template_userdata(name, is_fuzzy=False):
retmsg = { "message": "The userdata template {} already exists".format(name) }
retcode = 400
return flask.jsonify(retmsg), retcode
conn, cur = open_database(config)
try:
query = "INSERT INTO userdata_template (name, userdata) VALUES (%s, %s);"
args = (name, userdata)
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
#
# Template update functions
#
def update_template_userdata(name, userdata):
if not list_template_userdata(name, is_fuzzy=False):
retmsg = { "message": "The userdata template {} does not exist".format(name) }
retcode = 400
return flask.jsonify(retmsg), retcode
tid = list_template_userdata(name, is_fuzzy=False)[0]['id']
conn, cur = open_database(config)
try:
query = "UPDATE userdata_template SET userdata = %s WHERE id = %s;"
args = (userdata, tid)
cur.execute(query, args)
retmsg = { "name": name }
retcode = 200
except psycopg2.IntegrityError as e:
retmsg = { "message": "Failed to update entry {}".format(name), "error": e }
retcode = 400
close_database(conn, cur)
return flask.jsonify(retmsg), retcode
#
# Template Delete functions
#
@ -444,6 +495,25 @@ def delete_template_storage_element(name, disk_id):
close_database(conn, cur)
return flask.jsonify(retmsg), retcode
def delete_template_userdata(name):
if not list_template_userdata(name, is_fuzzy=False):
retmsg = { "message": "The userdata template {} does not exist".format(name) }
retcode = 400
return flask.jsonify(retmsg), retcode
conn, cur = open_database(config)
try:
query = "DELETE FROM userdata_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
#
# Script functions
#
@ -491,6 +561,27 @@ def create_script(name, script):
close_database(conn, cur)
return flask.jsonify(retmsg), retcode
def update_script(name, script):
if not list_script(name, is_fuzzy=False):
retmsg = { "message": "The script {} does not exist".format(name) }
retcode = 400
return flask.jsonify(retmsg), retcode
tid = list_script(name, is_fuzzy=False)[0]['id']
conn, cur = open_database(config)
try:
query = "UPDATE script SET script = %s WHERE id = %s;"
args = (script, tid)
cur.execute(query, args)
retmsg = { "name": name }
retcode = 200
except psycopg2.IntegrityError as e:
retmsg = { "message": "Failed to update 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) }
@ -540,7 +631,7 @@ def list_profile(limit, is_fuzzy=True):
profile_data = dict()
profile_data['name'] = profile['name']
# Parse the name of each subelement
for etype in 'system_template', 'network_template', 'storage_template', 'script':
for etype in 'system_template', 'network_template', 'storage_template', 'userdata_template', 'script':
query = 'SELECT name from {} WHERE id = %s'.format(etype)
args = (profile[etype],)
cur.execute(query, args)
@ -553,7 +644,7 @@ def list_profile(limit, is_fuzzy=True):
close_database(conn, cur)
return data
def create_profile(name, system_template, network_template, storage_template, script, arguments=[]):
def create_profile(name, system_template, network_template, storage_template, userdata_template, script, arguments=[]):
if list_profile(name, is_fuzzy=False):
retmsg = { "message": "The profile {} already exists".format(name) }
retcode = 400
@ -589,6 +680,16 @@ def create_profile(name, system_template, network_template, storage_template, sc
retcode = 400
return flask.jsonify(retmsg), retcode
userdata_templates = list_template_userdata(None)
userdata_template_id = None
for template in userdata_templates:
if template['name'] == userdata_template:
userdata_template_id = template['id']
if not userdata_template_id:
retmsg = { "message": "The userdata template {} for profile {} does not exist".format(userdata_template, name) }
retcode = 400
return flask.jsonify(retmsg), retcode
scripts = list_script(None)
script_id = None
for scr in scripts:
@ -603,8 +704,8 @@ def create_profile(name, system_template, network_template, storage_template, sc
conn, cur = open_database(config)
try:
query = "INSERT INTO profile (name, system_template, network_template, storage_template, script, arguments) VALUES (%s, %s, %s, %s, %s, %s);"
args = (name, system_template_id, network_template_id, storage_template_id, script_id, arguments_formatted)
query = "INSERT INTO profile (name, system_template, network_template, storage_template, userdata_template, script, arguments) VALUES (%s, %s, %s, %s, %s, %s, %s);"
args = (name, system_template_id, network_template_id, storage_template_id, userdata_template_id, script_id, arguments_formatted)
cur.execute(query, args)
retmsg = { "name": name }
retcode = 200
@ -663,7 +764,7 @@ def run_os_command(command_string, background=False, environment=None, timeout=N
#
# Cloned VM provisioning function - executed by the Celery worker
#
def clone_vm(self, vm_name, vm_profile):
def clone_vm(self, vm_name, vm_profile, source_volumes):
pass
#

View File

@ -0,0 +1,202 @@
#!/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 sys
import uu
import distutils.util
import gevent.pywsgi
import provisioner_lib.provisioner as pvc_provisioner
import client_lib.common as pvc_common
import client_lib.vm as pvc_vm
import client_lib.network as pvc_network
# Parse the configuration file
try:
pvc_config_file = os.environ['PVC_CONFIG_FILE']
except:
print('Error: The "PVC_CONFIG_FILE" environment variable must be set before starting pvc-provisioner.')
exit(1)
print('Starting PVC Provisioner Metadata API daemon')
# Read in the config
try:
with open(pvc_config_file, 'r') as cfgfile:
o_config = yaml.load(cfgfile)
except Exception as e:
print('Failed to parse configuration file: {}'.format(e))
exit(1)
try:
# Create the config object
config = {
'debug': o_config['pvc']['debug'],
'coordinators': o_config['pvc']['coordinators'],
'listen_address': o_config['pvc']['provisioner']['listen_address'],
'listen_port': int(o_config['pvc']['provisioner']['listen_port']),
'auth_enabled': o_config['pvc']['provisioner']['authentication']['enabled'],
'auth_secret_key': o_config['pvc']['provisioner']['authentication']['secret_key'],
'auth_tokens': o_config['pvc']['provisioner']['authentication']['tokens'],
'ssl_enabled': o_config['pvc']['provisioner']['ssl']['enabled'],
'ssl_key_file': o_config['pvc']['provisioner']['ssl']['key_file'],
'ssl_cert_file': o_config['pvc']['provisioner']['ssl']['cert_file'],
'database_host': o_config['pvc']['provisioner']['database']['host'],
'database_port': int(o_config['pvc']['provisioner']['database']['port']),
'database_name': o_config['pvc']['provisioner']['database']['name'],
'database_user': o_config['pvc']['provisioner']['database']['user'],
'database_password': o_config['pvc']['provisioner']['database']['pass'],
'queue_host': o_config['pvc']['provisioner']['queue']['host'],
'queue_port': o_config['pvc']['provisioner']['queue']['port'],
'queue_path': o_config['pvc']['provisioner']['queue']['path'],
'storage_hosts': o_config['pvc']['cluster']['storage_hosts'],
'storage_domain': o_config['pvc']['cluster']['storage_domain'],
'ceph_monitor_port': o_config['pvc']['cluster']['ceph_monitor_port'],
'ceph_storage_secret_uuid': o_config['pvc']['cluster']['ceph_storage_secret_uuid']
}
if not config['storage_hosts']:
config['storage_hosts'] = config['coordinators']
# Set the config object in the pvcapi namespace
pvc_provisioner.config = config
except Exception as e:
print('{}'.format(e))
exit(1)
# Get our listening address from the CLI
router_address = sys.argv[1]
# Try to connect to the database or fail
try:
print('Verifying connectivity to database')
conn, cur = pvc_provisioner.open_database(config)
pvc_provisioner.close_database(conn, cur)
except Exception as e:
print('{}'.format(e))
exit(1)
api = flask.Flask(__name__)
if config['debug']:
api.config['DEBUG'] = True
if config['auth_enabled']:
api.config["SECRET_KEY"] = config['auth_secret_key']
print(api.name)
def get_vm_details(source_address):
# Start connection to Zookeeper
zk_conn = pvc_common.startZKConnection(config['coordinators'])
_discard, networks = pvc_network.get_list(zk_conn, None)
# Figure out which server this is via the DHCP address
host_information = dict()
networks_managed = (x for x in networks if x['type'] == 'managed')
for network in networks_managed:
network_leases = pvc_network.getNetworkDHCPLeases(zk_conn, network['vni'])
for network_lease in network_leases:
information = pvc_network.getDHCPLeaseInformation(zk_conn, network['vni'], network_lease)
try:
if information['ip4_address'] == source_address:
host_information = information
except:
pass
# Get our real information on the host; now we can start querying about it
client_hostname = host_information['hostname']
client_macaddr = host_information['mac_address']
client_ipaddr = host_information['ip4_address']
# Find the VM with that MAC address - we can't assume that the hostname is actually right
_discard, vm_list = pvc_vm.get_list(zk_conn, None, None, None)
vm_name = None
vm_details = dict()
for vm in vm_list:
try:
for network in vm['networks']:
if network['mac'] == client_macaddr:
vm_name = vm['name']
vm_details = vm
except:
pass
# Stop connection to Zookeeper
pvc_common.stopZKConnection(zk_conn)
return vm_details
@api.route('/', methods=['GET'])
def api_root():
return flask.jsonify({"message": "PVC Provisioner Metadata API version 1"}), 209
@api.route('/<version>/meta-data/', methods=['GET'])
def api_metadata_root(version):
metadata = """instance-id"""
return metadata, 200
@api.route('/<version>/meta-data/instance-id', methods=['GET'])
def api_metadata_instanceid(version):
# router_address = flask.request.__dict__['environ']['SERVER_NAME']
source_address = flask.request.__dict__['environ']['REMOTE_ADDR']
vm_details = get_vm_details(source_address)
instance_id = vm_details['uuid']
return instance_id, 200
@api.route('/<version>/user-data', methods=['GET'])
def api_userdata(version):
source_address = flask.request.__dict__['environ']['REMOTE_ADDR']
vm_details = get_vm_details(source_address)
vm_profile = vm_details['profile']
print("Profile: {}".format(vm_profile))
# Get profile details
profile_details = pvc_provisioner.list_profile(vm_profile, is_fuzzy=False)[0]
# Get the userdata
userdata = pvc_provisioner.list_template_userdata(profile_details['userdata_template'])[0]['userdata']
print(userdata)
return flask.Response(userdata, mimetype='text/cloud-config')
#
# Entrypoint
#
if __name__ == '__main__':
# Start main API
if config['debug']:
# Run in Flask standard mode
api.run('169.254.169.254', 80)
else:
# Run the PYWSGI serve
http_server = gevent.pywsgi.WSGIServer(
('10.200.0.1', 80),
api
)
print('Starting PyWSGI server at {}:{}'.format('169.254.169.254', 80))
http_server.serve_forever()

View File

@ -573,7 +573,6 @@ def api_template_network_net_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():
@ -793,10 +792,127 @@ def api_template_storage_disk_element(template, disk_id):
if flask.request.method == 'DELETE':
return pvcprovisioner.delete_template_storage_element(template, disk_id)
@api.route('/api/v1/template/userdata', methods=['GET', 'POST', 'PUT'])
@authenticator
def api_template_userdata_root():
"""
/template/userdata - Manage userdata provisioning templates for VM creation.
GET: List all userdata 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 userdata template.
?name: The name of the template.
* type: text
* optional: false
* requires: N/A
?data: The raw text of the cloud-init user-data.
* type: text (freeform)
* optional: false
* requires: N/A
PUT: Update existing userdata template.
?name: The name of the template.
* type: text
* optional: false
* requires: N/A
?data: The raw text of the cloud-init user-data.
* 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_template_userdata(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 userdata data
if 'data' in flask.request.values:
data = flask.request.values['data']
else:
return flask.jsonify({"message": "A userdata object must be specified."}), 400
return pvcprovisioner.create_template_userdata(name, data)
if flask.request.method == 'PUT':
# 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 userdata data
if 'data' in flask.request.values:
data = flask.request.values['data']
else:
return flask.jsonify({"message": "A userdata object must be specified."}), 400
return pvcprovisioner.update_template_userdata(name, data)
@api.route('/api/v1/template/userdata/<template>', methods=['GET', 'POST','PUT', 'DELETE'])
@authenticator
def api_template_userdata_element(template):
"""
/template/userdata/<template> - Manage userdata provisioning template <template>.
GET: Show details of userdata template.
POST: Add new userdata template.
?data: The raw text of the cloud-init user-data.
* type: text (freeform)
* optional: false
* requires: N/A
PUT: Modify existing userdata template.
?data: The raw text of the cloud-init user-data.
* type: text (freeform)
* optional: false
* requires: N/A
DELETE: Remove userdata template.
"""
if flask.request.method == 'GET':
return flask.jsonify(pvcprovisioner.list_template_userdata(template, is_fuzzy=False)), 200
if flask.request.method == 'POST':
# Get userdata data
if 'data' in flask.request.values:
data = flask.request.values['data']
else:
return flask.jsonify({"message": "A userdata object must be specified."}), 400
return pvcprovisioner.create_template_userdata(template, data)
if flask.request.method == 'PUT':
# Get userdata data
if 'data' in flask.request.values:
data = flask.request.values['data']
else:
return flask.jsonify({"message": "A userdata object must be specified."}), 400
return pvcprovisioner.update_template_userdata(template, data)
if flask.request.method == 'DELETE':
return pvcprovisioner.delete_template_userdata(template)
#
# Script endpoints
#
@api.route('/api/v1/script', methods=['GET', 'POST'])
@api.route('/api/v1/script', methods=['GET', 'POST', 'PUT'])
@authenticator
def api_script_root():
"""
@ -817,6 +933,15 @@ def api_script_root():
* type: text (freeform)
* optional: false
* requires: N/A
PUT: Modify existing 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
@ -842,8 +967,23 @@ def api_script_root():
return pvcprovisioner.create_script(name, data)
if flask.request.method == 'PUT':
# 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
@api.route('/api/v1/script/<script>', methods=['GET', 'POST', 'DELETE'])
# 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.update_script(name, data)
@api.route('/api/v1/script/<script>', methods=['GET', 'POST', 'PUT', 'DELETE'])
@authenticator
def api_script_element(script):
"""
@ -857,6 +997,12 @@ def api_script_element(script):
* optional: false
* requires: N/A
PUT: Modify existing 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':
@ -871,6 +1017,15 @@ def api_script_element(script):
return pvcprovisioner.create_script(script, data)
if flask.request.method == 'PUT':
# 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.update_script(script, data)
if flask.request.method == 'DELETE':
return pvcprovisioner.delete_script(script)
@ -902,7 +1057,11 @@ def api_profile_root():
* type: text
* optional: false
* requires: N/A
?storage_template: The name of the disk template.
?storage_template: The name of the storage template.
* type: text
* optional: false
* requires: N/A
?userdata_template: The name of the userdata template.
* type: text
* optional: false
* requires: N/A
@ -947,7 +1106,13 @@ def api_profile_root():
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
return flask.jsonify({"message": "A storage template must be specified."}), 400
# Get userdata_template data
if 'userdata_template' in flask.request.values:
userdata_template = flask.request.values['userdata_template']
else:
return flask.jsonify({"message": "A userdata template must be specified."}), 400
# Get script data
if 'script' in flask.request.values:
@ -960,7 +1125,7 @@ def api_profile_root():
else:
arguments = None
return pvcprovisioner.create_profile(name, system_template, network_template, storage_template, script, arguments)
return pvcprovisioner.create_profile(name, system_template, network_template, storage_template, userdata_template, script, arguments)
@api.route('/api/v1/profile/<profile>', methods=['GET', 'POST', 'DELETE'])
@authenticator
@ -979,7 +1144,11 @@ def api_profile_element(profile):
* type: text
* optional: false
* requires: N/A
?storage_template: The name of the disk template.
?storage_template: The name of the storage template.
* type: text
* optional: false
* requires: N/A
?userdata_template: The name of the userdata template.
* type: text
* optional: false
* requires: N/A
@ -1010,7 +1179,13 @@ def api_profile_element(profile):
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
return flask.jsonify({"message": "A storage template must be specified."}), 400
# Get userdata_template data
if 'userdata_template' in flask.request.values:
userdata_template = flask.request.values['userdata_template']
else:
return flask.jsonify({"message": "A userdata template must be specified."}), 400
# Get script data
if 'script' in flask.request.values:
@ -1018,7 +1193,7 @@ def api_profile_element(profile):
else:
return flask.jsonify({"message": "A script must be specified."}), 400
return pvcprovisioner.create_profile(profile, system_template, network_template, storage_template, script)
return pvcprovisioner.create_profile(profile, system_template, network_template, storage_template, userdata_template, script)
if flask.request.method == 'DELETE':
return pvcprovisioner.delete_profile(profile)

View File

@ -5,8 +5,11 @@ create table network_template (id SERIAL PRIMARY KEY, name TEXT NOT NULL UNIQUE,
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), pool TEXT NOT NULL, disk_id TEXT NOT NULL, disk_size_gb INT NOT NULL, mountpoint TEXT, filesystem TEXT, filesystem_args TEXT);
create table userdata_template (id SERIAL PRIMARY KEY, name TEXT NOT NULL UNIQUE, userdata TEXT NOT NULL);
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), arguments text);
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), userdata_template INT REFERENCES userdata_template(id), script INT REFERENCES script(id), arguments text);
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;
insert into userdata_template(name, userdata) values ('empty', '');