diff --git a/client-provisioner/examples/multipart-userdata.yaml b/client-provisioner/examples/multipart-userdata.yaml new file mode 100644 index 00000000..3db3abee --- /dev/null +++ b/client-provisioner/examples/multipart-userdata.yaml @@ -0,0 +1,16 @@ +Content-Type: multipart/mixed; boundary="==BOUNDARY==" +MIME-Version: 1.0 + +--==BOUNDARY== +Content-Type: text/cloud-config; charset="us-ascii" + +users: + - blah + +--==BOUNDARY== +Content-Type: text/x-shellscript; charset="us-ascii" + +#!/bin/bash +echo "koz is koz" >> /etc/motd + +--==BOUNDARY==-- diff --git a/client-provisioner/examples/userdata.yaml b/client-provisioner/examples/userdata.yaml index 9215ff8f..faf1276d 100644 --- a/client-provisioner/examples/userdata.yaml +++ b/client-provisioner/examples/userdata.yaml @@ -1,3 +1,6 @@ +Content-Type: text/cloud-config; charset="us-ascii" +MIME-Version: 1.0 + #cloud-config # Example user-data file to set up an alternate /var/home, a first user and some SSH keys, and some packages bootcmd: @@ -16,8 +19,9 @@ users: lock_passwd: true ssh_authorized_keys: - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBRBGPzlbh5xYD6k8DMZdPNEwemZzKSSpWGOuU72ehfN joshua@bonifacelabs.net 2017-04 -usercmd: +runcmd: + - "userdel debian" - "groupmod -g 200 deploy" - "usermod -u 200 deploy" - - "userdel debian" - "systemctl disable cloud-init.target" + - "reboot" diff --git a/client-provisioner/pvc-metadata.py b/client-provisioner/pvc-metadata.py deleted file mode 100755 index 8da74aac..00000000 --- a/client-provisioner/pvc-metadata.py +++ /dev/null @@ -1,202 +0,0 @@ -#!/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 -# -# 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 . -# -############################################################################### - -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('//meta-data/', methods=['GET']) -def api_metadata_root(version): - metadata = """instance-id""" - return metadata, 200 - -@api.route('//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('//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() - diff --git a/client-provisioner/pvc-provisioner.py b/client-provisioner/pvc-provisioner.py index 858fe876..ecb0cffe 100755 --- a/client-provisioner/pvc-provisioner.py +++ b/client-provisioner/pvc-provisioner.py @@ -26,12 +26,17 @@ import yaml import os import uu import distutils.util - +import threading +import time import gevent.pywsgi import celery as Celery -import provisioner_lib.provisioner as pvcprovisioner +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: @@ -81,7 +86,7 @@ try: config['storage_hosts'] = config['coordinators'] # Set the config object in the pvcapi namespace - pvcprovisioner.config = config + pvc_provisioner.config = config except Exception as e: print('{}'.format(e)) exit(1) @@ -89,32 +94,39 @@ except Exception as e: # Try to connect to the database or fail try: print('Verifying connectivity to database') - conn, cur = pvcprovisioner.open_database(config) - pvcprovisioner.close_database(conn, cur) + 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__) -api.config['CELERY_BROKER_URL'] = 'redis://{}:{}{}'.format(config['queue_host'], config['queue_port'], config['queue_path']) -api.config['CELERY_RESULT_BACKEND'] = 'redis://{}:{}{}'.format(config['queue_host'], config['queue_port'], config['queue_path']) +# Primary provisioning API +prapi = flask.Flask(__name__) +prapi.config['CELERY_BROKER_URL'] = 'redis://{}:{}{}'.format(config['queue_host'], config['queue_port'], config['queue_path']) +prapi.config['CELERY_RESULT_BACKEND'] = 'redis://{}:{}{}'.format(config['queue_host'], config['queue_port'], config['queue_path']) if config['debug']: - api.config['DEBUG'] = True + prapi.config['DEBUG'] = True if config['auth_enabled']: - api.config["SECRET_KEY"] = config['auth_secret_key'] + prapi.config["SECRET_KEY"] = config['auth_secret_key'] -print(api.name) -celery = Celery.Celery(api.name, broker=api.config['CELERY_BROKER_URL']) -celery.conf.update(api.config) +celery = Celery.Celery(prapi.name, broker=prapi.config['CELERY_BROKER_URL']) +celery.conf.update(prapi.config) + +# Metadata API +mdapi = flask.Flask(__name__) + +if config['debug']: + mdapi.config['DEBUG'] = True # # Job functions # + @celery.task(bind=True) def create_vm(self, vm_name, profile_name): - return pvcprovisioner.create_vm(self, vm_name, profile_name) + return pvc_provisioner.create_vm(self, vm_name, profile_name) # Authentication decorator function def authenticator(function): @@ -140,11 +152,15 @@ def authenticator(function): authenticate.__name__ = function.__name__ return authenticate -@api.route('/api/v1', methods=['GET']) +# +# Provisioning API +# + +@prapi.route('/api/v1', methods=['GET']) def api_root(): return flask.jsonify({"message": "PVC Provisioner API version 1"}), 209 -@api.route('/api/v1/auth/login', methods=['GET', 'POST']) +@prapi.route('/api/v1/auth/login', methods=['GET', 'POST']) def api_auth_login(): # Just return a 200 if auth is disabled if not config['auth_enabled']: @@ -168,7 +184,7 @@ def api_auth_login(): else: return flask.jsonify({"message": "Authentication failed"}), 401 -@api.route('/api/v1/auth/logout', methods=['GET', 'POST']) +@prapi.route('/api/v1/auth/logout', methods=['GET', 'POST']) def api_auth_logout(): # Just return a 200 if auth is disabled if not config['auth_enabled']: @@ -181,7 +197,7 @@ def api_auth_logout(): # # Template endpoints # -@api.route('/api/v1/template', methods=['GET']) +@prapi.route('/api/v1/template', methods=['GET']) @authenticator def api_template_root(): """ @@ -196,9 +212,9 @@ def api_template_root(): else: limit = None - return flask.jsonify(pvcprovisioner.template_list(limit)), 200 + return flask.jsonify(pvc_provisioner.template_list(limit)), 200 -@api.route('/api/v1/template/system', methods=['GET', 'POST']) +@prapi.route('/api/v1/template/system', methods=['GET', 'POST']) @authenticator def api_template_system_root(): """ @@ -257,7 +273,7 @@ def api_template_system_root(): else: limit = None - return flask.jsonify(pvcprovisioner.list_template_system(limit)), 200 + return flask.jsonify(pvc_provisioner.list_template_system(limit)), 200 if flask.request.method == 'POST': # Get name data @@ -318,9 +334,9 @@ def api_template_system_root(): else: start_with_node = False - return pvcprovisioner.create_template_system(name, vcpu_count, vram_mb, serial, vnc, vnc_bind, node_limit, node_selector, start_with_node) + return pvc_provisioner.create_template_system(name, vcpu_count, vram_mb, serial, vnc, vnc_bind, node_limit, node_selector, start_with_node) -@api.route('/api/v1/template/system/