From 5fa8d51b8597b0a48b42f264e22add01a12deb0f Mon Sep 17 00:00:00 2001 From: "Brian C. Lane" Date: Wed, 4 Oct 2017 17:03:08 -0700 Subject: [PATCH] lorax-composer initial commit The lorax-composer program will launch a BDCS compatible API server using Flask and Gevent. Currently this is a skeleton application with only one active route (/api/v0/status). The API code lives in ./src/pylorax/api/v0.py with related code in other pylorax/api/* modules. --- src/pylorax/api/__init__.py | 21 ++++++ src/pylorax/api/crossdomain.py | 63 ++++++++++++++++++ src/pylorax/api/server.py | 32 +++++++++ src/pylorax/api/v0.py | 44 +++++++++++++ src/sbin/lorax | 1 - src/sbin/lorax-composer | 115 +++++++++++++++++++++++++++++++++ 6 files changed, 275 insertions(+), 1 deletion(-) create mode 100644 src/pylorax/api/__init__.py create mode 100644 src/pylorax/api/crossdomain.py create mode 100644 src/pylorax/api/server.py create mode 100644 src/pylorax/api/v0.py create mode 100755 src/sbin/lorax-composer diff --git a/src/pylorax/api/__init__.py b/src/pylorax/api/__init__.py new file mode 100644 index 00000000..5ee1ec8a --- /dev/null +++ b/src/pylorax/api/__init__.py @@ -0,0 +1,21 @@ +# +# lorax-composer API server +# +# Copyright (C) 2017 Red Hat, Inc. +# +# 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 2 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 . +from pylorax.api.crossdomain import crossdomain + +__all__ = ["crossdomain"] + diff --git a/src/pylorax/api/crossdomain.py b/src/pylorax/api/crossdomain.py new file mode 100644 index 00000000..d2da96ff --- /dev/null +++ b/src/pylorax/api/crossdomain.py @@ -0,0 +1,63 @@ +# +# Copyright (C) 2017 Red Hat, Inc. +# +# 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 2 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 . +# + +# crossdomain decorator from - http://flask.pocoo.org/snippets/56/ +from datetime import timedelta +from flask import make_response, request, current_app +from functools import update_wrapper + + +def crossdomain(origin, methods=None, headers=None, + max_age=21600, attach_to_all=True, + automatic_options=True): + if methods is not None: + methods = ', '.join(sorted(x.upper() for x in methods)) + if headers is not None and not isinstance(headers, basestring): + headers = ', '.join(x.upper() for x in headers) + if not isinstance(origin, list): + origin = [origin] + if isinstance(max_age, timedelta): + max_age = max_age.total_seconds() + + def get_methods(): + if methods is not None: + return methods + + options_resp = current_app.make_default_options_response() + return options_resp.headers['allow'] + + def decorator(f): + def wrapped_function(*args, **kwargs): + if automatic_options and request.method == 'OPTIONS': + resp = current_app.make_default_options_response() + else: + resp = make_response(f(*args, **kwargs)) + if not attach_to_all and request.method != 'OPTIONS': + return resp + + h = resp.headers + + h.extend([("Access-Control-Allow-Origin", orig) for orig in origin]) + h['Access-Control-Allow-Methods'] = get_methods() + h['Access-Control-Max-Age'] = str(max_age) + if headers is not None: + h['Access-Control-Allow-Headers'] = headers + return resp + + f.provide_automatic_options = False + return update_wrapper(wrapped_function, f) + return decorator diff --git a/src/pylorax/api/server.py b/src/pylorax/api/server.py new file mode 100644 index 00000000..5e1d2a8a --- /dev/null +++ b/src/pylorax/api/server.py @@ -0,0 +1,32 @@ +# +# Copyright (C) 2017 Red Hat, Inc. +# +# 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 2 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 . +# + +from flask import Flask, jsonify + +from pylorax.api import crossdomain +from pylorax.api.v0 import v0_api + +server = Flask(__name__) + +__all__ = ["server"] + +@server.route('/') +@crossdomain(origin="*") +def hello_world(): + return 'Hello, World!' + +v0_api(server) diff --git a/src/pylorax/api/v0.py b/src/pylorax/api/v0.py new file mode 100644 index 00000000..b8a8e7b5 --- /dev/null +++ b/src/pylorax/api/v0.py @@ -0,0 +1,44 @@ +# +# Copyright (C) 2017 Red Hat, Inc. +# +# 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 2 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 . +# +from flask import jsonify + +# Use pykickstart to calculate disk image size +from pykickstart.parser import KickstartParser +from pykickstart.version import makeVersion, RHEL7 + +from pylorax.api import crossdomain +from pylorax.creator import DRACUT_DEFAULT, mount_boot_part_over_root +from pylorax.creator import make_appliance, make_image, make_livecd, make_live_images +from pylorax.creator import make_runtime, make_squashfs +from pylorax.imgutils import copytree +from pylorax.imgutils import Mount, PartitionMount, umount +from pylorax.installer import InstallError +from pylorax.sysutils import joinpaths + + +# no-virt mode doesn't need libvirt, so make it optional +try: + import libvirt +except ImportError: + libvirt = None + +def v0_api(api): + """ Setup v0 of the API server""" + @api.route("/api/v0/status") + @crossdomain(origin="*") + def v0_status(): + return jsonify(build="devel", api="0", db_version="0", schema_version="0", db_supported=False) diff --git a/src/sbin/lorax b/src/sbin/lorax index b60c7ce7..0984e927 100755 --- a/src/sbin/lorax +++ b/src/sbin/lorax @@ -1,5 +1,4 @@ #!/usr/bin/python - # # lorax # diff --git a/src/sbin/lorax-composer b/src/sbin/lorax-composer new file mode 100755 index 00000000..87c438c3 --- /dev/null +++ b/src/sbin/lorax-composer @@ -0,0 +1,115 @@ +#!/usr/bin/python +# +# lorax-composer +# +# Copyright (C) 2017 Red Hat, Inc. +# +# 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 2 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 logging +log = logging.getLogger("lorax-composer") +program_log = logging.getLogger("program") +pylorax_log = logging.getLogger("pylorax") + +import argparse +import os +import shutil +import sys +import tempfile +from gevent.wsgi import WSGIServer + +from pylorax import vernum +from pylorax.api.server import server + +VERSION = "{0}-{1}".format(os.path.basename(sys.argv[0]), vernum) + +def get_parser(): + """ Return the ArgumentParser for lorax-composer""" + + parser = argparse.ArgumentParser( description="Composer API Server", + fromfile_prefix_chars="@" ) + + parser.add_argument("--host", default="127.0.0.1", metavar="HOST", + help="Host or IP to bind to (127.0.0.1)") + parser.add_argument("--port", default=4000, metavar="PORT", + help="Port to bind to (4000)") + parser.add_argument("--log", dest="logfile", default="/var/log/lorax-composer/composer.log", metavar="LOG", + help="Path to logfile (/var/log/lorax-composer/composer.log)") + parser.add_argument("--mockfiles", default="/var/tmp/bdcs-mockfiles/", metavar="MOCKFILES", + help="Path to JSON files used for /api/mock/ paths (/var/tmp/bdcs-mockfiles/)") + parser.add_argument("--bdcs", default="/mddb/cs.repo", metavar="BDCS", + help="Path to the content store directory (/mddb/cs.repo)") + parser.add_argument("-V", action="store_true", dest="showver", + help="show program's version number and exit") + parser.add_argument("DB", metavar="DATABASE", + help="Path to the BDCS sqlite database") + parser.add_argument("RECIPES", metavar="RECIPES", + help="Path to the recipes") + + return parser + + +def setup_logging(logfile): + # Setup logging to console and to logfile + log.setLevel(logging.DEBUG) + pylorax_log.setLevel(logging.DEBUG) + + sh = logging.StreamHandler() + sh.setLevel(logging.INFO) + fmt = logging.Formatter("%(asctime)s: %(message)s") + sh.setFormatter(fmt) + log.addHandler(sh) + pylorax_log.addHandler(sh) + + fh = logging.FileHandler(filename=logfile, mode="w") + fh.setLevel(logging.DEBUG) + fmt = logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s") + fh.setFormatter(fmt) + log.addHandler(fh) + pylorax_log.addHandler(fh) + + # External program output log + program_log.setLevel(logging.DEBUG) + logfile = os.path.abspath(os.path.dirname(logfile))+"/program.log" + fh = logging.FileHandler(filename=logfile, mode="w") + fh.setLevel(logging.DEBUG) + program_log.addHandler(fh) + + +if __name__ == '__main__': + # parse the arguments + opts = get_parser().parse_args() + + if opts.showver: + print(VERSION) + sys.exit(0) + + errors = [] + if not os.path.exists(opts.DB): + errors.append("Database, %s, doesn't exist." % opts.DB) + if not os.path.exists(opts.RECIPES): + errors.append("Recipe directory, %s, is missing." % opts.RECIPES) + + for e in errors: + print("ERROR: " + e) + if errors: + sys.exit(1) + + setup_logging(opts.logfile) + http_server = WSGIServer((opts.host, opts.port), server) + # The server writes directly to a file object, so point to our log directory + server_logfile = os.path.abspath(os.path.dirname(opts.logfile))+"/server.log" + server_log = open(server_logfile, "w") + http_server.log = server_log + http_server.serve_forever()