Finish the provisioner and metadata server
This commit is contained in:
		| @@ -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 | ||||||
|   | |||||||
							
								
								
									
										23
									
								
								client-provisioner/examples/userdata.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								client-provisioner/examples/userdata.yaml
									
									
									
									
									
										Normal 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" | ||||||
| @@ -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 | ||||||
|  |  | ||||||
| # | # | ||||||
|   | |||||||
							
								
								
									
										202
									
								
								client-provisioner/pvc-metadata.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										202
									
								
								client-provisioner/pvc-metadata.py
									
									
									
									
									
										Executable 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() | ||||||
|  |      | ||||||
| @@ -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', ''); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user