202 lines
7.5 KiB
Python
202 lines
7.5 KiB
Python
#!/usr/bin/env python3
|
|
|
|
# MetadataAPIInstance.py - Class implementing an EC2-compatible cloud-init Metadata server
|
|
# Part of the Parallel Virtual Cluster (PVC) system
|
|
#
|
|
# Copyright (C) 2018-2022 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, version 3.
|
|
#
|
|
# 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 gevent.pywsgi
|
|
import flask
|
|
import sys
|
|
import psycopg2
|
|
|
|
from threading import Thread
|
|
from psycopg2.extras import RealDictCursor
|
|
|
|
import daemon_lib.vm as pvc_vm
|
|
import daemon_lib.network as pvc_network
|
|
|
|
|
|
class MetadataAPIInstance(object):
|
|
mdapi = flask.Flask(__name__)
|
|
|
|
# Initialization function
|
|
def __init__(self, zkhandler, config, logger):
|
|
self.zkhandler = zkhandler
|
|
self.config = config
|
|
self.logger = logger
|
|
self.thread = None
|
|
self.md_http_server = None
|
|
self.add_routes()
|
|
|
|
# Add flask routes inside our instance
|
|
def add_routes(self):
|
|
@self.mdapi.route("/", methods=["GET"])
|
|
def api_root():
|
|
return (
|
|
flask.jsonify({"message": "PVC Provisioner Metadata API version 1"}),
|
|
209,
|
|
)
|
|
|
|
@self.mdapi.route("/<version>/meta-data/", methods=["GET"])
|
|
def api_metadata_root(version):
|
|
metadata = """instance-id\nname\nprofile"""
|
|
return metadata, 200
|
|
|
|
@self.mdapi.route("/<version>/meta-data/instance-id", methods=["GET"])
|
|
def api_metadata_instanceid(version):
|
|
source_address = flask.request.__dict__["environ"]["REMOTE_ADDR"]
|
|
vm_details = self.get_vm_details(source_address)
|
|
instance_id = vm_details.get("uuid", None)
|
|
return instance_id, 200
|
|
|
|
@self.mdapi.route("/<version>/meta-data/name", methods=["GET"])
|
|
def api_metadata_hostname(version):
|
|
source_address = flask.request.__dict__["environ"]["REMOTE_ADDR"]
|
|
vm_details = self.get_vm_details(source_address)
|
|
vm_name = vm_details.get("name", None)
|
|
return vm_name, 200
|
|
|
|
@self.mdapi.route("/<version>/meta-data/profile", methods=["GET"])
|
|
def api_metadata_profile(version):
|
|
source_address = flask.request.__dict__["environ"]["REMOTE_ADDR"]
|
|
vm_details = self.get_vm_details(source_address)
|
|
vm_profile = vm_details.get("profile", None)
|
|
return vm_profile, 200
|
|
|
|
@self.mdapi.route("/<version>/user-data", methods=["GET"])
|
|
def api_userdata(version):
|
|
source_address = flask.request.__dict__["environ"]["REMOTE_ADDR"]
|
|
vm_details = self.get_vm_details(source_address)
|
|
vm_profile = vm_details.get("profile", None)
|
|
# Get the userdata
|
|
if vm_profile:
|
|
userdata = self.get_profile_userdata(vm_profile)
|
|
self.logger.out(
|
|
"Returning userdata for profile {}".format(vm_profile),
|
|
state="i",
|
|
prefix="Metadata API",
|
|
)
|
|
else:
|
|
userdata = None
|
|
return flask.Response(userdata)
|
|
|
|
def launch_wsgi(self):
|
|
try:
|
|
self.md_http_server = gevent.pywsgi.WSGIServer(
|
|
("169.254.169.254", 80),
|
|
self.mdapi,
|
|
log=sys.stdout,
|
|
error_log=sys.stdout,
|
|
)
|
|
self.md_http_server.serve_forever()
|
|
except Exception as e:
|
|
self.logger.out("Error starting Metadata API: {}".format(e), state="e")
|
|
|
|
# WSGI start/stop
|
|
def start(self):
|
|
# Launch Metadata API
|
|
self.logger.out("Starting Metadata API at 169.254.169.254:80", state="i")
|
|
self.thread = Thread(target=self.launch_wsgi)
|
|
self.thread.start()
|
|
self.logger.out("Successfully started Metadata API thread", state="o")
|
|
|
|
def stop(self):
|
|
if not self.md_http_server:
|
|
return
|
|
|
|
self.logger.out("Stopping Metadata API at 169.254.169.254:80", state="i")
|
|
try:
|
|
self.md_http_server.stop()
|
|
self.md_http_server.close()
|
|
self.md_http_server = None
|
|
self.logger.out("Successfully stopped Metadata API", state="o")
|
|
except Exception as e:
|
|
self.logger.out("Error stopping Metadata API: {}".format(e), state="e")
|
|
|
|
# Helper functions
|
|
def open_database(self):
|
|
conn = psycopg2.connect(
|
|
host=self.config["metadata_postgresql_host"],
|
|
port=self.config["metadata_postgresql_port"],
|
|
dbname=self.config["metadata_postgresql_dbname"],
|
|
user=self.config["metadata_postgresql_user"],
|
|
password=self.config["metadata_postgresql_password"],
|
|
)
|
|
cur = conn.cursor(cursor_factory=RealDictCursor)
|
|
return conn, cur
|
|
|
|
def close_database(self, conn, cur):
|
|
cur.close()
|
|
conn.close()
|
|
|
|
# Obtain a list of templates
|
|
def get_profile_userdata(self, vm_profile):
|
|
query = """SELECT userdata.userdata FROM profile
|
|
JOIN userdata ON profile.userdata = userdata.id
|
|
WHERE profile.name = %s;
|
|
"""
|
|
args = (vm_profile,)
|
|
|
|
conn, cur = self.open_database()
|
|
cur.execute(query, args)
|
|
data_raw = cur.fetchone()
|
|
self.close_database(conn, cur)
|
|
if data_raw is not None:
|
|
data = data_raw.get("userdata", None)
|
|
return data
|
|
else:
|
|
return None
|
|
|
|
# VM details function
|
|
def get_vm_details(self, source_address):
|
|
# Start connection to Zookeeper
|
|
_discard, networks = pvc_network.get_list(self.zkhandler, None)
|
|
|
|
# Figure out which server this is via the DHCP address
|
|
host_information = dict()
|
|
networks_managed = (x for x in networks if x.get("type") == "managed")
|
|
for network in networks_managed:
|
|
network_leases = pvc_network.getNetworkDHCPLeases(
|
|
self.zkhandler, network.get("vni")
|
|
)
|
|
for network_lease in network_leases:
|
|
information = pvc_network.getDHCPLeaseInformation(
|
|
self.zkhandler, network.get("vni"), network_lease
|
|
)
|
|
try:
|
|
if information.get("ip4_address", None) == source_address:
|
|
host_information = information
|
|
except Exception:
|
|
pass
|
|
|
|
# Get our real information on the host; now we can start querying about it
|
|
client_macaddr = host_information.get("mac_address", None)
|
|
|
|
# Find the VM with that MAC address - we can't assume that the hostname is actually right
|
|
_discard, vm_list = pvc_vm.get_list(self.zkhandler, None, None, None, None)
|
|
vm_details = dict()
|
|
for vm in vm_list:
|
|
try:
|
|
for network in vm.get("networks"):
|
|
if network.get("mac", None) == client_macaddr:
|
|
vm_details = vm
|
|
except Exception:
|
|
pass
|
|
|
|
return vm_details
|