From 95038a060cb6b5ece8e5ab68c2594fe7ad94c35e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20=C5=A0urda?= Date: Thu, 18 Nov 2021 19:30:04 +0800 Subject: [PATCH] Add v2 API with UUID support - refactoring - code quality --- main.py | 255 ++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 183 insertions(+), 72 deletions(-) diff --git a/main.py b/main.py index 929b0a1..9e65697 100644 --- a/main.py +++ b/main.py @@ -8,6 +8,7 @@ import os import socket import sys from ipaddress import AddressValueError, IPv4Address, IPv6Address +from os import access, R_OK import cherrypy # from cherrypy.lib.static import serve_file @@ -24,17 +25,27 @@ META_DATA_FILENAME = CONFIG["app"].get("meta_data", "meta-data") REDIRECT_FILENAME = CONFIG["app"].get("redirect", "redirect") -class CloudInitApp: +class CloudInitRequest: """ - Serve cloud init files + Request data for persistence across methods """ - - def __init__(self): + def __init__(self, request, uuid=None): self.remoteip = None self.hostinfo = ('localhost', ) + self.request = request + self.uuid = uuid + self.meta_data = None + self.meta_data_loaded = False + self.user_data = None + self._init_ip() + self._generate_default_meta_data() def _can_ip_be_proxy(self): - self.remoteip = cherrypy.request.remote.ip + """ + Assuming the connection is through a proxy, is this proxy permitted? + Can't proxy from a publicly reachable IP. + """ + self.remoteip = self.request.remote.ip try: ipobj = IPv4Address(self.remoteip) except AddressValueError: @@ -50,9 +61,9 @@ class CloudInitApp: """ if self._can_ip_be_proxy(): try: - self.remoteip = cherrypy.request.headers.get( + self.remoteip = self.request.headers.get( 'X-Real-Ip', - cherrypy.request.remote.ip + self.request.remote.ip ) except KeyError: pass @@ -67,115 +78,211 @@ class CloudInitApp: except socket.gaierror: self.hostinfo = (self.remoteip, ) - def _redirect_if_needed(self): - filepath = os.path.join(PATH, "data", self.hostinfo[0], - REDIRECT_FILENAME) - if os.path.exists(filepath): - try: - with open(filepath) as redirect: - content = redirect.read().splitlines() - raise cherrypy.HTTPRedirect(content[0], 301) - except IOError: - return False - return False - - def _generate_metadata(self): - self._init_ip() + def _generate_default_meta_data(self): + """ + Build default meta-data based on the requesting IP + """ hostname = self.hostinfo[0] - base = cherrypy.request.base - data = { + base = self.request.base + self.meta_data = { "instance-id": hostname.split(".")[0], "local-hostname": hostname, "baseurl": base } - return data + + def _update_meta_data_from_file(self, filename): + """ + Load meta-data from a file + """ + with open(filename, "r") as metadata: + self.meta_data.update(yaml.safe_load(metadata)) + + def _get_filename(self, filetype): + """ + Get the best available filename for a given type + """ + check_dirs = ['localhost', ] + if self.uuid: + check_dirs.insert(0, self.uuid) + if self.meta_data['local-hostname'] != 'localhost': + check_dirs.insert(0, self.meta_data['local-hostname']) + for subdir in check_dirs: + filepath = os.path.join(PATH, "data", + subdir, + filetype) + if not os.path.isfile(filepath) \ + or not access(filepath, R_OK): + continue + return filepath + return None + + def get_meta_data(self): + """ + Return the appropriate meta-data for this request + """ + if self.meta_data_loaded: + return self.meta_data + self._generate_default_meta_data() + filename = self._get_filename(META_DATA_FILENAME) + if filename: + self._update_meta_data_from_file( + filename + ) + self.meta_data_loaded = True + return self.meta_data + + def save_meta_data(self, data): + """ + Save metadata + """ + filename = self._get_filename(META_DATA_FILENAME) + with open(filename, "wt") as filehandle: + filehandle.write(data) + + def get_user_data(self): + """ + Return the appropriate meta-data for this request + """ + if self.user_data: + return self.user_data + filename = self._get_filename(USER_DATA_FILENAME) + if not filename: + return None + with open(filename, "r") as userdata: + self.user_data = userdata.read() + return self.user_data + return None + + def optional_redirect(self): + """ + Return optional redirect, or None + """ + filename = self._get_filename(REDIRECT_FILENAME) + if not (filename and os.path.exists(filename)): + return None + try: + with open(filename) as redirect: + content = redirect.read().splitlines() + return content[0] + except IOError: + return None + return None + + +class CloudInitApp: + """ + Serve cloud init files + """ + + @staticmethod + def _redirect_if_needed(request): + redirect = request.optional_redirect() + if redirect: + raise cherrypy.HTTPRedirect(redirect, 301) + return False @staticmethod def _content_type(data): + if not data: + return "text/cloud-config" if data.startswith("#include"): return "text/x-include-url" - elif data.startswith("## template: jinja"): + if data.startswith("## template: jinja"): return "text/jinja2" - else: - return "text/cloud-config" + return "text/cloud-config" @staticmethod def _wrap_metadata(metadata): return {'ds': { 'meta_data': metadata - } - } + } + } - @cherrypy.expose - def user_data(self): + def _user_data(self, uuid=None): """ Serves a static file + But can process a template if x-include-url """ - self._init_ip() - self._redirect_if_needed() - filepath = os.path.join(PATH, "data", self.hostinfo[0], - USER_DATA_FILENAME) - if not os.path.exists(filepath): - filepath = os.path.join(PATH, "data", USER_DATA_FILENAME) - with open(filepath, "r") as userdata: - data = userdata.read() - - c = CloudInitApp._content_type(data) + request = CloudInitRequest(cherrypy.request, uuid) + self._redirect_if_needed(request) + user_data = request.get_user_data() - cherrypy.response.headers['Content-Type'] = c + c__ = self._content_type(user_data) + + cherrypy.response.headers['Content-Type'] = c__ cherrypy.response.headers['Content-Disposition'] = \ 'attachment; filename="user-data"' - if c == "text/x-include-url": - t = Template(data) - metadata = self._generate_metadata() + if c__ == "text/x-include-url": + t__ = Template(user_data) + metadata = request.get_meta_data() wrapped = CloudInitApp._wrap_metadata(metadata) - return t.render(**wrapped) - else: - return data + return t__.render(**wrapped) + return user_data - @cherrypy.expose - def meta_data(self): + def _meta_data(self, uuid=None): """ Return meta-data in YAML """ - self._init_ip() - self._redirect_if_needed() - - data = self._generate_metadata() - - filepath = os.path.join(PATH, "data", data['local-hostname'], META_DATA_FILENAME) - if os.path.exists(filepath): - with open(filepath, "r") as metadata: - data.update(yaml.safe_load(metadata)) + request = CloudInitRequest(cherrypy.request, uuid) + self._redirect_if_needed(request) cherrypy.response.headers['Content-Type'] = \ 'text/yaml' cherrypy.response.headers['Content-Disposition'] = \ 'attachment; filename="meta-data"' - return yaml.dump(data) + return yaml.dump(request.get_meta_data()) + + @staticmethod + def _vendor_data(): + return "" + + @cherrypy.expose + def user_data(self): + """ + v1 api endpoint user-data + """ + return self._user_data() + + @cherrypy.expose + def meta_data(self): + """ + v1 api endpoint meta-data + """ + return self._meta_data() @cherrypy.expose def vendor_data(self): """ - Return empty vendor-data + v1 api endpoint vendor-data """ - return "" + return self._vendor_data() @cherrypy.expose + def apiv2(self, uuid=None, filetype="user-data"): + """ + API endpoint v2 + extract command from arguments + """ + if filetype == "user-data": + return self._user_data(uuid) + if filetype == "meta-data": + return self._meta_data(uuid) + if filetype == "vendor-data": + return self._vendor_data() + return "" + + # unused + # @cherrypy.expose def finished(self, data): """ Saves additional meta-data :param data: meta-data to be added """ - self._init_ip() - folder = os.path.join(PATH, "data", self.hostinfo[0]) - if not os.path.exists(folder): - os.makedirs(folder) - - with open(os.path.join(folder, META_DATA_FILENAME), "w") as fin: - fin.write(data) + request = CloudInitRequest(cherrypy.request) + self._redirect_if_needed(request) + request.save_meta_data(data) ROOT = CloudInitApp() @@ -191,8 +298,12 @@ if __name__ == "__main__": config = { '/include': { 'tools.staticdir.on': True, - 'tools.staticdir.dir': os.path.join(current_dir, 'data', 'include'), - 'tools.staticdir.content_types': {'yml': 'text/yaml'} + 'tools.staticdir.dir': os.path.join(current_dir, + 'data', + 'include'), + 'tools.staticdir.content_types': { + 'yml': 'text/yaml' + } } } cherrypy.tree.mount(ROOT, config=config) @@ -203,7 +314,7 @@ if __name__ == "__main__": ENGINE.console_control_handler.subscribe() try: ENGINE.start() - except Exception: + except Exception: # pylint: disable=broad-except sys.exit(1) else: ENGINE.block()