Waiting for the daemons to stop took too much time on some nodes and could throw off the lockstep. Instead, leverage background=True to run the systemctl os_commands in the background (when they complete is irrelevant), stop the Metadata API first, and don't delay during its stop at all.
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
|