Finish the provisioner and metadata server
This commit is contained in:
		| @@ -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 | ||||
|   | ||||
							
								
								
									
										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 | ||||
|  | ||||
| # 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 | ||||
|  | ||||
| # | ||||
|   | ||||
							
								
								
									
										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': | ||||
|         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', ''); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user