Finish the provisioner and metadata server
This commit is contained in:
parent
88924497c2
commit
708de48065
|
@ -113,31 +113,64 @@ def install(**kwargs):
|
||||||
|
|
||||||
# Append the fstab line
|
# Append the fstab line
|
||||||
with open(fstab_file, 'a') as fh:
|
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'],
|
disk=disk['disk_id'],
|
||||||
mountpoint=disk['mountpoint'],
|
mountpoint=disk['mountpoint'],
|
||||||
filesystem=disk['filesystem'],
|
filesystem=disk['filesystem'],
|
||||||
options=options,
|
options=options,
|
||||||
dump=dump,
|
dump=dump,
|
||||||
cpass=cpass
|
cpass=cpass
|
||||||
))
|
)
|
||||||
|
fh.write(data)
|
||||||
|
|
||||||
# Write the hostname
|
# Write the hostname
|
||||||
hostname_file = "{}/etc/hostname".format(temporary_directory)
|
hostname_file = "{}/etc/hostname".format(temporary_directory)
|
||||||
with open(hostname_file, 'w') as fh:
|
with open(hostname_file, 'w') as fh:
|
||||||
fh.write("{}".format(vm_name))
|
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
|
# 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".
|
# 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)
|
ens2_network_file = "{}/etc/network/interfaces.d/ens2".format(temporary_directory)
|
||||||
with open(ens2_network_file, 'w') as fh:
|
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
|
# Write the GRUB configuration
|
||||||
grubcfg_file = "{}/etc/default/grub".format(temporary_directory)
|
grubcfg_file = "{}/etc/default/grub".format(temporary_directory)
|
||||||
with open(grubcfg_file, 'w') as fh:
|
with open(grubcfg_file, 'w') as fh:
|
||||||
fh.write("""# Written by the PVC provisioner
|
data = """# Written by the PVC provisioner
|
||||||
GRUB_DEFAULT=0
|
GRUB_DEFAULT=0
|
||||||
GRUB_TIMEOUT=1
|
GRUB_TIMEOUT=1
|
||||||
GRUB_DISTRIBUTOR="PVC Virtual Machine"
|
GRUB_DISTRIBUTOR="PVC Virtual Machine"
|
||||||
|
@ -146,25 +179,39 @@ GRUB_CMDLINE_LINUX=""
|
||||||
GRUB_TERMINAL=console
|
GRUB_TERMINAL=console
|
||||||
GRUB_SERIAL_COMMAND="serial --speed=115200 --unit=0 --word=8 --parity=no --stop=1"
|
GRUB_SERIAL_COMMAND="serial --speed=115200 --unit=0 --word=8 --parity=no --stop=1"
|
||||||
GRUB_DISABLE_LINUX_UUID=false
|
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
|
# 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.
|
# WILL FAIL IN UNEXPECTED WAYS! Keep this in mind when using chroot in your scripts.
|
||||||
real_root = os.open("/", os.O_RDONLY)
|
real_root = os.open("/", os.O_RDONLY)
|
||||||
os.chroot(temporary_directory)
|
os.chroot(temporary_directory)
|
||||||
fake_root = os.open("/", os.O_RDONLY)
|
fake_root = os.open("/", os.O_RDONLY)
|
||||||
os.fchdir(fake_root)
|
os.fchdir(fake_root)
|
||||||
|
|
||||||
|
# Install and update GRUB
|
||||||
os.system(
|
os.system(
|
||||||
"grub-install --force /dev/rbd/{}/{}_{}".format(root_disk['pool'], vm_name, root_disk['disk_id'])
|
"grub-install --force /dev/rbd/{}/{}_{}".format(root_disk['pool'], vm_name, root_disk['disk_id'])
|
||||||
)
|
)
|
||||||
os.system(
|
os.system(
|
||||||
"update-grub"
|
"update-grub"
|
||||||
)
|
)
|
||||||
|
# Set a really dumb root password [TEMPORARY]
|
||||||
os.system(
|
os.system(
|
||||||
"echo root:test123 | chpasswd"
|
"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.fchdir(real_root)
|
||||||
os.chroot(".")
|
os.chroot(".")
|
||||||
os.fchdir(real_root)
|
os.fchdir(real_root)
|
||||||
|
@ -182,4 +229,4 @@ GRUB_DISABLE_LINUX_UUID=false
|
||||||
del fake_root
|
del fake_root
|
||||||
del real_root
|
del real_root
|
||||||
|
|
||||||
# Everything else is done via cloud-init
|
# Everything else is done via cloud-init user-data
|
||||||
|
|
|
@ -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"
|
|
@ -1,6 +1,6 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
# pvcapi.py - PVC HTTP API functions
|
# provisioner.py - PVC Provisioner functions
|
||||||
# Part of the Parallel Virtual Cluster (PVC) system
|
# Part of the Parallel Virtual Cluster (PVC) system
|
||||||
#
|
#
|
||||||
# Copyright (C) 2018-2019 Joshua M. Boniface <joshua@boniface.me>
|
# Copyright (C) 2018-2019 Joshua M. Boniface <joshua@boniface.me>
|
||||||
|
@ -165,12 +165,20 @@ def list_template_storage_disks(name):
|
||||||
disks = data['disks']
|
disks = data['disks']
|
||||||
return 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):
|
def template_list(limit):
|
||||||
system_templates = list_template_system(limit)
|
system_templates = list_template_system(limit)
|
||||||
network_templates = list_template_network(limit)
|
network_templates = list_template_network(limit)
|
||||||
storage_templates = list_template_storage(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
|
# Template Create functions
|
||||||
|
@ -304,6 +312,49 @@ def create_template_storage_element(name, pool, disk_id, disk_size_gb, filesyste
|
||||||
close_database(conn, cur)
|
close_database(conn, cur)
|
||||||
return flask.jsonify(retmsg), retcode
|
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
|
# Template Delete functions
|
||||||
#
|
#
|
||||||
|
@ -444,6 +495,25 @@ def delete_template_storage_element(name, disk_id):
|
||||||
close_database(conn, cur)
|
close_database(conn, cur)
|
||||||
return flask.jsonify(retmsg), retcode
|
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
|
# Script functions
|
||||||
#
|
#
|
||||||
|
@ -491,6 +561,27 @@ def create_script(name, script):
|
||||||
close_database(conn, cur)
|
close_database(conn, cur)
|
||||||
return flask.jsonify(retmsg), retcode
|
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):
|
def delete_script(name):
|
||||||
if not list_script(name, is_fuzzy=False):
|
if not list_script(name, is_fuzzy=False):
|
||||||
retmsg = { "message": "The script {} does not exist".format(name) }
|
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 = dict()
|
||||||
profile_data['name'] = profile['name']
|
profile_data['name'] = profile['name']
|
||||||
# Parse the name of each subelement
|
# 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)
|
query = 'SELECT name from {} WHERE id = %s'.format(etype)
|
||||||
args = (profile[etype],)
|
args = (profile[etype],)
|
||||||
cur.execute(query, args)
|
cur.execute(query, args)
|
||||||
|
@ -553,7 +644,7 @@ def list_profile(limit, is_fuzzy=True):
|
||||||
close_database(conn, cur)
|
close_database(conn, cur)
|
||||||
return data
|
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):
|
if list_profile(name, is_fuzzy=False):
|
||||||
retmsg = { "message": "The profile {} already exists".format(name) }
|
retmsg = { "message": "The profile {} already exists".format(name) }
|
||||||
retcode = 400
|
retcode = 400
|
||||||
|
@ -589,6 +680,16 @@ def create_profile(name, system_template, network_template, storage_template, sc
|
||||||
retcode = 400
|
retcode = 400
|
||||||
return flask.jsonify(retmsg), retcode
|
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)
|
scripts = list_script(None)
|
||||||
script_id = None
|
script_id = None
|
||||||
for scr in scripts:
|
for scr in scripts:
|
||||||
|
@ -603,8 +704,8 @@ def create_profile(name, system_template, network_template, storage_template, sc
|
||||||
|
|
||||||
conn, cur = open_database(config)
|
conn, cur = open_database(config)
|
||||||
try:
|
try:
|
||||||
query = "INSERT INTO profile (name, system_template, network_template, storage_template, script, arguments) VALUES (%s, %s, %s, %s, %s, %s);"
|
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, script_id, arguments_formatted)
|
args = (name, system_template_id, network_template_id, storage_template_id, userdata_template_id, script_id, arguments_formatted)
|
||||||
cur.execute(query, args)
|
cur.execute(query, args)
|
||||||
retmsg = { "name": name }
|
retmsg = { "name": name }
|
||||||
retcode = 200
|
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
|
# 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
|
pass
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -573,7 +573,6 @@ def api_template_network_net_element(template, vni):
|
||||||
if flask.request.method == 'DELETE':
|
if flask.request.method == 'DELETE':
|
||||||
return pvcprovisioner.delete_template_network_element(template, vni)
|
return pvcprovisioner.delete_template_network_element(template, vni)
|
||||||
|
|
||||||
|
|
||||||
@api.route('/api/v1/template/storage', methods=['GET', 'POST'])
|
@api.route('/api/v1/template/storage', methods=['GET', 'POST'])
|
||||||
@authenticator
|
@authenticator
|
||||||
def api_template_storage_root():
|
def api_template_storage_root():
|
||||||
|
@ -793,10 +792,127 @@ def api_template_storage_disk_element(template, disk_id):
|
||||||
if flask.request.method == 'DELETE':
|
if flask.request.method == 'DELETE':
|
||||||
return pvcprovisioner.delete_template_storage_element(template, disk_id)
|
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
|
# Script endpoints
|
||||||
#
|
#
|
||||||
@api.route('/api/v1/script', methods=['GET', 'POST'])
|
@api.route('/api/v1/script', methods=['GET', 'POST', 'PUT'])
|
||||||
@authenticator
|
@authenticator
|
||||||
def api_script_root():
|
def api_script_root():
|
||||||
"""
|
"""
|
||||||
|
@ -817,6 +933,15 @@ def api_script_root():
|
||||||
* type: text (freeform)
|
* type: text (freeform)
|
||||||
* optional: false
|
* optional: false
|
||||||
* requires: N/A
|
* 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':
|
if flask.request.method == 'GET':
|
||||||
# Get name limit
|
# Get name limit
|
||||||
|
@ -842,8 +967,23 @@ def api_script_root():
|
||||||
|
|
||||||
return pvcprovisioner.create_script(name, data)
|
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
|
@authenticator
|
||||||
def api_script_element(script):
|
def api_script_element(script):
|
||||||
"""
|
"""
|
||||||
|
@ -857,6 +997,12 @@ def api_script_element(script):
|
||||||
* optional: false
|
* optional: false
|
||||||
* requires: N/A
|
* 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.
|
DELETE: Remove provisioning script.
|
||||||
"""
|
"""
|
||||||
if flask.request.method == 'GET':
|
if flask.request.method == 'GET':
|
||||||
|
@ -871,6 +1017,15 @@ def api_script_element(script):
|
||||||
|
|
||||||
return pvcprovisioner.create_script(script, data)
|
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':
|
if flask.request.method == 'DELETE':
|
||||||
return pvcprovisioner.delete_script(script)
|
return pvcprovisioner.delete_script(script)
|
||||||
|
|
||||||
|
@ -902,7 +1057,11 @@ def api_profile_root():
|
||||||
* type: text
|
* type: text
|
||||||
* optional: false
|
* optional: false
|
||||||
* requires: N/A
|
* 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
|
* type: text
|
||||||
* optional: false
|
* optional: false
|
||||||
* requires: N/A
|
* requires: N/A
|
||||||
|
@ -947,7 +1106,13 @@ def api_profile_root():
|
||||||
if 'storage_template' in flask.request.values:
|
if 'storage_template' in flask.request.values:
|
||||||
storage_template = flask.request.values['storage_template']
|
storage_template = flask.request.values['storage_template']
|
||||||
else:
|
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
|
# Get script data
|
||||||
if 'script' in flask.request.values:
|
if 'script' in flask.request.values:
|
||||||
|
@ -960,7 +1125,7 @@ def api_profile_root():
|
||||||
else:
|
else:
|
||||||
arguments = None
|
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'])
|
@api.route('/api/v1/profile/<profile>', methods=['GET', 'POST', 'DELETE'])
|
||||||
@authenticator
|
@authenticator
|
||||||
|
@ -979,7 +1144,11 @@ def api_profile_element(profile):
|
||||||
* type: text
|
* type: text
|
||||||
* optional: false
|
* optional: false
|
||||||
* requires: N/A
|
* 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
|
* type: text
|
||||||
* optional: false
|
* optional: false
|
||||||
* requires: N/A
|
* requires: N/A
|
||||||
|
@ -1010,7 +1179,13 @@ def api_profile_element(profile):
|
||||||
if 'storage_template' in flask.request.values:
|
if 'storage_template' in flask.request.values:
|
||||||
storage_template = flask.request.values['storage_template']
|
storage_template = flask.request.values['storage_template']
|
||||||
else:
|
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
|
# Get script data
|
||||||
if 'script' in flask.request.values:
|
if 'script' in flask.request.values:
|
||||||
|
@ -1018,7 +1193,7 @@ def api_profile_element(profile):
|
||||||
else:
|
else:
|
||||||
return flask.jsonify({"message": "A script must be specified."}), 400
|
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':
|
if flask.request.method == 'DELETE':
|
||||||
return pvcprovisioner.delete_profile(profile)
|
return pvcprovisioner.delete_profile(profile)
|
||||||
|
|
|
@ -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 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_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 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 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 database pvcprov to pvcprov;
|
||||||
grant all privileges on all tables in schema public to pvcprov;
|
grant all privileges on all tables in schema public to pvcprov;
|
||||||
grant all privileges on all sequences in schema public to pvcprov;
|
grant all privileges on all sequences in schema public to pvcprov;
|
||||||
|
|
||||||
|
insert into userdata_template(name, userdata) values ('empty', '');
|
||||||
|
|
Loading…
Reference in New Issue