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
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
||||
# 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
|
||||
|
||||
#
|
||||
|
|
|
@ -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':
|
||||
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)
|
||||
|
|
|
@ -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', '');
|
||||
|
|
Loading…
Reference in New Issue