diff --git a/lorax-composer/.buildinfo b/lorax-composer/.buildinfo index 130de2d2..3c3eb358 100644 --- a/lorax-composer/.buildinfo +++ b/lorax-composer/.buildinfo @@ -1,4 +1,4 @@ # Sphinx build info version 1 # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. -config: 66d7c44afa220f9e7a6f61390f7186ec +config: 06e0a93740d8be1d7bb18b9ff5eed1aa tags: fbb0d17656682115ca4d033fb2f83ba1 diff --git a/lorax-composer/.doctrees/composer-cli.doctree b/lorax-composer/.doctrees/composer-cli.doctree index c5e4f7f9..349f3496 100644 Binary files a/lorax-composer/.doctrees/composer-cli.doctree and b/lorax-composer/.doctrees/composer-cli.doctree differ diff --git a/lorax-composer/.doctrees/composer.cli.doctree b/lorax-composer/.doctrees/composer.cli.doctree index 3a8e3b49..f031a718 100644 Binary files a/lorax-composer/.doctrees/composer.cli.doctree and b/lorax-composer/.doctrees/composer.cli.doctree differ diff --git a/lorax-composer/.doctrees/composer.doctree b/lorax-composer/.doctrees/composer.doctree index 46f0423b..fdc8580d 100644 Binary files a/lorax-composer/.doctrees/composer.doctree and b/lorax-composer/.doctrees/composer.doctree differ diff --git a/lorax-composer/.doctrees/environment.pickle b/lorax-composer/.doctrees/environment.pickle index 73e5f721..60149ef0 100644 Binary files a/lorax-composer/.doctrees/environment.pickle and b/lorax-composer/.doctrees/environment.pickle differ diff --git a/lorax-composer/.doctrees/index.doctree b/lorax-composer/.doctrees/index.doctree index 00292331..cd64c804 100644 Binary files a/lorax-composer/.doctrees/index.doctree and b/lorax-composer/.doctrees/index.doctree differ diff --git a/lorax-composer/.doctrees/intro.doctree b/lorax-composer/.doctrees/intro.doctree index ae15758e..004bc8f6 100644 Binary files a/lorax-composer/.doctrees/intro.doctree and b/lorax-composer/.doctrees/intro.doctree differ diff --git a/lorax-composer/.doctrees/livemedia-creator.doctree b/lorax-composer/.doctrees/livemedia-creator.doctree index 01cd323b..96d09883 100644 Binary files a/lorax-composer/.doctrees/livemedia-creator.doctree and b/lorax-composer/.doctrees/livemedia-creator.doctree differ diff --git a/lorax-composer/.doctrees/lorax-composer.doctree b/lorax-composer/.doctrees/lorax-composer.doctree index dbc6176c..3f411994 100644 Binary files a/lorax-composer/.doctrees/lorax-composer.doctree and b/lorax-composer/.doctrees/lorax-composer.doctree differ diff --git a/lorax-composer/.doctrees/lorax.doctree b/lorax-composer/.doctrees/lorax.doctree index c87f93db..c4730d86 100644 Binary files a/lorax-composer/.doctrees/lorax.doctree and b/lorax-composer/.doctrees/lorax.doctree differ diff --git a/lorax-composer/.doctrees/modules.doctree b/lorax-composer/.doctrees/modules.doctree index 01a71d71..e0b19e52 100644 Binary files a/lorax-composer/.doctrees/modules.doctree and b/lorax-composer/.doctrees/modules.doctree differ diff --git a/lorax-composer/.doctrees/product-images.doctree b/lorax-composer/.doctrees/product-images.doctree index 10c47522..494ffddb 100644 Binary files a/lorax-composer/.doctrees/product-images.doctree and b/lorax-composer/.doctrees/product-images.doctree differ diff --git a/lorax-composer/.doctrees/pylorax.api.doctree b/lorax-composer/.doctrees/pylorax.api.doctree index 367b13dc..dac98f24 100644 Binary files a/lorax-composer/.doctrees/pylorax.api.doctree and b/lorax-composer/.doctrees/pylorax.api.doctree differ diff --git a/lorax-composer/.doctrees/pylorax.doctree b/lorax-composer/.doctrees/pylorax.doctree index 9322d0eb..0b789463 100644 Binary files a/lorax-composer/.doctrees/pylorax.doctree and b/lorax-composer/.doctrees/pylorax.doctree differ diff --git a/lorax-composer/_modules/composer/cli.html b/lorax-composer/_modules/composer/cli.html index 965dad1b..3e7fe20c 100644 --- a/lorax-composer/_modules/composer/cli.html +++ b/lorax-composer/_modules/composer/cli.html @@ -8,7 +8,7 @@ - composer.cli — Lorax 19.7.18 documentation + composer.cli — Lorax 19.7.21 documentation @@ -16,7 +16,7 @@ - + @@ -38,7 +38,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • @@ -75,13 +75,15 @@ from composer.cli.projects import projects_cmd from composer.cli.compose import compose_cmd from composer.cli.sources import sources_cmd +from composer.cli.status import status_cmd command_map = { "blueprints": blueprints_cmd, "modules": modules_cmd, "projects": projects_cmd, "compose": compose_cmd, - "sources": sources_cmd + "sources": sources_cmd, + "status": status_cmd } @@ -136,7 +138,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • diff --git a/lorax-composer/_modules/composer/cli/blueprints.html b/lorax-composer/_modules/composer/cli/blueprints.html index 3316c123..334577b3 100644 --- a/lorax-composer/_modules/composer/cli/blueprints.html +++ b/lorax-composer/_modules/composer/cli/blueprints.html @@ -8,7 +8,7 @@ - composer.cli.blueprints — Lorax 19.7.18 documentation + composer.cli.blueprints — Lorax 19.7.21 documentation @@ -16,7 +16,7 @@ - + @@ -38,7 +38,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • composer.cli »
  • @@ -70,7 +70,6 @@ log = logging.getLogger("composer-cli") import os -import json from composer import http_client as client from composer.cli.help import blueprints_help @@ -125,14 +124,15 @@ blueprints list """ api_route = client.api_url(api_version, "/blueprints/list") - result = client.get_url_json(socket_path, api_route) - if show_json: - print(json.dumps(result, indent=4)) - return 0 + result = client.get_url_json_unlimited(socket_path, api_route) + (rc, exit_now) = handle_api_result(result, show_json) + if exit_now: + return rc - print("blueprints: " + ", ".join([r for r in result["blueprints"]])) + # "list" should output a plain list of identifiers, one per line. + print("\n".join(result["blueprints"])) - return 0 + return rc
    [docs]def blueprints_show(socket_path, api_version, args, show_json=False): """Show the blueprints, in TOML format @@ -171,17 +171,17 @@ blueprints changes <blueprint,...> Display the changes for each blueprint. """ api_route = client.api_url(api_version, "/blueprints/changes/%s" % (",".join(argify(args)))) - result = client.get_url_json(socket_path, api_route) - if show_json: - print(json.dumps(result, indent=4)) - return 0 + result = client.get_url_json_unlimited(socket_path, api_route) + (rc, exit_now) = handle_api_result(result, show_json) + if exit_now: + return rc for blueprint in result["blueprints"]: print(blueprint["name"]) for change in blueprint["changes"]: prettyCommitDetails(change) - return 0 + return rc
    [docs]def prettyCommitDetails(change, indent=4): """Print the blueprint's change in a nice way @@ -197,8 +197,8 @@ else: return "" - print " " * indent + change["timestamp"] + " " + change["commit"] + revision() - print " " * indent + change["message"] + "\n" + print(" " * indent + change["timestamp"] + " " + change["commit"] + revision()) + print(" " * indent + change["message"] + "\n")
    [docs]def blueprints_diff(socket_path, api_version, args, show_json=False): """Display the differences between 2 versions of a blueprint @@ -228,21 +228,14 @@ api_route = client.api_url(api_version, "/blueprints/diff/%s/%s/%s" % (args[0], args[1], args[2])) result = client.get_url_json(socket_path, api_route) - - if show_json: - print(json.dumps(result, indent=4)) - return 0 - - for err in result.get("errors", []): - log.error(err) - - if result.get("errors", False): - return 1 + (rc, exit_now) = handle_api_result(result, show_json) + if exit_now: + return rc for diff in result["diff"]: print(prettyDiffEntry(diff)) - return 0 + return rc
    [docs]def prettyDiffEntry(diff): """Generate nice diff entry string. @@ -283,11 +276,15 @@ elif change(diff) == "Added": if name(diff) in ["Module", "Package"]: return "%s %s" % (diff["new"][name(diff)]["name"], diff["new"][name(diff)]["version"]) + elif name(diff) in ["Group"]: + return diff["new"][name(diff)]["name"] else: return " ".join([diff["new"][k] for k in diff["new"]]) elif change(diff) == "Removed": if name(diff) in ["Module", "Package"]: return "%s %s" % (diff["old"][name(diff)]["name"], diff["old"][name(diff)]["version"]) + elif name(diff) in ["Group"]: + return diff["old"][name(diff)]["name"] else: return " ".join([diff["old"][k] for k in diff["old"]]) @@ -331,7 +328,7 @@ api_route = client.api_url(api_version, "/blueprints/delete/%s" % args[0]) result = client.delete_url_json(socket_path, api_route) - return handle_api_result(result, show_json) + return handle_api_result(result, show_json)[0]
    [docs]def blueprints_depsolve(socket_path, api_version, args, show_json=False): """Display the packages needed to install the blueprint @@ -349,10 +346,9 @@ """ api_route = client.api_url(api_version, "/blueprints/depsolve/%s" % (",".join(argify(args)))) result = client.get_url_json(socket_path, api_route) - - if show_json: - print(json.dumps(result, indent=4)) - return 0 + (rc, exit_now) = handle_api_result(result, show_json) + if exit_now: + return rc for blueprint in result["blueprints"]: if blueprint["blueprint"].get("version", ""): @@ -362,7 +358,7 @@ for dep in blueprint["dependencies"]: print(" " + packageNEVRA(dep)) - return 0 + return rc
    [docs]def blueprints_push(socket_path, api_version, args, show_json=False): """Push a blueprint TOML file to the server, updating the blueprint @@ -387,7 +383,7 @@ blueprint_toml = open(blueprint, "r").read() result = client.post_url_toml(socket_path, api_route, blueprint_toml) - if handle_api_result(result, show_json): + if handle_api_result(result, show_json)[0]: rval = 1 return rval @@ -419,32 +415,24 @@ api_route = client.api_url(api_version, "/blueprints/freeze/%s" % (",".join(argify(args)))) result = client.get_url_json(socket_path, api_route) + (rc, exit_now) = handle_api_result(result, show_json) + if exit_now: + return rc - if show_json: - print(json.dumps(result, indent=4)) - else: - for entry in result["blueprints"]: - blueprint = entry["blueprint"] - if blueprint.get("version", ""): - print("blueprint: %s v%s" % (blueprint["name"], blueprint["version"])) - else: - print("blueprint: %s" % (blueprint["name"])) + for entry in result["blueprints"]: + blueprint = entry["blueprint"] + if blueprint.get("version", ""): + print("blueprint: %s v%s" % (blueprint["name"], blueprint["version"])) + else: + print("blueprint: %s" % (blueprint["name"])) - for m in blueprint["modules"]: - print(" %s-%s" % (m["name"], m["version"])) + for m in blueprint["modules"]: + print(" %s-%s" % (m["name"], m["version"])) - for p in blueprint["packages"]: - print(" %s-%s" % (p["name"], p["version"])) + for p in blueprint["packages"]: + print(" %s-%s" % (p["name"], p["version"])) - # Print any errors - for err in result.get("errors", []): - log.error(err) - - # Return a 1 if there are any errors - if result.get("errors", []): - return 1 - else: - return 0 + return rc
    [docs]def blueprints_freeze_show(socket_path, api_version, args, show_json=False): """Show the frozen blueprint in TOML format @@ -512,7 +500,7 @@ api_route = client.api_url(api_version, "/blueprints/tag/%s" % args[0]) result = client.post_url(socket_path, api_route, "") - return handle_api_result(result, show_json) + return handle_api_result(result, show_json)[0]
    [docs]def blueprints_undo(socket_path, api_version, args, show_json=False): """Undo changes to a blueprint @@ -538,7 +526,7 @@ api_route = client.api_url(api_version, "/blueprints/undo/%s/%s" % (args[0], args[1])) result = client.post_url(socket_path, api_route, "") - return handle_api_result(result, show_json) + return handle_api_result(result, show_json)[0]
    [docs]def blueprints_workspace(socket_path, api_version, args, show_json=False): """Push the blueprint TOML to the temporary workspace storage @@ -563,14 +551,7 @@ blueprint_toml = open(blueprint, "r").read() result = client.post_url_toml(socket_path, api_route, blueprint_toml) - if show_json: - print(json.dumps(result, indent=4)) - - for err in result.get("errors", []): - log.error(err) - - # Any errors results in returning a 1, but we continue with the rest first - if not result.get("status", False): + if handle_api_result(result, show_json)[0]: rval = 1 return rval
    @@ -607,7 +588,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • composer.cli »
  • diff --git a/lorax-composer/_modules/composer/cli/compose.html b/lorax-composer/_modules/composer/cli/compose.html index da427331..d60be148 100644 --- a/lorax-composer/_modules/composer/cli/compose.html +++ b/lorax-composer/_modules/composer/cli/compose.html @@ -8,7 +8,7 @@ - composer.cli.compose — Lorax 19.7.18 documentation + composer.cli.compose — Lorax 19.7.21 documentation @@ -16,7 +16,7 @@ - + @@ -38,7 +38,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • composer.cli »
  • @@ -69,6 +69,7 @@ import logging log = logging.getLogger("composer-cli") +from datetime import datetime import sys import json @@ -87,6 +88,7 @@ This dispatches the compose commands to a function """ cmd_map = { + "list": compose_list, "status": compose_status, "types": compose_types, "start": compose_start, @@ -99,7 +101,7 @@ "logs": compose_logs, "image": compose_image, } - if opts.args == "help" or opts.args == "--help": + if opts.args[1] == "help" or opts.args[1] == "--help": print(compose_help) return 0 elif opts.args[1] not in cmd_map: @@ -108,6 +110,50 @@ return cmd_map[opts.args[1]](opts.socket, opts.api_version, opts.args[2:], opts.json, opts.testmode) +
    [docs]def compose_list(socket_path, api_version, args, show_json=False, testmode=0): + """Return a simple list of compose identifiers""" + + states = ("running", "waiting", "finished", "failed") + + which = set() + + if any(a not in states for a in args): + # TODO: error about unknown state + return 1 + elif not args: + which.update(states) + else: + which.update(args) + + results = [] + + if "running" in which or "waiting" in which: + api_route = client.api_url(api_version, "/compose/queue") + r = client.get_url_json(socket_path, api_route) + if "running" in which: + results += r["run"] + if "waiting" in which: + results += r["new"] + + if "finished" in which: + api_route = client.api_url(api_version, "/compose/finished") + r = client.get_url_json(socket_path, api_route) + results += r["finished"] + + if "failed" in which: + api_route = client.api_url(api_version, "/compose/failed") + r = client.get_url_json(socket_path, api_route) + results += r["failed"] + + if results: + if show_json: + print(json.dumps(results, indent=4)) + else: + list_fmt = "{id} {queue_status} {blueprint} {version} {compose_type}" + print("\n".join(list_fmt.format(**c) for c in results)) + + return 0 +
    [docs]def compose_status(socket_path, api_version, args, show_json=False, testmode=0): """Return the status of all known composes @@ -131,7 +177,10 @@ "version": compose["version"], "compose_type": compose["compose_type"], "image_size": compose["image_size"], - "status": compose["queue_status"]} + "status": compose["queue_status"], + "created": compose.get("job_created"), + "started": compose.get("job_started"), + "finished": compose.get("job_finished")} # Sort the status in a specific order def sort_status(a): @@ -169,8 +218,10 @@ else: image_size = "" - print("%s %-8s %-15s %s %-16s %s" % (c["id"], c["status"], c["blueprint"], c["version"], c["compose_type"], - image_size)) + dt = datetime.fromtimestamp(c.get("finished") or c.get("started") or c.get("created")) + + print("%s %-8s %s %-15s %s %-16s %s" % (c["id"], c["status"], dt.strftime("%c"), c["blueprint"], + c["version"], c["compose_type"], image_size))
    [docs]def compose_types(socket_path, api_version, args, show_json=False, testmode=0): @@ -196,7 +247,7 @@ print(json.dumps(result, indent=4)) return 0 - print("Compose Types: " + ", ".join([t["name"] for t in result["types"]])) + print("\n".join(t["name"] for t in result["types"]))
    [docs]def compose_start(socket_path, api_version, args, show_json=False, testmode=0): """Start a new compose using the selected blueprint and type @@ -232,19 +283,12 @@ test_url = "" api_route = client.api_url(api_version, "/compose" + test_url) result = client.post_url_json(socket_path, api_route, json.dumps(config)) - - if show_json: - print(json.dumps(result, indent=4)) - return 0 - - for err in result.get("errors", []): - log.error(err) - - if result["status"] == False or result.get("errors", False): - return 1 + (rc, exit_now) = handle_api_result(result, show_json) + if exit_now: + return rc print("Compose %s added to the queue" % result["build_id"]) - return 0 + return rc
    [docs]def compose_log(socket_path, api_version, args, show_json=False, testmode=0): """Show the last part of the compose log @@ -311,7 +355,7 @@ api_route = client.api_url(api_version, "/compose/cancel/%s" % args[0]) result = client.delete_url_json(socket_path, api_route) - return handle_api_result(result, show_json) + return handle_api_result(result, show_json)[0]
    [docs]def compose_delete(socket_path, api_version, args, show_json=False, testmode=0): """Delete a finished compose's results @@ -338,19 +382,7 @@ api_route = client.api_url(api_version, "/compose/delete/%s" % (",".join(argify(args)))) result = client.delete_url_json(socket_path, api_route) - - if show_json: - print(json.dumps(result, indent=4)) - return 0 - - # Print any errors - for err in result.get("errors", []): - log.error(err) - - if result.get("errors", []): - return 1 - else: - return 0 + return handle_api_result(result, show_json)[0]
    [docs]def compose_details(socket_path, api_version, args, show_json=False, testmode=0): """Return detailed information about the compose @@ -376,15 +408,9 @@ api_route = client.api_url(api_version, "/compose/info/%s" % args[0]) result = client.get_url_json(socket_path, api_route) - if show_json: - print(json.dumps(result, indent=4)) - return 0 - - for err in result.get("errors", []): - log.error(err) - - if result.get("errors", []): - return 1 + (rc, exit_now) = handle_api_result(result, show_json) + if exit_now: + return rc if result["image_size"] > 0: image_size = str(result["image_size"]) @@ -409,6 +435,8 @@ print("Dependencies:") for d in result["deps"]["packages"]: print(" " + packageNEVRA(d)) + + return rc
    [docs]def compose_metadata(socket_path, api_version, args, show_json=False, testmode=0): """Download a tar file of the compose's metadata @@ -433,7 +461,13 @@ return 1 api_route = client.api_url(api_version, "/compose/metadata/%s" % args[0]) - return client.download_file(socket_path, api_route) + try: + rc = client.download_file(socket_path, api_route) + except RuntimeError as e: + print(str(e)) + rc = 1 + + return rc
    [docs]def compose_results(socket_path, api_version, args, show_json=False, testmode=0): """Download a tar file of the compose's results @@ -459,7 +493,13 @@ return 1 api_route = client.api_url(api_version, "/compose/results/%s" % args[0]) - return client.download_file(socket_path, api_route, sys.stdout.isatty()) + try: + rc = client.download_file(socket_path, api_route, sys.stdout.isatty()) + except RuntimeError as e: + print(str(e)) + rc = 1 + + return rc
    [docs]def compose_logs(socket_path, api_version, args, show_json=False, testmode=0): """Download a tar of the compose's logs @@ -484,7 +524,13 @@ return 1 api_route = client.api_url(api_version, "/compose/logs/%s" % args[0]) - return client.download_file(socket_path, api_route, sys.stdout.isatty()) + try: + rc = client.download_file(socket_path, api_route, sys.stdout.isatty()) + except RuntimeError as e: + print(str(e)) + rc = 1 + + return rc
    [docs]def compose_image(socket_path, api_version, args, show_json=False, testmode=0): """Download the compose's output image @@ -510,7 +556,13 @@ return 1 api_route = client.api_url(api_version, "/compose/image/%s" % args[0]) - return client.download_file(socket_path, api_route, sys.stdout.isatty())
    + try: + rc = client.download_file(socket_path, api_route, sys.stdout.isatty()) + except RuntimeError as e: + print(str(e)) + rc = 1 + + return rc @@ -544,7 +596,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • composer.cli »
  • diff --git a/lorax-composer/_modules/composer/cli/modules.html b/lorax-composer/_modules/composer/cli/modules.html index 802cdcec..d7fa02ee 100644 --- a/lorax-composer/_modules/composer/cli/modules.html +++ b/lorax-composer/_modules/composer/cli/modules.html @@ -8,7 +8,7 @@ - composer.cli.modules — Lorax 19.7.18 documentation + composer.cli.modules — Lorax 19.7.21 documentation @@ -16,7 +16,7 @@ - + @@ -38,7 +38,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • composer.cli »
  • @@ -69,10 +69,9 @@ import logging log = logging.getLogger("composer-cli") -import json - from composer import http_client as client from composer.cli.help import modules_help +from composer.cli.utilities import handle_api_result
    [docs]def modules_cmd(opts): """Process modules commands @@ -90,14 +89,15 @@ return 1 api_route = client.api_url(opts.api_version, "/modules/list") - result = client.get_url_json(opts.socket, api_route) - if opts.json: - print(json.dumps(result, indent=4)) - return 0 + result = client.get_url_json_unlimited(opts.socket, api_route) + (rc, exit_now) = handle_api_result(result, opts.json) + if exit_now: + return rc - print("Modules:\n" + "\n".join([" "+r["name"] for r in result["modules"]])) + # "list" should output a plain list of identifiers, one per line. + print("\n".join(r["name"] for r in result["modules"])) - return 0
    + return rc @@ -131,7 +131,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • composer.cli »
  • diff --git a/lorax-composer/_modules/composer/cli/projects.html b/lorax-composer/_modules/composer/cli/projects.html index a3c22379..510f87e6 100644 --- a/lorax-composer/_modules/composer/cli/projects.html +++ b/lorax-composer/_modules/composer/cli/projects.html @@ -8,7 +8,7 @@ - composer.cli.projects — Lorax 19.7.18 documentation + composer.cli.projects — Lorax 19.7.21 documentation @@ -16,7 +16,7 @@ - + @@ -38,7 +38,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • composer.cli »
  • @@ -69,11 +69,11 @@ import logging log = logging.getLogger("composer-cli") -import json import textwrap from composer import http_client as client from composer.cli.help import projects_help +from composer.cli.utilities import handle_api_result
    [docs]def projects_cmd(opts): """Process projects commands @@ -111,17 +111,17 @@ projects list """ api_route = client.api_url(api_version, "/projects/list") - result = client.get_url_json(socket_path, api_route) - if show_json: - print(json.dumps(result, indent=4)) - return 0 + result = client.get_url_json_unlimited(socket_path, api_route) + (rc, exit_now) = handle_api_result(result, show_json) + if exit_now: + return rc for proj in result["projects"]: - for k in ["name", "summary", "homepage", "description"]: + for k in [field for field in ("name", "summary", "homepage", "description") if proj[field]]: print("%s: %s" % (k.title(), textwrap.fill(proj[k], subsequent_indent=" " * (len(k)+2)))) print("\n\n") - return 0 + return rc
    [docs]def projects_info(socket_path, api_version, args, show_json=False): """Output info on a list of projects @@ -143,23 +143,23 @@ api_route = client.api_url(api_version, "/projects/info/%s" % ",".join(args)) result = client.get_url_json(socket_path, api_route) - if show_json: - print(json.dumps(result, indent=4)) - return 0 + (rc, exit_now) = handle_api_result(result, show_json) + if exit_now: + return rc for proj in result["projects"]: - for k in ["name", "summary", "homepage", "description"]: + for k in [field for field in ("name", "summary", "homepage", "description") if proj[field]]: print("%s: %s" % (k.title(), textwrap.fill(proj[k], subsequent_indent=" " * (len(k)+2)))) print("Builds: ") for build in proj["builds"]: - print(" %s%s-%s.%s at %s for %s" % ("" if not build["epoch"] else build["epoch"] + ":", + print(" %s%s-%s.%s at %s for %s" % ("" if not build["epoch"] else str(build["epoch"]) + ":", build["source"]["version"], build["release"], build["arch"], build["build_time"], build["changelog"])) print("") - return 0
    + return rc @@ -193,7 +193,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • composer.cli »
  • diff --git a/lorax-composer/_modules/composer/cli/sources.html b/lorax-composer/_modules/composer/cli/sources.html index e44035d7..656c5d8a 100644 --- a/lorax-composer/_modules/composer/cli/sources.html +++ b/lorax-composer/_modules/composer/cli/sources.html @@ -8,7 +8,7 @@ - composer.cli.sources — Lorax 19.7.18 documentation + composer.cli.sources — Lorax 19.7.21 documentation @@ -16,7 +16,7 @@ - + @@ -38,7 +38,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • composer.cli »
  • @@ -70,7 +70,6 @@ log = logging.getLogger("composer-cli") import os -import json from composer import http_client as client from composer.cli.help import sources_help @@ -116,12 +115,13 @@ """ api_route = client.api_url(api_version, "/projects/source/list") result = client.get_url_json(socket_path, api_route) - if show_json: - print(json.dumps(result, indent=4)) - return 0 + (rc, exit_now) = handle_api_result(result, show_json) + if exit_now: + return rc - print("Sources: %s" % ", ".join(result["sources"])) - return 0 + # "list" should output a plain list of identifiers, one per line. + print("\n".join(result["sources"])) + return rc
    [docs]def sources_info(socket_path, api_version, args, show_json=False): """Output info on a list of projects @@ -144,13 +144,18 @@ if show_json: api_route = client.api_url(api_version, "/projects/source/info/%s" % ",".join(args)) result = client.get_url_json(socket_path, api_route) - print(json.dumps(result, indent=4)) - return 0 + rc = handle_api_result(result, show_json)[0] else: api_route = client.api_url(api_version, "/projects/source/info/%s?format=toml" % ",".join(args)) - result = client.get_url_raw(socket_path, api_route) - print(result) - return 0 + try: + result = client.get_url_raw(socket_path, api_route) + print(result) + rc = 0 + except RuntimeError as e: + print(str(e)) + rc = 1 + + return rc
    [docs]def sources_add(socket_path, api_version, args, show_json=False): """Add or change a source @@ -175,7 +180,7 @@ source_toml = open(source, "r").read() result = client.post_url_toml(socket_path, api_route, source_toml) - if handle_api_result(result, show_json): + if handle_api_result(result, show_json)[0]: rval = 1 return rval
    @@ -196,7 +201,7 @@ api_route = client.api_url(api_version, "/projects/source/delete/%s" % args[0]) result = client.delete_url_json(socket_path, api_route) - return handle_api_result(result, show_json) + return handle_api_result(result, show_json)[0] @@ -230,7 +235,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • composer.cli »
  • diff --git a/lorax-composer/_modules/composer/cli/status.html b/lorax-composer/_modules/composer/cli/status.html new file mode 100644 index 00000000..4951d147 --- /dev/null +++ b/lorax-composer/_modules/composer/cli/status.html @@ -0,0 +1,152 @@ + + + + + + + + + + composer.cli.status — Lorax 19.7.21 documentation + + + + + + + + + + + + + + +
    +
    +
    +
    + +

    Source code for composer.cli.status

    +#
    +# Copyright (C) 2018  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 <http://www.gnu.org/licenses/>.
    +#
    +import logging
    +log = logging.getLogger("composer-cli")
    +
    +from composer import http_client as client
    +from composer.cli.help import status_help
    +from composer.cli.utilities import handle_api_result
    +
    +
    [docs]def status_cmd(opts): + """Process status commands + + :param opts: Cmdline arguments + :type opts: argparse.Namespace + :returns: Value to return from sys.exit() + :rtype: int + """ + if opts.args[1] == "help" or opts.args[1] == "--help": + print(status_help) + return 0 + elif opts.args[1] != "show": + log.error("Unknown status command: %s", opts.args[1]) + return 1 + + result = client.get_url_json(opts.socket, "/api/status") + (rc, exit_now) = handle_api_result(result, opts.json) + if exit_now: + return rc + + print("API server status:") + print(" Database version: " + result["db_version"]) + print(" Database supported: %s" % result["db_supported"]) + print(" Schema version: " + result["schema_version"]) + print(" API version: " + result["api"]) + print(" Backend: " + result["backend"]) + print(" Build: " + result["build"]) + + if result["msgs"]: + print("Error messages:") + print("\n".join([" " + r for r in result["msgs"]])) + + return rc
    +
    + +
    +
    +
    +
    +
    + + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/lorax-composer/_modules/composer/cli/utilities.html b/lorax-composer/_modules/composer/cli/utilities.html index 0702e427..6c26bcac 100644 --- a/lorax-composer/_modules/composer/cli/utilities.html +++ b/lorax-composer/_modules/composer/cli/utilities.html @@ -8,7 +8,7 @@ - composer.cli.utilities — Lorax 19.7.18 documentation + composer.cli.utilities — Lorax 19.7.21 documentation @@ -16,7 +16,7 @@ - + @@ -38,7 +38,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • composer.cli »
  • @@ -110,17 +110,28 @@ :param result: JSON result from the http query :type result: dict + :rtype: tuple + :returns: (rc, should_exit_now) + + Return the correct rc for the program (0 or 1), and whether or + not to continue processing the results. """ if show_json: print(json.dumps(result, indent=4)) - - for err in result.get("errors", []): - log.error(err) - - if result["status"] == True: - return 0 else: - return 1 + for err in result.get("errors", []): + log.error(err["msg"]) + + # What's the rc? If status is present, use that + # If not, use length of errors + if "status" in result: + rc = bool(not result["status"]) + else: + rc = bool(len(result.get("errors", [])) > 0) + + # Caller should return if showing json, or status was present and False + exit_now = show_json or ("status" in result and rc) + return (rc, exit_now)
    [docs]def packageNEVRA(pkg): """Return the package info as a NEVRA @@ -167,7 +178,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • composer.cli »
  • diff --git a/lorax-composer/_modules/composer/http_client.html b/lorax-composer/_modules/composer/http_client.html index 4a276851..862acb40 100644 --- a/lorax-composer/_modules/composer/http_client.html +++ b/lorax-composer/_modules/composer/http_client.html @@ -8,7 +8,7 @@ - composer.http_client — Lorax 19.7.18 documentation + composer.http_client — Lorax 19.7.21 documentation @@ -16,7 +16,7 @@ - + @@ -38,7 +38,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • @@ -71,6 +71,7 @@ import os import sys import json +from urlparse import urlparse, urlunparse from composer.unix_socket import UnixHTTPConnectionPool @@ -86,6 +87,29 @@ """ return os.path.normpath("/api/v%s/%s" % (api_version, url)) +
    [docs]def append_query(url, query): + """Add a query argument to a URL + + The query should be of the form "param1=what&param2=ever", i.e., no + leading '?'. The new query data will be appended to any existing + query string. + + :param url: The original URL + :type url: str + :param query: The query to append + :type query: str + :returns: The new URL with the query argument included + :rtype: str + """ + + url_parts = urlparse(url) + if url_parts.query: + new_query = url_parts.query + "&" + query + else: + new_query = query + return urlunparse([url_parts[0], url_parts[1], url_parts[2], + url_parts[3], new_query, url_parts[5]]) +
    [docs]def get_url_raw(socket_path, url): """Return the raw results of a GET request @@ -101,7 +125,8 @@ if r.status == 400: err = json.loads(r.data.decode("utf-8")) if "status" in err and err["status"] == False: - raise RuntimeError(", ".join(err["errors"])) + msgs = [e["msg"] for e in err["errors"]] + raise RuntimeError(", ".join(msgs)) return r.data.decode('utf-8')
    @@ -119,6 +144,31 @@ r = http.request("GET", url) return json.loads(r.data.decode('utf-8')) +
    [docs]def get_url_json_unlimited(socket_path, url): + """Return the JSON results of a GET request + + For URLs that use offset/limit arguments, this command will + fetch all results for the given request. + + :param socket_path: Path to the Unix socket to use for API communication + :type socket_path: str + :param url: URL to request + :type url: str + :returns: The json response from the server + :rtype: dict + """ + http = UnixHTTPConnectionPool(socket_path) + + # Start with limit=0 to just get the number of objects + total_url = append_query(url, "limit=0") + r_total = http.request("GET", total_url) + json_total = json.loads(r_total.data.decode('utf-8')) + + # Add the "total" returned by limit=0 as the new limit + unlimited_url = append_query(url, "limit=%d" % json_total["total"]) + r_unlimited = http.request("GET", unlimited_url) + return json.loads(r_unlimited.data.decode('utf-8')) +
    [docs]def delete_url_json(socket_path, url): """Send a DELETE request to the url and return JSON response @@ -223,7 +273,8 @@ if r.status == 400: err = json.loads(r.data.decode("utf-8")) if not err["status"]: - raise RuntimeError(", ".join(err["errors"])) + msgs = [e["msg"] for e in err["errors"]] + raise RuntimeError(", ".join(msgs)) filename = get_filename(r.headers) if os.path.exists(filename): @@ -283,7 +334,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • diff --git a/lorax-composer/_modules/composer/unix_socket.html b/lorax-composer/_modules/composer/unix_socket.html index 7453bac6..2d226d54 100644 --- a/lorax-composer/_modules/composer/unix_socket.html +++ b/lorax-composer/_modules/composer/unix_socket.html @@ -8,7 +8,7 @@ - composer.unix_socket — Lorax 19.7.18 documentation + composer.unix_socket — Lorax 19.7.21 documentation @@ -16,7 +16,7 @@ - + @@ -38,7 +38,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • @@ -143,7 +143,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • diff --git a/lorax-composer/_modules/index.html b/lorax-composer/_modules/index.html index 624c7136..ed92cd0d 100644 --- a/lorax-composer/_modules/index.html +++ b/lorax-composer/_modules/index.html @@ -8,7 +8,7 @@ - Overview: module code — Lorax 19.7.18 documentation + Overview: module code — Lorax 19.7.21 documentation @@ -16,7 +16,7 @@ - + @@ -53,18 +53,21 @@
  • composer.cli.modules
  • composer.cli.projects
  • composer.cli.sources
  • +
  • composer.cli.status
  • composer.cli.utilities
  • composer.http_client
  • composer.unix_socket
  • pylorax
  • diff --git a/lorax-composer/_modules/pylorax/api.html b/lorax-composer/_modules/pylorax/api.html index e1957441..5e528c4a 100644 --- a/lorax-composer/_modules/pylorax/api.html +++ b/lorax-composer/_modules/pylorax/api.html @@ -8,7 +8,7 @@ - pylorax.api — Lorax 19.7.18 documentation + pylorax.api — Lorax 19.7.21 documentation @@ -16,7 +16,7 @@ - + @@ -38,7 +38,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • @@ -103,7 +103,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • diff --git a/lorax-composer/_modules/pylorax/api/checkparams.html b/lorax-composer/_modules/pylorax/api/checkparams.html new file mode 100644 index 00000000..de84b4e7 --- /dev/null +++ b/lorax-composer/_modules/pylorax/api/checkparams.html @@ -0,0 +1,142 @@ + + + + + + + + + + pylorax.api.checkparams — Lorax 19.7.21 documentation + + + + + + + + + + + + + + +
    +
    +
    +
    + +

    Source code for pylorax.api.checkparams

    +#
    +# Copyright (C) 2018  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 <http://www.gnu.org/licenses/>.
    +#
    +
    +import logging
    +log = logging.getLogger("lorax-composer")
    +
    +from flask import jsonify
    +from functools import update_wrapper
    +
    +# A decorator for checking the parameters provided to the API route implementing
    +# functions.  The tuples parameter is a list of tuples.  Each tuple is the string
    +# name of a parameter ("blueprint_name", not blueprint_name), the value it's set
    +# to by flask if the caller did not provide it, and a message to be returned to
    +# the user.
    +#
    +# If the parameter is set to its default, the error message is returned.  Otherwise,
    +# the decorated function is called and its return value is returned.
    +
    [docs]def checkparams(tuples): + def decorator(f): + def wrapped_function(*args, **kwargs): + for tup in tuples: + if kwargs[tup[0]] == tup[1]: + log.error("(%s) %s", f.__name__, tup[2]) + return jsonify(status=False, errors=[tup[2]]), 400 + + return f(*args, **kwargs) + + return update_wrapper(wrapped_function, f) + + return decorator
    +
    + +
    +
    +
    +
    +
    + + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/lorax-composer/_modules/pylorax/api/compose.html b/lorax-composer/_modules/pylorax/api/compose.html index c90f3ca2..003f7a23 100644 --- a/lorax-composer/_modules/pylorax/api/compose.html +++ b/lorax-composer/_modules/pylorax/api/compose.html @@ -8,7 +8,7 @@ - pylorax.api.compose — Lorax 19.7.18 documentation + pylorax.api.compose — Lorax 19.7.21 documentation @@ -16,7 +16,7 @@ - + @@ -38,7 +38,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • pylorax.api »
  • @@ -99,13 +99,43 @@ from pykickstart.parser import KickstartParser from pykickstart.version import makeVersion, RHEL7 -from pylorax.api.projects import projects_depsolve_with_size, dep_nevra +from pylorax.api.projects import projects_depsolve, projects_depsolve_with_size, dep_nevra from pylorax.api.projects import ProjectsError from pylorax.api.recipes import read_recipe_and_id +from pylorax.api.timestamp import TS_CREATED, write_timestamp from pylorax.imgutils import default_image_name from pylorax.sysutils import joinpaths +
    [docs]def test_templates(yb, share_dir): + """ Try depsolving each of the the templates and report any errors + + :param yb: yum base object + :type yb: YumBase + :returns: List of template types and errors + :rtype: List of errors + + Return a list of templates and errors encountered or an empty list + """ + template_errors = [] + for compose_type in compose_types(share_dir): + # Read the kickstart template for this type + ks_template_path = joinpaths(share_dir, "composer", compose_type) + ".ks" + ks_template = open(ks_template_path, "r").read() + + # How much space will the packages in the default template take? + ks_version = makeVersion(RHEL7) + ks = KickstartParser(ks_version, errorsAreFatal=False, missingIncludeIsFatal=False) + ks.readKickstartFromString(ks_template+"\n%end\n") + pkgs = [(name, "*") for name in ks.handler.packages.packageList] + grps = [grp.name for grp in ks.handler.packages.groupList] + try: + _ = projects_depsolve(yb, pkgs, grps) + except ProjectsError as e: + template_errors.append("Error depsolving %s: %s" % (compose_type, str(e))) + + return template_errors +
    [docs]def repo_to_ks(r, url="url"): """ Return a kickstart line with the correct args. @@ -289,7 +319,7 @@ deps = [] try: with yumlock.lock: - (installed_size, deps) = projects_depsolve_with_size(yumlock.yb, projects, with_core=False) + (installed_size, deps) = projects_depsolve_with_size(yumlock.yb, projects, recipe.group_names, with_core=False) except ProjectsError as e: log.error("start_build depsolve: %s", str(e)) raise RuntimeError("Problem depsolving %s: %s" % (recipe["name"], str(e))) @@ -303,9 +333,10 @@ ks = KickstartParser(ks_version, errorsAreFatal=False, missingIncludeIsFatal=False) ks.readKickstartFromString(ks_template+"\n%end\n") pkgs = [(name, "*") for name in ks.handler.packages.packageList] + grps = [grp.name for grp in ks.handler.packages.groupList] try: with yumlock.lock: - (template_size, _) = projects_depsolve_with_size(yumlock.yb, pkgs, + (template_size, _) = projects_depsolve_with_size(yumlock.yb, pkgs, grps, with_core=not ks.handler.packages.nocore) except ProjectsError as e: log.error("start_build depsolve: %s", str(e)) @@ -361,6 +392,10 @@ log.debug("repo composer-%s = %s", idx, ks_repo) f.write('repo --name="composer-%s" %s\n' % (idx, ks_repo)) + # Setup the disk for booting + # TODO Add GPT and UEFI boot support + f.write('clearpart --all\n') + # Write the root partition and it's size in MB (rounded up) f.write('part / --fstype="ext4" --size=%d\n' % ceil(installed_size / 1024**2)) @@ -407,6 +442,7 @@ if test_mode > 0: open(joinpaths(results_dir, "TEST"), "w").write("%s" % test_mode) + write_timestamp(results_dir, TS_CREATED) log.info("Adding %s (%s %s) to compose queue", build_id, recipe["name"], compose_type) os.symlink(results_dir, joinpaths(lib_dir, "queue/new/", build_id)) @@ -587,7 +623,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • pylorax.api »
  • diff --git a/lorax-composer/_modules/pylorax/api/config.html b/lorax-composer/_modules/pylorax/api/config.html index c0ef46f3..f311bfe8 100644 --- a/lorax-composer/_modules/pylorax/api/config.html +++ b/lorax-composer/_modules/pylorax/api/config.html @@ -8,7 +8,7 @@ - pylorax.api.config — Lorax 19.7.18 documentation + pylorax.api.config — Lorax 19.7.21 documentation @@ -16,7 +16,7 @@ - + @@ -38,7 +38,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • pylorax.api »
  • @@ -197,7 +197,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • pylorax.api »
  • diff --git a/lorax-composer/_modules/pylorax/api/crossdomain.html b/lorax-composer/_modules/pylorax/api/crossdomain.html index b933a1ef..0ceb626c 100644 --- a/lorax-composer/_modules/pylorax/api/crossdomain.html +++ b/lorax-composer/_modules/pylorax/api/crossdomain.html @@ -8,7 +8,7 @@ - pylorax.api.crossdomain — Lorax 19.7.18 documentation + pylorax.api.crossdomain — Lorax 19.7.21 documentation @@ -16,7 +16,7 @@ - + @@ -38,7 +38,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • pylorax.api »
  • @@ -148,7 +148,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • pylorax.api »
  • diff --git a/lorax-composer/_modules/pylorax/api/projects.html b/lorax-composer/_modules/pylorax/api/projects.html index b4dc8779..2cadc67e 100644 --- a/lorax-composer/_modules/pylorax/api/projects.html +++ b/lorax-composer/_modules/pylorax/api/projects.html @@ -8,7 +8,7 @@ - pylorax.api.projects — Lorax 19.7.18 documentation + pylorax.api.projects — Lorax 19.7.21 documentation @@ -16,7 +16,7 @@ - + @@ -38,7 +38,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • pylorax.api »
  • @@ -242,13 +242,15 @@ return sorted(map(yaps_to_project_info, ybl.available), key=lambda p: p["name"].lower())
    -
    [docs]def projects_depsolve(yb, projects): +
    [docs]def projects_depsolve(yb, projects, groups): """Return the dependencies for a list of projects :param yb: yum base object :type yb: YumBase :param projects: The projects and version globs to find the dependencies for :type projects: List of tuples + :param groups: The groups to include in dependency solving + :type groups: List of str :returns: NEVRA's of the project and its dependencies :rtype: list of dicts :raises: ProjectsError if there was a problem installing something @@ -256,10 +258,23 @@ try: # This resets the transaction yb.closeRpmDB() + install_errors = [] + for name in groups: + yb.selectGroup(name, ["mandatory", "default"]) + for name, version in projects: if not version: version = "*" - yb.install(pattern="%s-%s" % (name, version)) + pattern = "%s-%s" % (name, version) + try: + yb.install(pattern=pattern) + except YumBaseError as e: + install_errors.append((pattern, str(e))) + + # Were there problems installing these packages? + if install_errors: + raise ProjectsError("The following package(s) had problems: %s" % ",".join(["%s (%s)" % (pattern, err) for pattern, err in install_errors])) + (rc, msg) = yb.buildTransaction() if rc not in [0, 1, 2]: raise ProjectsError("There was a problem depsolving %s: %s" % (projects, msg)) @@ -291,13 +306,15 @@ installed_size += p.po.installedsize return installed_size
    -
    [docs]def projects_depsolve_with_size(yb, projects, with_core=True): +
    [docs]def projects_depsolve_with_size(yb, projects, groups, with_core=True): """Return the dependencies and installed size for a list of projects :param yb: yum base object :type yb: YumBase :param projects: The projects and version globs to find the dependencies for :type projects: List of tuples + :param groups: The groups to include in dependency solving + :type groups: List of str :returns: installed size and a list of NEVRA's of the project and its dependencies :rtype: tuple of (int, list of dicts) :raises: ProjectsError if there was a problem installing something @@ -305,10 +322,23 @@ try: # This resets the transaction yb.closeRpmDB() + install_errors = [] + for name in groups: + yb.selectGroup(name, ["mandatory", "default"]) + for name, version in projects: if not version: version = "*" - yb.install(pattern="%s-%s" % (name, version)) + pattern = "%s-%s" % (name, version) + try: + yb.install(pattern=pattern) + except YumBaseError as e: + install_errors.append((pattern, str(e))) + + # Were there problems installing these packages? + if install_errors: + raise ProjectsError("The following package(s) had problems: %s" % ",".join(["%s (%s)" % (pattern, err) for pattern, err in install_errors])) + if with_core: yb.selectGroup("core", group_package_types=['mandatory', 'default', 'optional']) (rc, msg) = yb.buildTransaction() @@ -368,7 +398,7 @@ modules = sorted(map(yaps_to_project, ybl.available), key=lambda p: p["name"].lower()) # Add the dependency info to each one for module in modules: - module["dependencies"] = projects_depsolve(yb, [(module["name"], "*")]) + module["dependencies"] = projects_depsolve(yb, [(module["name"], "*")], []) return modules
    @@ -618,7 +648,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • pylorax.api »
  • diff --git a/lorax-composer/_modules/pylorax/api/queue.html b/lorax-composer/_modules/pylorax/api/queue.html index ea11e9a8..6dcc98bc 100644 --- a/lorax-composer/_modules/pylorax/api/queue.html +++ b/lorax-composer/_modules/pylorax/api/queue.html @@ -8,7 +8,7 @@ - pylorax.api.queue — Lorax 19.7.18 documentation + pylorax.api.queue — Lorax 19.7.21 documentation @@ -16,7 +16,7 @@ - + @@ -38,7 +38,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • pylorax.api »
  • @@ -83,6 +83,7 @@ from pylorax.api.compose import move_compose_results from pylorax.api.recipes import recipe_from_file +from pylorax.api.timestamp import TS_CREATED, TS_STARTED, TS_FINISHED, write_timestamp, timestamp_dict from pylorax.base import DataHolder from pylorax.creator import run_creator from pylorax.sysutils import joinpaths @@ -159,6 +160,7 @@ make_compose(cfg, os.path.realpath(dst)) log.info("Finished building %s, results are in %s", dst, os.path.realpath(dst)) open(joinpaths(dst, "STATUS"), "w").write("FINISHED\n") + write_timestamp(dst, TS_FINISHED) except Exception: import traceback log.error("traceback: %s", traceback.format_exc()) @@ -166,6 +168,7 @@ # TODO - Write the error message to an ERROR-LOG file to include with the status # log.error("Error running compose: %s", e) open(joinpaths(dst, "STATUS"), "w").write("FAILED\n") + write_timestamp(dst, TS_FINISHED) os.unlink(dst)
    @@ -243,6 +246,7 @@ log.debug("cfg = %s", install_cfg) try: test_path = joinpaths(results_dir, "TEST") + write_timestamp(results_dir, TS_STARTED) if os.path.exists(test_path): # Pretend to run the compose time.sleep(5) @@ -302,15 +306,21 @@ * id - The uuid of the comoposition * queue_status - The final status of the composition (FINISHED or FAILED) - * timestamp - The time of the last status change * compose_type - The type of output generated (tar, iso, etc.) * blueprint - Blueprint name * version - Blueprint version * image_size - Size of the image, if finished. 0 otherwise. + + Various timestamps are also included in the dict. These are all Unix UTC timestamps. + It is possible for these timestamps to not always exist, in which case they will be + None in Python (or null in JSON). The following timestamps are included: + + * job_created - When the user submitted the compose + * job_started - Anaconda started running + * job_finished - Job entered FINISHED or FAILED state """ build_id = os.path.basename(os.path.abspath(results_dir)) status = open(joinpaths(results_dir, "STATUS")).read().strip() - mtime = os.stat(joinpaths(results_dir, "STATUS")).st_mtime blueprint = recipe_from_file(joinpaths(results_dir, "blueprint.toml")) compose_type = get_compose_type(results_dir) @@ -321,9 +331,13 @@ else: image_size = 0 + times = timestamp_dict(results_dir) + return {"id": build_id, "queue_status": status, - "timestamp": mtime, + "job_created": times.get(TS_CREATED), + "job_started": times.get(TS_STARTED), + "job_finished": times.get(TS_FINISHED), "compose_type": compose_type, "blueprint": blueprint["name"], "version": blueprint["version"], @@ -491,7 +505,7 @@ :type cfg: ComposerConfig :param uuid: The UUID of the build :type uuid: str - :returns: dictionary of information about the composition + :returns: dictionary of information about the composition or None :rtype: dict :raises: RuntimeError if there was a problem @@ -507,7 +521,7 @@ """ uuid_dir = joinpaths(cfg.get("composer", "lib_dir"), "results", uuid) if not os.path.exists(uuid_dir): - raise RuntimeError("%s is not a valid build_id" % uuid) + return None # Load the compose configuration cfg_path = joinpaths(uuid_dir, "config.toml") @@ -696,7 +710,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • pylorax.api »
  • diff --git a/lorax-composer/_modules/pylorax/api/recipes.html b/lorax-composer/_modules/pylorax/api/recipes.html index 4b2827f3..d4ab83e2 100644 --- a/lorax-composer/_modules/pylorax/api/recipes.html +++ b/lorax-composer/_modules/pylorax/api/recipes.html @@ -8,7 +8,7 @@ - pylorax.api.recipes — Lorax 19.7.18 documentation + pylorax.api.recipes — Lorax 19.7.21 documentation @@ -16,7 +16,7 @@ - + @@ -38,7 +38,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • pylorax.api »
  • @@ -100,21 +100,24 @@ and adds a .filename property to return the recipe's filename, and a .toml() function to return the recipe as a TOML string. """ - def __init__(self, name, description, version, modules, packages, customizations=None): + def __init__(self, name, description, version, modules, packages, groups, customizations=None): # Check that version is empty or semver compatible if version: semver.Version(version) - # Make sure modules and packages are listed by their case-insensitive names + # Make sure modules, packages, and groups are listed by their case-insensitive names if modules is not None: modules = sorted(modules, key=lambda m: m["name"].lower()) if packages is not None: packages = sorted(packages, key=lambda p: p["name"].lower()) + if groups is not None: + groups = sorted(groups, key=lambda g: g["name"].lower()) dict.__init__(self, name=name, description=description, version=version, modules=modules, packages=packages, + groups=groups, customizations=customizations) # We don't want customizations=None to show up in the TOML so remove it @@ -142,6 +145,11 @@ return [(m["name"], m["version"]) for m in self["modules"] or []]
    @property +
    [docs] def group_names(self): + """Return the names of the groups. Groups do not have versions.""" + return map(lambda g: g["name"], self["groups"] or []) +
    + @property
    [docs] def filename(self): """Return the Recipe's filename @@ -197,21 +205,25 @@ """ module_names = self.module_names package_names = self.package_names + group_names = self.group_names new_modules = [] new_packages = [] + new_groups = [] for dep in deps: if dep["name"] in package_names: new_packages.append(RecipePackage(dep["name"], dep_evra(dep))) elif dep["name"] in module_names: new_modules.append(RecipeModule(dep["name"], dep_evra(dep))) + elif dep["name"] in group_names: + new_groups.append(RecipeGroup(dep["name"])) if "customizations" in self: customizations = self["customizations"] else: customizations = None return Recipe(self["name"], self["description"], self["version"], - new_modules, new_packages, customizations) + new_modules, new_packages, new_groups, customizations)
    [docs]class RecipeModule(dict): def __init__(self, name, version): @@ -220,6 +232,10 @@
    [docs]class RecipePackage(RecipeModule): pass
    +
    [docs]class RecipeGroup(dict): + def __init__(self, name): + dict.__init__(self, name=name) +
    [docs]def recipe_from_file(recipe_path): """Return a recipe file as a Recipe object @@ -263,6 +279,10 @@ packages = [RecipePackage(p.get("name"), p.get("version")) for p in recipe_dict["packages"]] else: packages = [] + if recipe_dict.get("groups"): + groups = [RecipeGroup(g.get("name")) for g in recipe_dict["groups"]] + else: + groups = [] name = recipe_dict["name"] description = recipe_dict["description"] version = recipe_dict.get("version", None) @@ -270,7 +290,7 @@ except KeyError as e: raise RecipeError("There was a problem parsing the recipe: %s" % str(e)) - return Recipe(name, description, version, modules, packages, customizations) + return Recipe(name, description, version, modules, packages, groups, customizations)
    [docs]def gfile(path): """Convert a string path to GFile for use with Git""" @@ -950,6 +970,7 @@ diffs.extend(diff_items("Module", old_recipe["modules"], new_recipe["modules"])) diffs.extend(diff_items("Package", old_recipe["packages"], new_recipe["packages"])) + diffs.extend(diff_items("Group", old_recipe["groups"], new_recipe["groups"])) return diffs
    @@ -985,7 +1006,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • pylorax.api »
  • diff --git a/lorax-composer/_modules/pylorax/api/server.html b/lorax-composer/_modules/pylorax/api/server.html index 638b687f..e5907a32 100644 --- a/lorax-composer/_modules/pylorax/api/server.html +++ b/lorax-composer/_modules/pylorax/api/server.html @@ -8,7 +8,7 @@ - pylorax.api.server — Lorax 19.7.18 documentation + pylorax.api.server — Lorax 19.7.21 documentation @@ -16,7 +16,7 @@ - + @@ -38,7 +38,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • pylorax.api »
  • @@ -118,14 +118,20 @@ "db_supported": true, "db_version": "0", "schema_version": "0", - "backend": "lorax-composer"} + "backend": "lorax-composer", + "msgs": []} + + The 'msgs' field can be a list of strings describing startup problems or status that + should be displayed to the user. eg. if the compose templates are not depsolving properly + the errors will be in 'msgs'. """ return jsonify(backend="lorax-composer", build=vernum, api="0", db_version="0", schema_version="0", - db_supported=True) + db_supported=True, + msgs=server.config["TEMPLATE_ERRORS"]) v0_api(server) @@ -161,7 +167,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • pylorax.api »
  • diff --git a/lorax-composer/_modules/pylorax/api/timestamp.html b/lorax-composer/_modules/pylorax/api/timestamp.html new file mode 100644 index 00000000..cf69e681 --- /dev/null +++ b/lorax-composer/_modules/pylorax/api/timestamp.html @@ -0,0 +1,149 @@ + + + + + + + + + + pylorax.api.timestamp — Lorax 19.7.21 documentation + + + + + + + + + + + + + + +
    +
    +
    +
    + +

    Source code for pylorax.api.timestamp

    +#
    +# Copyright (C) 2018  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 <http://www.gnu.org/licenses/>.
    +#
    +
    +import pytoml as toml
    +import time
    +
    +from pylorax.sysutils import joinpaths
    +
    +TS_CREATED  = "created"
    +TS_STARTED  = "started"
    +TS_FINISHED = "finished"
    +
    +
    [docs]def write_timestamp(destdir, ty): + path = joinpaths(destdir, "times.toml") + + try: + contents = toml.loads(open(path, "r").read()) + except IOError: + contents = toml.loads("") + + if ty == TS_CREATED: + contents[TS_CREATED] = time.time() + elif ty == TS_STARTED: + contents[TS_STARTED] = time.time() + elif ty == TS_FINISHED: + contents[TS_FINISHED] = time.time() + + with open(path, "w") as f: + f.write(toml.dumps(contents).encode("UTF-8")) +
    +
    [docs]def timestamp_dict(destdir): + path = joinpaths(destdir, "times.toml") + + try: + return toml.loads(open(path, "r").read()) + except IOError: + return toml.loads("")
    +
    + +
    +
    +
    +
    +
    + + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/lorax-composer/_modules/pylorax/api/v0.html b/lorax-composer/_modules/pylorax/api/v0.html index 8366550e..24e2741d 100644 --- a/lorax-composer/_modules/pylorax/api/v0.html +++ b/lorax-composer/_modules/pylorax/api/v0.html @@ -8,7 +8,7 @@ - pylorax.api.v0 — Lorax 19.7.18 documentation + pylorax.api.v0 — Lorax 19.7.21 documentation @@ -16,7 +16,7 @@ - + @@ -38,7 +38,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • pylorax.api »
  • @@ -780,14 +780,14 @@ "id": "45502a6d-06e8-48a5-a215-2b4174b3614b", "blueprint": "glusterfs", "queue_status": "WAITING", - "timestamp": 1517362647.4570868, + "job_created": 1517362647.4570868, "version": "0.0.6" }, { "id": "6d292bd0-bec7-4825-8d7d-41ef9c3e4b73", "blueprint": "kubernetes", "queue_status": "WAITING", - "timestamp": 1517362659.0034983, + "job_created": 1517362659.0034983, "version": "0.0.1" } ], @@ -796,7 +796,8 @@ "id": "745712b2-96db-44c0-8014-fe925c35e795", "blueprint": "glusterfs", "queue_status": "RUNNING", - "timestamp": 1517362633.7965999, + "job_created": 1517362633.7965999, + "job_started": 1517362633.8001345, "version": "0.0.6" } ] @@ -815,14 +816,18 @@ "id": "70b84195-9817-4b8a-af92-45e380f39894", "blueprint": "glusterfs", "queue_status": "FINISHED", - "timestamp": 1517351003.8210032, + "job_created": 1517351003.8210032, + "job_started": 1517351003.8230415, + "job_finished": 1517359234.1003145, "version": "0.0.6" }, { "id": "e695affd-397f-4af9-9022-add2636e7459", "blueprint": "glusterfs", "queue_status": "FINISHED", - "timestamp": 1517362289.7193348, + "job_created": 1517362289.7193348, + "job_started": 1517362289.9751132, + "job_finished": 1517363500.1234567, "version": "0.0.6" } ] @@ -841,16 +846,19 @@ "id": "8c8435ef-d6bd-4c68-9bf1-a2ef832e6b1a", "blueprint": "http-server", "queue_status": "FAILED", - "timestamp": 1517523249.9301329, + "job_created": 1517523249.9301329, + "job_started": 1517523249.9314211, + "job_finished": 1517523255.5623411, "version": "0.0.2" } ] } -`/api/v0/compose/status/<uuids>` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +`/api/v0/compose/status/<uuids>[?blueprint=<blueprint_name>&status=<compose_status>&type=<compose_type>]` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - Return the details for each of the comma-separated list of uuids. + Return the details for each of the comma-separated list of uuids. A uuid of '*' will return + details for all composes. Example:: @@ -860,14 +868,18 @@ "id": "8c8435ef-d6bd-4c68-9bf1-a2ef832e6b1a", "blueprint": "http-server", "queue_status": "FINISHED", - "timestamp": 1517523644.2384307, + "job_created": 1517523644.2384307, + "job_started": 1517523644.2551234, + "job_finished": 1517523689.9864314, "version": "0.0.2" }, { "id": "45502a6d-06e8-48a5-a215-2b4174b3614b", "blueprint": "glusterfs", "queue_status": "FINISHED", - "timestamp": 1517363442.188399, + "job_created": 1517363442.188399, + "job_started": 1517363442.325324, + "job_finished": 1517363451.653621, "version": "0.0.6" } ] @@ -1022,16 +1034,19 @@ import pytoml as toml from pylorax.sysutils import joinpaths +from pylorax.api.checkparams import checkparams from pylorax.api.compose import start_build, compose_types from pylorax.api.crossdomain import crossdomain +from pylorax.api.errors import * # pylint: disable=wildcard-import from pylorax.api.projects import projects_list, projects_info, projects_depsolve from pylorax.api.projects import modules_list, modules_info, ProjectsError, repo_to_source from pylorax.api.projects import get_repo_sources, delete_repo_source, source_to_repo, yum_repo_to_file_repo from pylorax.api.queue import queue_status, build_status, uuid_delete, uuid_status, uuid_info from pylorax.api.queue import uuid_tar, uuid_image, uuid_cancel, uuid_log -from pylorax.api.recipes import list_branch_files, read_recipe_commit, recipe_filename, list_commits +from pylorax.api.recipes import RecipeError, list_branch_files, read_recipe_commit, recipe_filename, list_commits from pylorax.api.recipes import recipe_from_dict, recipe_from_toml, commit_recipe, delete_recipe, revert_recipe from pylorax.api.recipes import tag_recipe_commit, recipe_diff +from pylorax.api.regexes import VALID_API_STRING from pylorax.api.workspace import workspace_read, workspace_write, workspace_delete from pylorax.api.yumbase import update_metadata @@ -1051,6 +1066,15 @@ """ return iterable[offset:][:limit] +
    [docs]def blueprint_exists(api, branch, blueprint_name): + try: + with api.config["GITLOCK"].lock: + read_recipe_commit(api.config["GITLOCK"].repo, branch, blueprint_name) + + return True + except RecipeError: + return False +
    [docs]def v0_api(api): # Note that Sphinx will not generate documentations for any of these. @api.route("/api/v0/blueprints/list") @@ -1058,22 +1082,36 @@ def v0_blueprints_list(): """List the available blueprints on a branch.""" branch = request.args.get("branch", "master") + if VALID_API_STRING.match(branch) is None: + return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in branch argument"}]), 400 + try: limit = int(request.args.get("limit", "20")) offset = int(request.args.get("offset", "0")) except ValueError as e: - return jsonify(status=False, errors=[str(e)]), 400 + return jsonify(status=False, errors=[{"id": BAD_LIMIT_OR_OFFSET, "msg": str(e)}]), 400 with api.config["GITLOCK"].lock: blueprints = take_limits(map(lambda f: f[:-5], list_branch_files(api.config["GITLOCK"].repo, branch)), offset, limit) return jsonify(blueprints=blueprints, limit=limit, offset=offset, total=len(blueprints)) + @api.route("/api/v0/blueprints/info", defaults={'blueprint_names': ""}) @api.route("/api/v0/blueprints/info/<blueprint_names>") @crossdomain(origin="*") + @checkparams([("blueprint_names", "", "no blueprint names given")]) def v0_blueprints_info(blueprint_names): """Return the contents of the blueprint, or a list of blueprints""" + if VALID_API_STRING.match(blueprint_names) is None: + return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in API path"}]), 400 + branch = request.args.get("branch", "master") + if VALID_API_STRING.match(branch) is None: + return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in branch argument"}]), 400 + out_fmt = request.args.get("format", "json") + if VALID_API_STRING.match(out_fmt) is None: + return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in format argument"}]), 400 + blueprints = [] changes = [] errors = [] @@ -1099,7 +1137,7 @@ if not ws_blueprint and not git_blueprint: # Neither blueprint, return an error - errors.append("%s: %s" % (blueprint_name, ", ".join(exceptions))) + errors.append({"id": UNKNOWN_BLUEPRINT, "msg": "%s: %s" % (blueprint_name, ", ".join(exceptions))}) elif ws_blueprint and not git_blueprint: # No git blueprint, return the workspace blueprint changes.append({"name":blueprint_name, "changed":True}) @@ -1116,7 +1154,6 @@ # Sort all the results by case-insensitive blueprint name changes = sorted(changes, key=lambda c: c["name"].lower()) blueprints = sorted(blueprints, key=lambda r: r["name"].lower()) - errors = sorted(errors, key=lambda e: e.lower()) if out_fmt == "toml": # With TOML output we just want to dump the raw blueprint, skipping the rest. @@ -1124,32 +1161,44 @@ else: return jsonify(changes=changes, blueprints=blueprints, errors=errors) + @api.route("/api/v0/blueprints/changes", defaults={'blueprint_names': ""}) @api.route("/api/v0/blueprints/changes/<blueprint_names>") @crossdomain(origin="*") + @checkparams([("blueprint_names", "", "no blueprint names given")]) def v0_blueprints_changes(blueprint_names): """Return the changes to a blueprint or list of blueprints""" + if VALID_API_STRING.match(blueprint_names) is None: + return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in API path"}]), 400 + branch = request.args.get("branch", "master") + if VALID_API_STRING.match(branch) is None: + return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in branch argument"}]), 400 + try: limit = int(request.args.get("limit", "20")) offset = int(request.args.get("offset", "0")) except ValueError as e: - return jsonify(status=False, errors=[str(e)]), 400 + return jsonify(status=False, errors=[{"id": BAD_LIMIT_OR_OFFSET, "msg": str(e)}]), 400 blueprints = [] errors = [] for blueprint_name in [n.strip() for n in blueprint_names.split(",")]: filename = recipe_filename(blueprint_name) + + if not blueprint_exists(api, branch, blueprint_name): + errors.append({"id": UNKNOWN_BLUEPRINT, "msg": "Unknown blueprint name: %s" % blueprint_name}) + continue + try: with api.config["GITLOCK"].lock: commits = take_limits(list_commits(api.config["GITLOCK"].repo, branch, filename), offset, limit) except Exception as e: - errors.append("%s: %s" % (blueprint_name, str(e))) + errors.append({"id": BLUEPRINTS_ERROR, "msg": "%s: %s" % (blueprint_name, str(e))}) log.error("(v0_blueprints_changes) %s", str(e)) else: blueprints.append({"name":blueprint_name, "changes":commits, "total":len(commits)}) blueprints = sorted(blueprints, key=lambda r: r["name"].lower()) - errors = sorted(errors, key=lambda e: e.lower()) return jsonify(blueprints=blueprints, errors=errors, offset=offset, limit=limit) @@ -1158,12 +1207,18 @@ def v0_blueprints_new(): """Commit a new blueprint""" branch = request.args.get("branch", "master") + if VALID_API_STRING.match(branch) is None: + return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in branch argument"}]), 400 + try: if request.headers['Content-Type'] == "text/x-toml": blueprint = recipe_from_toml(request.data) else: blueprint = recipe_from_dict(request.get_json(cache=False)) + if VALID_API_STRING.match(blueprint["name"]) is None: + return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in API path"}]), 400 + with api.config["GITLOCK"].lock: commit_recipe(api.config["GITLOCK"].repo, branch, blueprint) @@ -1172,21 +1227,29 @@ workspace_write(api.config["GITLOCK"].repo, branch, blueprint) except Exception as e: log.error("(v0_blueprints_new) %s", str(e)) - return jsonify(status=False, errors=[str(e)]), 400 + return jsonify(status=False, errors=[{"id": BLUEPRINTS_ERROR, "msg": str(e)}]), 400 else: return jsonify(status=True) + @api.route("/api/v0/blueprints/delete", defaults={'blueprint_name': ""}, methods=["DELETE"]) @api.route("/api/v0/blueprints/delete/<blueprint_name>", methods=["DELETE"]) @crossdomain(origin="*") + @checkparams([("blueprint_name", "", "no blueprint name given")]) def v0_blueprints_delete(blueprint_name): """Delete a blueprint from git""" + if VALID_API_STRING.match(blueprint_name) is None: + return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in API path"}]), 400 + branch = request.args.get("branch", "master") + if VALID_API_STRING.match(branch) is None: + return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in branch argument"}]), 400 + try: with api.config["GITLOCK"].lock: delete_recipe(api.config["GITLOCK"].repo, branch, blueprint_name) except Exception as e: log.error("(v0_blueprints_delete) %s", str(e)) - return jsonify(status=False, errors=[str(e)]), 400 + return jsonify(status=False, errors=[{"id": BLUEPRINTS_ERROR, "msg": str(e)}]), 400 else: return jsonify(status=True) @@ -1195,39 +1258,63 @@ def v0_blueprints_workspace(): """Write a blueprint to the workspace""" branch = request.args.get("branch", "master") + if VALID_API_STRING.match(branch) is None: + return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in branch argument"}]), 400 + try: if request.headers['Content-Type'] == "text/x-toml": blueprint = recipe_from_toml(request.data) else: blueprint = recipe_from_dict(request.get_json(cache=False)) + if VALID_API_STRING.match(blueprint["name"]) is None: + return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in API path"}]), 400 + with api.config["GITLOCK"].lock: workspace_write(api.config["GITLOCK"].repo, branch, blueprint) except Exception as e: log.error("(v0_blueprints_workspace) %s", str(e)) - return jsonify(status=False, errors=[str(e)]), 400 + return jsonify(status=False, errors=[{"id": BLUEPRINTS_ERROR, "msg": str(e)}]), 400 else: return jsonify(status=True) + @api.route("/api/v0/blueprints/workspace", defaults={'blueprint_name': ""}, methods=["DELETE"]) @api.route("/api/v0/blueprints/workspace/<blueprint_name>", methods=["DELETE"]) @crossdomain(origin="*") + @checkparams([("blueprint_name", "", "no blueprint name given")]) def v0_blueprints_delete_workspace(blueprint_name): """Delete a blueprint from the workspace""" + if VALID_API_STRING.match(blueprint_name) is None: + return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in API path"}]), 400 + branch = request.args.get("branch", "master") + if VALID_API_STRING.match(branch) is None: + return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in branch argument"}]), 400 + try: with api.config["GITLOCK"].lock: workspace_delete(api.config["GITLOCK"].repo, branch, blueprint_name) except Exception as e: log.error("(v0_blueprints_delete_workspace) %s", str(e)) - return jsonify(status=False, error=[str(e)]), 400 + return jsonify(status=False, errors=[{"id": BLUEPRINTS_ERROR, "msg": str(e)}]), 400 else: return jsonify(status=True) + @api.route("/api/v0/blueprints/undo", defaults={'blueprint_name': "", 'commit': ""}, methods=["POST"]) + @api.route("/api/v0/blueprints/undo/<blueprint_name>", defaults={'commit': ""}, methods=["POST"]) @api.route("/api/v0/blueprints/undo/<blueprint_name>/<commit>", methods=["POST"]) @crossdomain(origin="*") + @checkparams([("blueprint_name", "", "no blueprint name given"), + ("commit", "", "no commit ID given")]) def v0_blueprints_undo(blueprint_name, commit): """Undo changes to a blueprint by reverting to a previous commit.""" + if VALID_API_STRING.match(blueprint_name) is None: + return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in API path"}]), 400 + branch = request.args.get("branch", "master") + if VALID_API_STRING.match(branch) is None: + return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in branch argument"}]), 400 + try: with api.config["GITLOCK"].lock: revert_recipe(api.config["GITLOCK"].repo, branch, blueprint_name, commit) @@ -1237,29 +1324,50 @@ workspace_write(api.config["GITLOCK"].repo, branch, blueprint) except Exception as e: log.error("(v0_blueprints_undo) %s", str(e)) - return jsonify(status=False, errors=[str(e)]), 400 + return jsonify(status=False, errors=[{"id": UNKNOWN_COMMIT, "msg": str(e)}]), 400 else: return jsonify(status=True) + @api.route("/api/v0/blueprints/tag", defaults={'blueprint_name': ""}, methods=["POST"]) @api.route("/api/v0/blueprints/tag/<blueprint_name>", methods=["POST"]) @crossdomain(origin="*") + @checkparams([("blueprint_name", "", "no blueprint name given")]) def v0_blueprints_tag(blueprint_name): """Tag a blueprint's latest blueprint commit as a 'revision'""" + if VALID_API_STRING.match(blueprint_name) is None: + return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in API path"}]), 400 + branch = request.args.get("branch", "master") + if VALID_API_STRING.match(branch) is None: + return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in branch argument"}]), 400 + try: with api.config["GITLOCK"].lock: tag_recipe_commit(api.config["GITLOCK"].repo, branch, blueprint_name) except Exception as e: log.error("(v0_blueprints_tag) %s", str(e)) - return jsonify(status=False, errors=[str(e)]), 400 + return jsonify(status=False, errors=[{"id": BLUEPRINTS_ERROR, "msg": str(e)}]), 400 else: return jsonify(status=True) + @api.route("/api/v0/blueprints/diff", defaults={'blueprint_name': "", 'from_commit': "", 'to_commit': ""}) + @api.route("/api/v0/blueprints/diff/<blueprint_name>", defaults={'from_commit': "", 'to_commit': ""}) + @api.route("/api/v0/blueprints/diff/<blueprint_name>/<from_commit>", defaults={'to_commit': ""}) @api.route("/api/v0/blueprints/diff/<blueprint_name>/<from_commit>/<to_commit>") @crossdomain(origin="*") + @checkparams([("blueprint_name", "", "no blueprint name given"), + ("from_commit", "", "no from commit ID given"), + ("to_commit", "", "no to commit ID given")]) def v0_blueprints_diff(blueprint_name, from_commit, to_commit): """Return the differences between two commits of a blueprint""" + for s in [blueprint_name, from_commit, to_commit]: + if VALID_API_STRING.match(s) is None: + return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in API path"}]), 400 + branch = request.args.get("branch", "master") + if VALID_API_STRING.match(branch) is None: + return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in branch argument"}]), 400 + try: if from_commit == "NEWEST": with api.config["GITLOCK"].lock: @@ -1269,7 +1377,7 @@ old_blueprint = read_recipe_commit(api.config["GITLOCK"].repo, branch, blueprint_name, from_commit) except Exception as e: log.error("(v0_blueprints_diff) %s", str(e)) - return jsonify(status=False, errors=[str(e)]), 400 + return jsonify(status=False, errors=[{"id": UNKNOWN_COMMIT, "msg": str(e)}]), 400 try: if to_commit == "WORKSPACE": @@ -1287,17 +1395,28 @@ new_blueprint = read_recipe_commit(api.config["GITLOCK"].repo, branch, blueprint_name, to_commit) except Exception as e: log.error("(v0_blueprints_diff) %s", str(e)) - return jsonify(status=False, errors=[str(e)]), 400 + return jsonify(status=False, errors=[{"id": UNKNOWN_COMMIT, "msg": str(e)}]), 400 diff = recipe_diff(old_blueprint, new_blueprint) return jsonify(diff=diff) + @api.route("/api/v0/blueprints/freeze", defaults={'blueprint_names': ""}) @api.route("/api/v0/blueprints/freeze/<blueprint_names>") @crossdomain(origin="*") + @checkparams([("blueprint_names", "", "no blueprint names given")]) def v0_blueprints_freeze(blueprint_names): """Return the blueprint with the exact modules and packages selected by depsolve""" + if VALID_API_STRING.match(blueprint_names) is None: + return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in API path"}]), 400 + branch = request.args.get("branch", "master") + if VALID_API_STRING.match(branch) is None: + return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in branch argument"}]), 400 + out_fmt = request.args.get("format", "json") + if VALID_API_STRING.match(out_fmt) is None: + return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in format argument"}]), 400 + blueprints = [] errors = [] for blueprint_name in [n.strip() for n in sorted(blueprint_names.split(","), key=lambda n: n.lower())]: @@ -1316,12 +1435,12 @@ with api.config["GITLOCK"].lock: blueprint = read_recipe_commit(api.config["GITLOCK"].repo, branch, blueprint_name) except Exception as e: - errors.append("%s: %s" % (blueprint_name, str(e))) + errors.append({"id": BLUEPRINTS_ERROR, "msg": "%s: %s" % (blueprint_name, str(e))}) log.error("(v0_blueprints_freeze) %s", str(e)) # No blueprint found, skip it. if not blueprint: - errors.append("%s: blueprint_not_found" % (blueprint_name)) + errors.append({"id": UNKNOWN_BLUEPRINT, "msg": "%s: blueprint_not_found" % blueprint_name}) continue # Combine modules and packages and depsolve the list @@ -1332,9 +1451,9 @@ deps = [] try: with api.config["YUMLOCK"].lock: - deps = projects_depsolve(api.config["YUMLOCK"].yb, projects) + deps = projects_depsolve(api.config["YUMLOCK"].yb, projects, blueprint.group_names) except ProjectsError as e: - errors.append("%s: %s" % (blueprint_name, str(e))) + errors.append({"id": BLUEPRINTS_ERROR, "msg": "%s: %s" % (blueprint_name, str(e))}) log.error("(v0_blueprints_freeze) %s", str(e)) blueprints.append({"blueprint": blueprint.freeze(deps)}) @@ -1345,11 +1464,19 @@ else: return jsonify(blueprints=blueprints, errors=errors) + @api.route("/api/v0/blueprints/depsolve", defaults={'blueprint_names': ""}) @api.route("/api/v0/blueprints/depsolve/<blueprint_names>") @crossdomain(origin="*") + @checkparams([("blueprint_names", "", "no blueprint names given")]) def v0_blueprints_depsolve(blueprint_names): """Return the dependencies for a blueprint""" + if VALID_API_STRING.match(blueprint_names) is None: + return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in API path"}]), 400 + branch = request.args.get("branch", "master") + if VALID_API_STRING.match(branch) is None: + return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in branch argument"}]), 400 + blueprints = [] errors = [] for blueprint_name in [n.strip() for n in sorted(blueprint_names.split(","), key=lambda n: n.lower())]: @@ -1368,12 +1495,12 @@ with api.config["GITLOCK"].lock: blueprint = read_recipe_commit(api.config["GITLOCK"].repo, branch, blueprint_name) except Exception as e: - errors.append("%s: %s" % (blueprint_name, str(e))) + errors.append({"id": BLUEPRINTS_ERROR, "msg": "%s: %s" % (blueprint_name, str(e))}) log.error("(v0_blueprints_depsolve) %s", str(e)) # No blueprint found, skip it. if not blueprint: - errors.append("%s: blueprint not found" % blueprint_name) + errors.append({"id": UNKNOWN_BLUEPRINT, "msg": "%s: blueprint not found" % blueprint_name}) continue # Combine modules and packages and depsolve the list @@ -1383,9 +1510,9 @@ deps = [] try: with api.config["YUMLOCK"].lock: - deps = projects_depsolve(api.config["YUMLOCK"].yb, projects) + deps = projects_depsolve(api.config["YUMLOCK"].yb, projects, blueprint.group_names) except ProjectsError as e: - errors.append("%s: %s" % (blueprint_name, str(e))) + errors.append({"id": BLUEPRINTS_ERROR, "msg": "%s: %s" % (blueprint_name, str(e))}) log.error("(v0_blueprints_depsolve) %s", str(e)) # Get the NEVRA's of the modules and projects, add as "modules" @@ -1407,41 +1534,61 @@ limit = int(request.args.get("limit", "20")) offset = int(request.args.get("offset", "0")) except ValueError as e: - return jsonify(status=False, errors=[str(e)]), 400 + return jsonify(status=False, errors=[{"id": BAD_LIMIT_OR_OFFSET, "msg": str(e)}]), 400 try: with api.config["YUMLOCK"].lock: available = projects_list(api.config["YUMLOCK"].yb) except ProjectsError as e: log.error("(v0_projects_list) %s", str(e)) - return jsonify(status=False, errors=[str(e)]), 400 + return jsonify(status=False, errors=[{"id": PROJECTS_ERROR, "msg": str(e)}]), 400 projects = take_limits(available, offset, limit) return jsonify(projects=projects, offset=offset, limit=limit, total=len(available)) + @api.route("/api/v0/projects/info", defaults={'project_names': ""}) @api.route("/api/v0/projects/info/<project_names>") @crossdomain(origin="*") + @checkparams([("project_names", "", "no project names given")]) def v0_projects_info(project_names): """Return detailed information about the listed projects""" + if VALID_API_STRING.match(project_names) is None: + return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in API path"}]), 400 + try: with api.config["YUMLOCK"].lock: projects = projects_info(api.config["YUMLOCK"].yb, project_names.split(",")) except ProjectsError as e: log.error("(v0_projects_info) %s", str(e)) - return jsonify(status=False, errors=[str(e)]), 400 + return jsonify(status=False, errors=[{"id": PROJECTS_ERROR, "msg": str(e)}]), 400 + + if not projects: + msg = "one of the requested projects does not exist: %s" % project_names + log.error("(v0_projects_info) %s", msg) + return jsonify(status=False, errors=[{"id": UNKNOWN_PROJECT, "msg": msg}]), 400 return jsonify(projects=projects) + @api.route("/api/v0/projects/depsolve", defaults={'project_names': ""}) @api.route("/api/v0/projects/depsolve/<project_names>") @crossdomain(origin="*") + @checkparams([("project_names", "", "no project names given")]) def v0_projects_depsolve(project_names): """Return detailed information about the listed projects""" + if VALID_API_STRING.match(project_names) is None: + return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in API path"}]), 400 + try: with api.config["YUMLOCK"].lock: - deps = projects_depsolve(api.config["YUMLOCK"].yb, [(n, "*") for n in project_names.split(",")]) + deps = projects_depsolve(api.config["YUMLOCK"].yb, [(n, "*") for n in project_names.split(",")], []) except ProjectsError as e: log.error("(v0_projects_depsolve) %s", str(e)) - return jsonify(status=False, errors=[str(e)]), 400 + return jsonify(status=False, errors=[{"id": PROJECTS_ERROR, "msg": str(e)}]), 400 + + if not deps: + msg = "one of the requested projects does not exist: %s" % project_names + log.error("(v0_projects_depsolve) %s", msg) + return jsonify(status=False, errors=[{"id": UNKNOWN_PROJECT, "msg": msg}]), 400 return jsonify(projects=deps) @@ -1454,11 +1601,18 @@ sources = sorted([r.id for r in repos]) return jsonify(sources=sources) + @api.route("/api/v0/projects/source/info", defaults={'source_names': ""}) @api.route("/api/v0/projects/source/info/<source_names>") @crossdomain(origin="*") + @checkparams([("source_names", "", "no source names given")]) def v0_projects_source_info(source_names): """Return detailed info about the list of sources""" + if VALID_API_STRING.match(source_names) is None: + return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in API path"}]), 400 + out_fmt = request.args.get("format", "json") + if VALID_API_STRING.match(out_fmt) is None: + return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in format argument"}]), 400 # Return info on all of the sources if source_names == "*": @@ -1472,7 +1626,7 @@ with api.config["YUMLOCK"].lock: repo = api.config["YUMLOCK"].yb.repos.repos.get(source, None) if not repo: - errors.append("%s is not a valid source" % source) + errors.append({"id": UNKNOWN_SOURCE, "msg": "%s is not a valid source" % source}) continue sources[repo.id] = repo_to_source(repo, repo.id in system_sources) @@ -1496,7 +1650,7 @@ system_sources = get_repo_sources("/etc/yum.repos.d/*.repo") if source["name"] in system_sources: - return jsonify(status=False, errors=["%s is a system source, it cannot be deleted." % source["name"]]), 400 + return jsonify(status=False, errors=[{"id": SYSTEM_SOURCE, "msg": "%s is a system source, it cannot be changed." % source["name"]}]), 400 try: # Delete it from yum (if it exists) and replace it with the new one @@ -1544,17 +1698,22 @@ log.info("Updating repository metadata after adding %s failed", source["name"]) update_metadata(yb) - return jsonify(status=False, errors=[str(e)]), 400 + return jsonify(status=False, errors=[{"id": PROJECTS_ERROR, "msg": str(e)}]), 400 return jsonify(status=True) + @api.route("/api/v0/projects/source/delete", defaults={'source_name': ""}, methods=["DELETE"]) @api.route("/api/v0/projects/source/delete/<source_name>", methods=["DELETE"]) @crossdomain(origin="*") + @checkparams([("source_name", "", "no source name given")]) def v0_projects_source_delete(source_name): """Delete the named source and return a status response""" + if VALID_API_STRING.match(source_name) is None: + return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in API path"}]), 400 + system_sources = get_repo_sources("/etc/yum.repos.d/*.repo") if source_name in system_sources: - return jsonify(status=False, errors=["%s is a system source, it cannot be deleted." % source_name]), 400 + return jsonify(status=False, errors=[{"id": SYSTEM_SOURCE, "msg": "%s is a system source, it cannot be deleted." % source_name}]), 400 share_dir = api.config["COMPOSER_CFG"].get("composer", "repo_dir") try: # Remove the file entry for the source @@ -1574,7 +1733,7 @@ except ProjectsError as e: log.error("(v0_projects_source_delete) %s", str(e)) - return jsonify(status=False, errors=[str(e)]), 400 + return jsonify(status=False, errors=[{"id": UNKNOWN_SOURCE, "msg": str(e)}]), 400 return jsonify(status=True) @@ -1583,11 +1742,14 @@ @crossdomain(origin="*") def v0_modules_list(module_names=None): """List available modules, filtering by module_names""" + if module_names and VALID_API_STRING.match(module_names) is None: + return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in API path"}]), 400 + try: limit = int(request.args.get("limit", "20")) offset = int(request.args.get("offset", "0")) except ValueError as e: - return jsonify(status=False, errors=[str(e)]), 400 + return jsonify(status=False, errors=[{"id": BAD_LIMIT_OR_OFFSET, "msg": str(e)}]), 400 if module_names: module_names = module_names.split(",") @@ -1597,21 +1759,35 @@ available = modules_list(api.config["YUMLOCK"].yb, module_names) except ProjectsError as e: log.error("(v0_modules_list) %s", str(e)) - return jsonify(status=False, errors=[str(e)]), 400 + return jsonify(status=False, errors=[{"id": MODULES_ERROR, "msg": str(e)}]), 400 + + if module_names and not available: + msg = "one of the requested modules does not exist: %s" % module_names + log.error("(v0_modules_list) %s", msg) + return jsonify(status=False, errors=[{"id": UNKNOWN_MODULE, "msg": msg}]), 400 modules = take_limits(available, offset, limit) return jsonify(modules=modules, offset=offset, limit=limit, total=len(available)) + @api.route("/api/v0/modules/info", defaults={'module_names': ""}) @api.route("/api/v0/modules/info/<module_names>") @crossdomain(origin="*") + @checkparams([("module_names", "", "no module names given")]) def v0_modules_info(module_names): """Return detailed information about the listed modules""" + if VALID_API_STRING.match(module_names) is None: + return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in API path"}]), 400 try: with api.config["YUMLOCK"].lock: modules = modules_info(api.config["YUMLOCK"].yb, module_names.split(",")) except ProjectsError as e: log.error("(v0_modules_info) %s", str(e)) - return jsonify(status=False, errors=[str(e)]), 400 + return jsonify(status=False, errors=[{"id": MODULES_ERROR, "msg": str(e)}]), 400 + + if not modules: + msg = "one of the requested modules does not exist: %s" % module_names + log.error("(v0_modules_info) %s", msg) + return jsonify(status=False, errors=[{"id": UNKNOWN_MODULE, "msg": msg}]), 400 return jsonify(modules=modules) @@ -1636,10 +1812,10 @@ errors = [] if not compose: - return jsonify(status=False, errors=["Missing POST body"]), 400 + return jsonify(status=False, errors=[{"id": MISSING_POST, "msg": "Missing POST body"}]), 400 if "blueprint_name" not in compose: - errors.append("No 'blueprint_name' in the JSON request") + errors.append({"id": UNKNOWN_BLUEPRINT,"msg": "No 'blueprint_name' in the JSON request"}) else: blueprint_name = compose["blueprint_name"] @@ -1649,10 +1825,13 @@ branch = compose["branch"] if "compose_type" not in compose: - errors.append("No 'compose_type' in the JSON request") + errors.append({"id": BAD_COMPOSE_TYPE, "msg": "No 'compose_type' in the JSON request"}) else: compose_type = compose["compose_type"] + if VALID_API_STRING.match(blueprint_name) is None: + errors.append({"id": INVALID_CHARS, "msg": "Invalid characters in API path"}) + if errors: return jsonify(status=False, errors=errors), 400 @@ -1660,7 +1839,10 @@ build_id = start_build(api.config["COMPOSER_CFG"], api.config["YUMLOCK"], api.config["GITLOCK"], branch, blueprint_name, compose_type, test_mode) except Exception as e: - return jsonify(status=False, errors=[str(e)]), 400 + if "Invalid compose type" in str(e): + return jsonify(status=False, errors=[{"id": BAD_COMPOSE_TYPE, "msg": str(e)}]), 400 + else: + return jsonify(status=False, errors=[{"id": BUILD_FAILED, "msg": str(e)}]), 400 return jsonify(status=True, build_id=build_id) @@ -1692,152 +1874,227 @@ """Return the list of failed composes""" return jsonify(failed=build_status(api.config["COMPOSER_CFG"], "FAILED")) + @api.route("/api/v0/compose/status", defaults={'uuids': ""}) @api.route("/api/v0/compose/status/<uuids>") @crossdomain(origin="*") + @checkparams([("uuids", "", "no UUIDs given")]) def v0_compose_status(uuids): """Return the status of the listed uuids""" + if VALID_API_STRING.match(uuids) is None: + return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in API path"}]), 400 + + blueprint = request.args.get("blueprint", None) + status = request.args.get("status", None) + compose_type = request.args.get("type", None) + results = [] - for uuid in [n.strip().lower() for n in uuids.split(",")]: - details = uuid_status(api.config["COMPOSER_CFG"], uuid) - if details is not None: - results.append(details) + errors = [] - return jsonify(uuids=results) + if uuids.strip() == '*': + queue_status_dict = queue_status(api.config["COMPOSER_CFG"]) + queue_new = queue_status_dict["new"] + queue_running = queue_status_dict["run"] + candidates = queue_new + queue_running + build_status(api.config["COMPOSER_CFG"]) + else: + candidates = [] + for uuid in [n.strip().lower() for n in uuids.split(",")]: + details = uuid_status(api.config["COMPOSER_CFG"], uuid) + if details is None: + errors.append({"id": UNKNOWN_UUID, "msg": "%s is not a valid build uuid" % uuid}) + else: + candidates.append(details) + for details in candidates: + if blueprint is not None and details['blueprint'] != blueprint: + continue + + if status is not None and details['queue_status'] != status: + continue + + if compose_type is not None and details['compose_type'] != compose_type: + continue + + results.append(details) + + return jsonify(uuids=results, errors=errors) + + @api.route("/api/v0/compose/cancel", defaults={'uuid': ""}, methods=["DELETE"]) @api.route("/api/v0/compose/cancel/<uuid>", methods=["DELETE"]) @crossdomain(origin="*") + @checkparams([("uuid", "", "no UUID given")]) def v0_compose_cancel(uuid): """Cancel a running compose and delete its results directory""" + if VALID_API_STRING.match(uuid) is None: + return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in API path"}]), 400 + status = uuid_status(api.config["COMPOSER_CFG"], uuid) if status is None: - return jsonify(status=False, errors=["%s is not a valid build uuid" % uuid]), 400 + return jsonify(status=False, errors=[{"id": UNKNOWN_UUID, "msg": "%s is not a valid build uuid" % uuid}]), 400 if status["queue_status"] not in ["WAITING", "RUNNING"]: - return jsonify(status=False, errors=["Build %s is not in WAITING or RUNNING." % uuid]) + return jsonify(status=False, errors=[{"id": BUILD_IN_WRONG_STATE, "msg": "Build %s is not in WAITING or RUNNING." % uuid}]) try: uuid_cancel(api.config["COMPOSER_CFG"], uuid) except Exception as e: - return jsonify(status=False, errors=["%s: %s" % (uuid, str(e))]),400 + return jsonify(status=False, errors=[{"id": COMPOSE_ERROR, "msg": "%s: %s" % (uuid, str(e))}]),400 else: return jsonify(status=True, uuid=uuid) + @api.route("/api/v0/compose/delete", defaults={'uuids': ""}, methods=["DELETE"]) @api.route("/api/v0/compose/delete/<uuids>", methods=["DELETE"]) @crossdomain(origin="*") + @checkparams([("uuids", "", "no UUIDs given")]) def v0_compose_delete(uuids): """Delete the compose results for the listed uuids""" + if VALID_API_STRING.match(uuids) is None: + return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in API path"}]), 400 + results = [] errors = [] for uuid in [n.strip().lower() for n in uuids.split(",")]: status = uuid_status(api.config["COMPOSER_CFG"], uuid) if status is None: - errors.append("%s is not a valid build uuid" % uuid) + errors.append({"id": UNKNOWN_UUID, "msg": "%s is not a valid build uuid" % uuid}) elif status["queue_status"] not in ["FINISHED", "FAILED"]: - errors.append("Build %s is not in FINISHED or FAILED." % uuid) + errors.append({"id": BUILD_IN_WRONG_STATE, "msg": "Build %s is not in FINISHED or FAILED." % uuid}) else: try: uuid_delete(api.config["COMPOSER_CFG"], uuid) except Exception as e: - errors.append("%s: %s" % (uuid, str(e))) + errors.append({"id": COMPOSE_ERROR, "msg": "%s: %s" % (uuid, str(e))}) else: results.append({"uuid":uuid, "status":True}) return jsonify(uuids=results, errors=errors) + @api.route("/api/v0/compose/info", defaults={'uuid': ""}) @api.route("/api/v0/compose/info/<uuid>") @crossdomain(origin="*") + @checkparams([("uuid", "", "no UUID given")]) def v0_compose_info(uuid): """Return detailed info about a compose""" + if VALID_API_STRING.match(uuid) is None: + return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in API path"}]), 400 + try: info = uuid_info(api.config["COMPOSER_CFG"], uuid) except Exception as e: - return jsonify(status=False, errors=[str(e)]), 400 + return jsonify(status=False, errors=[{"id": COMPOSE_ERROR, "msg": str(e)}]), 400 - return jsonify(**info) + if info is None: + return jsonify(status=False, errors=[{"id": UNKNOWN_UUID, "msg": "%s is not a valid build uuid" % uuid}]), 400 + else: + return jsonify(**info) + @api.route("/api/v0/compose/metadata", defaults={'uuid': ""}) @api.route("/api/v0/compose/metadata/<uuid>") @crossdomain(origin="*") + @checkparams([("uuid","", "no UUID given")]) def v0_compose_metadata(uuid): """Return a tar of the metadata for the build""" + if VALID_API_STRING.match(uuid) is None: + return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in API path"}]), 400 + status = uuid_status(api.config["COMPOSER_CFG"], uuid) if status is None: - return jsonify(status=False, errors=["%s is not a valid build uuid" % uuid]), 400 + return jsonify(status=False, errors=[{"id": UNKNOWN_UUID, "msg": "%s is not a valid build uuid" % uuid}]), 400 if status["queue_status"] not in ["FINISHED", "FAILED"]: - return jsonify(status=False, errors=["Build %s not in FINISHED or FAILED state." % uuid]), 400 + return jsonify(status=False, errors=[{"id": BUILD_IN_WRONG_STATE, "msg": "Build %s not in FINISHED or FAILED state." % uuid}]), 400 else: return Response(uuid_tar(api.config["COMPOSER_CFG"], uuid, metadata=True, image=False, logs=False), mimetype="application/x-tar", headers=[("Content-Disposition", "attachment; filename=%s-metadata.tar;" % uuid)], direct_passthrough=True) + @api.route("/api/v0/compose/results", defaults={'uuid': ""}) @api.route("/api/v0/compose/results/<uuid>") @crossdomain(origin="*") + @checkparams([("uuid","", "no UUID given")]) def v0_compose_results(uuid): """Return a tar of the metadata and the results for the build""" + if VALID_API_STRING.match(uuid) is None: + return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in API path"}]), 400 + status = uuid_status(api.config["COMPOSER_CFG"], uuid) if status is None: - return jsonify(status=False, errors=["%s is not a valid build uuid" % uuid]), 400 + return jsonify(status=False, errors=[{"id": UNKNOWN_UUID, "msg": "%s is not a valid build uuid" % uuid}]), 400 elif status["queue_status"] not in ["FINISHED", "FAILED"]: - return jsonify(status=False, errors=["Build %s not in FINISHED or FAILED state." % uuid]), 400 + return jsonify(status=False, errors=[{"id": BUILD_IN_WRONG_STATE, "msg": "Build %s not in FINISHED or FAILED state." % uuid}]), 400 else: return Response(uuid_tar(api.config["COMPOSER_CFG"], uuid, metadata=True, image=True, logs=True), mimetype="application/x-tar", headers=[("Content-Disposition", "attachment; filename=%s.tar;" % uuid)], direct_passthrough=True) + @api.route("/api/v0/compose/logs", defaults={'uuid': ""}) @api.route("/api/v0/compose/logs/<uuid>") @crossdomain(origin="*") + @checkparams([("uuid","", "no UUID given")]) def v0_compose_logs(uuid): """Return a tar of the metadata for the build""" + if VALID_API_STRING.match(uuid) is None: + return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in API path"}]), 400 + status = uuid_status(api.config["COMPOSER_CFG"], uuid) if status is None: - return jsonify(status=False, errors=["%s is not a valid build uuid" % uuid]), 400 + return jsonify(status=False, errors=[{"id": UNKNOWN_UUID, "msg": "%s is not a valid build uuid" % uuid}]), 400 elif status["queue_status"] not in ["FINISHED", "FAILED"]: - return jsonify(status=False, errors=["Build %s not in FINISHED or FAILED state." % uuid]), 400 + return jsonify(status=False, errors=[{"id": BUILD_IN_WRONG_STATE, "msg": "Build %s not in FINISHED or FAILED state." % uuid}]), 400 else: return Response(uuid_tar(api.config["COMPOSER_CFG"], uuid, metadata=False, image=False, logs=True), mimetype="application/x-tar", headers=[("Content-Disposition", "attachment; filename=%s-logs.tar;" % uuid)], direct_passthrough=True) + @api.route("/api/v0/compose/image", defaults={'uuid': ""}) @api.route("/api/v0/compose/image/<uuid>") @crossdomain(origin="*") + @checkparams([("uuid","", "no UUID given")]) def v0_compose_image(uuid): """Return the output image for the build""" + if VALID_API_STRING.match(uuid) is None: + return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in API path"}]), 400 + status = uuid_status(api.config["COMPOSER_CFG"], uuid) if status is None: - return jsonify(status=False, errors=["%s is not a valid build uuid" % uuid]), 400 + return jsonify(status=False, errors=[{"id": UNKNOWN_UUID, "msg": "%s is not a valid build uuid" % uuid}]), 400 elif status["queue_status"] not in ["FINISHED", "FAILED"]: - return jsonify(status=False, errors=["Build %s not in FINISHED or FAILED state." % uuid]), 400 + return jsonify(status=False, errors=[{"id": BUILD_IN_WRONG_STATE, "msg": "Build %s not in FINISHED or FAILED state." % uuid}]), 400 else: image_name, image_path = uuid_image(api.config["COMPOSER_CFG"], uuid) # Make sure it really exists if not os.path.exists(image_path): - return jsonify(status=False, errors=["Build %s is missing image file %s" % (uuid, image_name)]), 400 + return jsonify(status=False, errors=[{"id": BUILD_MISSING_FILE, "msg": "Build %s is missing image file %s" % (uuid, image_name)}]), 400 # Make the image name unique image_name = uuid + "-" + image_name # XXX - Will mime type guessing work for all our output? return send_file(image_path, as_attachment=True, attachment_filename=image_name, add_etags=False) + @api.route("/api/v0/compose/log", defaults={'uuid': ""}) @api.route("/api/v0/compose/log/<uuid>") @crossdomain(origin="*") + @checkparams([("uuid","", "no UUID given")]) def v0_compose_log_tail(uuid): """Return the end of the main anaconda.log, defaults to 1Mbytes""" + if VALID_API_STRING.match(uuid) is None: + return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in API path"}]), 400 + try: size = int(request.args.get("size", "1024")) except ValueError as e: - return jsonify(status=False, errors=[str(e)]), 400 + return jsonify(status=False, errors=[{"id": COMPOSE_ERROR, "msg": str(e)}]), 400 status = uuid_status(api.config["COMPOSER_CFG"], uuid) if status is None: - return jsonify(status=False, errors=["%s is not a valid build uuid" % uuid]), 400 + return jsonify(status=False, errors=[{"id": UNKNOWN_UUID, "msg": "%s is not a valid build uuid" % uuid}]), 400 elif status["queue_status"] == "WAITING": - return jsonify(status=False, errors=["Build %s has not started yet. No logs to view" % uuid]) + return jsonify(status=False, errors=[{"id": BUILD_IN_WRONG_STATE, "msg": "Build %s has not started yet. No logs to view" % uuid}]) try: return Response(uuid_log(api.config["COMPOSER_CFG"], uuid, size), direct_passthrough=True) except RuntimeError as e: - return jsonify(status=False, errors=[str(e)]), 400
    + return jsonify(status=False, errors=[{"id": COMPOSE_ERROR, "msg": str(e)}]), 400 @@ -1871,7 +2128,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • pylorax.api »
  • diff --git a/lorax-composer/_modules/pylorax/api/workspace.html b/lorax-composer/_modules/pylorax/api/workspace.html index 3db01955..49963713 100644 --- a/lorax-composer/_modules/pylorax/api/workspace.html +++ b/lorax-composer/_modules/pylorax/api/workspace.html @@ -8,7 +8,7 @@ - pylorax.api.workspace — Lorax 19.7.18 documentation + pylorax.api.workspace — Lorax 19.7.21 documentation @@ -16,7 +16,7 @@ - + @@ -38,7 +38,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • pylorax.api »
  • @@ -183,7 +183,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • pylorax.api »
  • diff --git a/lorax-composer/_modules/pylorax/api/yumbase.html b/lorax-composer/_modules/pylorax/api/yumbase.html index 67890928..bc73d6cf 100644 --- a/lorax-composer/_modules/pylorax/api/yumbase.html +++ b/lorax-composer/_modules/pylorax/api/yumbase.html @@ -8,7 +8,7 @@ - pylorax.api.yumbase — Lorax 19.7.18 documentation + pylorax.api.yumbase — Lorax 19.7.21 documentation @@ -16,7 +16,7 @@ - + @@ -38,7 +38,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • pylorax.api »
  • @@ -77,6 +77,8 @@ from glob import glob import os import yum +from yum.Errors import YumBaseError + # This is a hack to short circuit yum's internal logging yum.logginglevels._added_handlers = True @@ -171,9 +173,13 @@ for r in yb.repos.sort(): r.metadata_expire = 0 r.mdpolicy = "group:all" - yb.doRepoSetup() - yb.repos.doSetup() - yb.repos.populateSack(mdtype='all', cacheonly=1) + try: + yb.doRepoSetup() + yb.repos.doSetup() + yb.repos.populateSack(mdtype='all', cacheonly=0) + except YumBaseError as e: + log.error("Failed to update metadata: %s", str(e)) + raise RuntimeError("Fetching metadata failed: %s" % str(e)) @@ -207,7 +213,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • pylorax.api »
  • diff --git a/lorax-composer/_modules/pylorax/base.html b/lorax-composer/_modules/pylorax/base.html index 4bce37ac..dbadeec9 100644 --- a/lorax-composer/_modules/pylorax/base.html +++ b/lorax-composer/_modules/pylorax/base.html @@ -8,7 +8,7 @@ - pylorax.base — Lorax 19.7.18 documentation + pylorax.base — Lorax 19.7.21 documentation @@ -16,7 +16,7 @@ - + @@ -38,7 +38,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • @@ -150,7 +150,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • diff --git a/lorax-composer/_modules/pylorax/buildstamp.html b/lorax-composer/_modules/pylorax/buildstamp.html index ba0a69e3..1441a842 100644 --- a/lorax-composer/_modules/pylorax/buildstamp.html +++ b/lorax-composer/_modules/pylorax/buildstamp.html @@ -8,7 +8,7 @@ - pylorax.buildstamp — Lorax 19.7.18 documentation + pylorax.buildstamp — Lorax 19.7.21 documentation @@ -16,7 +16,7 @@ - + @@ -38,7 +38,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • @@ -141,7 +141,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • diff --git a/lorax-composer/_modules/pylorax/creator.html b/lorax-composer/_modules/pylorax/creator.html index 9383b2b8..f6956585 100644 --- a/lorax-composer/_modules/pylorax/creator.html +++ b/lorax-composer/_modules/pylorax/creator.html @@ -8,7 +8,7 @@ - pylorax.creator — Lorax 19.7.18 documentation + pylorax.creator — Lorax 19.7.21 documentation @@ -16,7 +16,7 @@ - + @@ -38,7 +38,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • @@ -697,7 +697,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • diff --git a/lorax-composer/_modules/pylorax/decorators.html b/lorax-composer/_modules/pylorax/decorators.html index cd5a3f1f..9b5cd684 100644 --- a/lorax-composer/_modules/pylorax/decorators.html +++ b/lorax-composer/_modules/pylorax/decorators.html @@ -8,7 +8,7 @@ - pylorax.decorators — Lorax 19.7.18 documentation + pylorax.decorators — Lorax 19.7.21 documentation @@ -16,7 +16,7 @@ - + @@ -38,7 +38,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • @@ -113,7 +113,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • diff --git a/lorax-composer/_modules/pylorax/discinfo.html b/lorax-composer/_modules/pylorax/discinfo.html index c93a6c99..c1a405e5 100644 --- a/lorax-composer/_modules/pylorax/discinfo.html +++ b/lorax-composer/_modules/pylorax/discinfo.html @@ -8,7 +8,7 @@ - pylorax.discinfo — Lorax 19.7.18 documentation + pylorax.discinfo — Lorax 19.7.21 documentation @@ -16,7 +16,7 @@ - + @@ -38,7 +38,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • @@ -122,7 +122,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • diff --git a/lorax-composer/_modules/pylorax/executils.html b/lorax-composer/_modules/pylorax/executils.html index e4f46e03..b21ec972 100644 --- a/lorax-composer/_modules/pylorax/executils.html +++ b/lorax-composer/_modules/pylorax/executils.html @@ -8,7 +8,7 @@ - pylorax.executils — Lorax 19.7.18 documentation + pylorax.executils — Lorax 19.7.21 documentation @@ -16,7 +16,7 @@ - + @@ -38,7 +38,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • @@ -508,7 +508,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • diff --git a/lorax-composer/_modules/pylorax/imgutils.html b/lorax-composer/_modules/pylorax/imgutils.html index dc206b50..a2ab05bb 100644 --- a/lorax-composer/_modules/pylorax/imgutils.html +++ b/lorax-composer/_modules/pylorax/imgutils.html @@ -8,7 +8,7 @@ - pylorax.imgutils — Lorax 19.7.18 documentation + pylorax.imgutils — Lorax 19.7.21 documentation @@ -16,7 +16,7 @@ - + @@ -38,7 +38,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • @@ -569,7 +569,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • diff --git a/lorax-composer/_modules/pylorax/installer.html b/lorax-composer/_modules/pylorax/installer.html index e1d8a8ea..1d746907 100644 --- a/lorax-composer/_modules/pylorax/installer.html +++ b/lorax-composer/_modules/pylorax/installer.html @@ -8,7 +8,7 @@ - pylorax.installer — Lorax 19.7.18 documentation + pylorax.installer — Lorax 19.7.21 documentation @@ -16,7 +16,7 @@ - + @@ -38,7 +38,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • @@ -493,7 +493,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • diff --git a/lorax-composer/_modules/pylorax/logmonitor.html b/lorax-composer/_modules/pylorax/logmonitor.html index bab4c4fb..ab40fe33 100644 --- a/lorax-composer/_modules/pylorax/logmonitor.html +++ b/lorax-composer/_modules/pylorax/logmonitor.html @@ -8,7 +8,7 @@ - pylorax.logmonitor — Lorax 19.7.18 documentation + pylorax.logmonitor — Lorax 19.7.21 documentation @@ -16,7 +16,7 @@ - + @@ -38,7 +38,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • @@ -206,7 +206,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • diff --git a/lorax-composer/_modules/pylorax/ltmpl.html b/lorax-composer/_modules/pylorax/ltmpl.html index 5ee57071..be2eb4d2 100644 --- a/lorax-composer/_modules/pylorax/ltmpl.html +++ b/lorax-composer/_modules/pylorax/ltmpl.html @@ -8,7 +8,7 @@ - pylorax.ltmpl — Lorax 19.7.18 documentation + pylorax.ltmpl — Lorax 19.7.21 documentation @@ -16,7 +16,7 @@ - + @@ -38,7 +38,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • @@ -748,7 +748,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • diff --git a/lorax-composer/_modules/pylorax/sysutils.html b/lorax-composer/_modules/pylorax/sysutils.html index 8444fe8f..3ff82595 100644 --- a/lorax-composer/_modules/pylorax/sysutils.html +++ b/lorax-composer/_modules/pylorax/sysutils.html @@ -8,7 +8,7 @@ - pylorax.sysutils — Lorax 19.7.18 documentation + pylorax.sysutils — Lorax 19.7.21 documentation @@ -16,7 +16,7 @@ - + @@ -38,7 +38,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • @@ -191,7 +191,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • diff --git a/lorax-composer/_modules/pylorax/treebuilder.html b/lorax-composer/_modules/pylorax/treebuilder.html index 3f94cef0..87f59f2f 100644 --- a/lorax-composer/_modules/pylorax/treebuilder.html +++ b/lorax-composer/_modules/pylorax/treebuilder.html @@ -8,7 +8,7 @@ - pylorax.treebuilder — Lorax 19.7.18 documentation + pylorax.treebuilder — Lorax 19.7.21 documentation @@ -16,7 +16,7 @@ - + @@ -38,7 +38,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • @@ -403,7 +403,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • diff --git a/lorax-composer/_modules/pylorax/treeinfo.html b/lorax-composer/_modules/pylorax/treeinfo.html index 5890bdfc..5c881940 100644 --- a/lorax-composer/_modules/pylorax/treeinfo.html +++ b/lorax-composer/_modules/pylorax/treeinfo.html @@ -8,7 +8,7 @@ - pylorax.treeinfo — Lorax 19.7.18 documentation + pylorax.treeinfo — Lorax 19.7.21 documentation @@ -16,7 +16,7 @@ - + @@ -38,7 +38,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • @@ -140,7 +140,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • diff --git a/lorax-composer/_modules/pylorax/yumhelper.html b/lorax-composer/_modules/pylorax/yumhelper.html index 354c135d..352ce843 100644 --- a/lorax-composer/_modules/pylorax/yumhelper.html +++ b/lorax-composer/_modules/pylorax/yumhelper.html @@ -8,7 +8,7 @@ - pylorax.yumhelper — Lorax 19.7.18 documentation + pylorax.yumhelper — Lorax 19.7.21 documentation @@ -16,7 +16,7 @@ - + @@ -38,7 +38,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • @@ -208,7 +208,7 @@
  • modules |
  • -
  • Lorax 19.7.18 documentation »
  • +
  • Lorax 19.7.21 documentation »
  • Module code »
  • pylorax »
  • diff --git a/lorax-composer/_sources/composer-cli.rst.txt b/lorax-composer/_sources/composer-cli.rst.txt new file mode 100644 index 00000000..366e01ad --- /dev/null +++ b/lorax-composer/_sources/composer-cli.rst.txt @@ -0,0 +1,62 @@ +composer-cli +============ + +:Authors: + Brian C. Lane + +``composer-cli`` is used to interact with the ``lorax-composer`` API server, managing blueprints, exploring available packages, and building new images. + +It requires `lorax-composer `_ to be installed on the +local system, and the user running it needs to be a member of the ``weldr`` +group. They do not need to be root, but all of the `security precautions +`_ apply. + +composer-cli cmdline arguments +------------------------------ + +.. argparse:: + :ref: composer.cli.cmdline.composer_cli_parser + :prog: composer-cli + +Edit a Blueprint +---------------- + +Start out by listing the available blueprints using ``composer-cli blueprints +list``, pick one and save it to the local directory by running ``composer-cli +blueprints save http-server``. If there are no blueprints available you can +copy one of the examples `from the test suite +`_. + +Edit the file (it will be saved with a .toml extension) and change the +description, add a package or module to it. Send it back to the server by +running ``composer-cli blueprints push http-server.toml``. You can verify that it was +saved by viewing the changelog - ``composer-cli blueprints changes http-server``. + +Build an image +---------------- + +Build a ``qcow2`` disk image from this blueprint by running ``composer-cli +compose start http-server qcow2``. It will print a UUID that you can use to +keep track of the build. You can also cancel the build if needed. + +The available types of images is displayed by ``composer-cli compose types``. +Currently this consists of: ext4-filesystem, live-iso, partitioned-disk, qcow2, +tar + +Monitor the build status +------------------------ + +Monitor it using ``composer-cli compose status``, which will show the status of +all the builds on the system. You can view the end of the anaconda build logs +once it is in the ``RUNNING`` state using ``composer-cli compose log UUID`` +where UUID is the UUID returned by the start command. + +Once the build is in the ``FINISHED`` state you can download the image. + +Download the image +------------------ + +Downloading the final image is done with ``composer-cli compose image UUID`` and it will +save the qcow2 image as ``UUID-disk.qcow2`` which you can then use to boot a VM like this:: + + qemu-kvm --name test-image -m 1024 -hda ./UUID-disk.qcow2 diff --git a/lorax-composer/_sources/composer.cli.rst.txt b/lorax-composer/_sources/composer.cli.rst.txt new file mode 100644 index 00000000..b71dcda4 --- /dev/null +++ b/lorax-composer/_sources/composer.cli.rst.txt @@ -0,0 +1,78 @@ +composer\.cli package +===================== + +Submodules +---------- + +composer\.cli\.blueprints module +-------------------------------- + +.. automodule:: composer.cli.blueprints + :members: + :undoc-members: + :show-inheritance: + +composer\.cli\.compose module +----------------------------- + +.. automodule:: composer.cli.compose + :members: + :undoc-members: + :show-inheritance: + +composer\.cli\.help module +-------------------------- + +.. automodule:: composer.cli.help + :members: + :undoc-members: + :show-inheritance: + +composer\.cli\.modules module +----------------------------- + +.. automodule:: composer.cli.modules + :members: + :undoc-members: + :show-inheritance: + +composer\.cli\.projects module +------------------------------ + +.. automodule:: composer.cli.projects + :members: + :undoc-members: + :show-inheritance: + +composer\.cli\.sources module +----------------------------- + +.. automodule:: composer.cli.sources + :members: + :undoc-members: + :show-inheritance: + +composer\.cli\.status module +---------------------------- + +.. automodule:: composer.cli.status + :members: + :undoc-members: + :show-inheritance: + +composer\.cli\.utilities module +------------------------------- + +.. automodule:: composer.cli.utilities + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: composer.cli + :members: + :undoc-members: + :show-inheritance: diff --git a/lorax-composer/_sources/composer.cli.txt b/lorax-composer/_sources/composer.cli.txt index ed753029..a93fedfa 100644 --- a/lorax-composer/_sources/composer.cli.txt +++ b/lorax-composer/_sources/composer.cli.txt @@ -57,6 +57,14 @@ cli Package :undoc-members: :show-inheritance: +:mod:`status` Module +-------------------- + +.. automodule:: composer.cli.status + :members: + :undoc-members: + :show-inheritance: + :mod:`utilities` Module ----------------------- diff --git a/lorax-composer/_sources/composer.rst.txt b/lorax-composer/_sources/composer.rst.txt new file mode 100644 index 00000000..f6658ce6 --- /dev/null +++ b/lorax-composer/_sources/composer.rst.txt @@ -0,0 +1,37 @@ +composer package +================ + +Subpackages +----------- + +.. toctree:: + + composer.cli + +Submodules +---------- + +composer\.http\_client module +----------------------------- + +.. automodule:: composer.http_client + :members: + :undoc-members: + :show-inheritance: + +composer\.unix\_socket module +----------------------------- + +.. automodule:: composer.unix_socket + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: composer + :members: + :undoc-members: + :show-inheritance: diff --git a/lorax-composer/_sources/index.rst.txt b/lorax-composer/_sources/index.rst.txt new file mode 100644 index 00000000..67f2b9ed --- /dev/null +++ b/lorax-composer/_sources/index.rst.txt @@ -0,0 +1,29 @@ +.. Lorax documentation master file, created by + sphinx-quickstart on Wed Apr 8 13:46:00 2015. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to Lorax's documentation! +================================= + +Contents: + +.. toctree:: + :maxdepth: 1 + + intro + lorax + livemedia-creator + lorax-composer + composer-cli + product-images + modules + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/lorax-composer/_sources/intro.rst.txt b/lorax-composer/_sources/intro.rst.txt new file mode 100644 index 00000000..01857ee9 --- /dev/null +++ b/lorax-composer/_sources/intro.rst.txt @@ -0,0 +1,67 @@ +Introduction to Lorax +===================== + +I am the Lorax. I speak for the trees [and images]. + +Lorax is used to build the Anaconda Installer boot.iso, it consists of a +library, pylorax, a set of templates, and the lorax script. Its operation +is driven by a customized set of Mako templates that lists the packages +to be installed, steps to execute to remove unneeded files, and creation +of the iso for all of the supported architectures. + + + + + + +Before Lorax +============ + +Tree building tools such as pungi and revisor rely on 'buildinstall' in +anaconda/scripts/ to produce the boot images and other such control files +in the final tree. The existing buildinstall scripts written in a mix of +bash and Python are unmaintainable. Lorax is an attempt to replace them +with something more flexible. + + +EXISTING WORKFLOW: + +pungi and other tools call scripts/buildinstall, which in turn call other +scripts to do the image building and data generation. Here's how it +currently looks: + + -> buildinstall + * process command line options + * write temporary yum.conf to point to correct repo + * find anaconda release RPM + * unpack RPM, pull in those versions of upd-instroot, mk-images, + maketreeinfo.py, makestamp.py, and buildinstall + + -> call upd-instroot + + -> call maketreeinfo.py + + -> call mk-images (which figures out which mk-images.ARCH to call) + + -> call makestamp.py + + * clean up + + +PROBLEMS: + +The existing workflow presents some problems with maintaining the scripts. +First, almost all knowledge of what goes in to the stage 1 and stage 2 +images lives in upd-instroot. The mk-images* scripts copy things from the +root created by upd-instroot in order to build the stage 1 image, though +it's not completely clear from reading the scripts. + + +NEW IDEAS: + +Create a new central driver with all information living in Python modules. +Configuration files will provide the knowledge previously contained in the +upd-instroot and mk-images* scripts. + + + diff --git a/lorax-composer/_sources/livemedia-creator.rst.txt b/lorax-composer/_sources/livemedia-creator.rst.txt new file mode 100644 index 00000000..94d6a331 --- /dev/null +++ b/lorax-composer/_sources/livemedia-creator.rst.txt @@ -0,0 +1,391 @@ +livemedia-creator +================= + +:Authors: + Brian C. Lane + +livemedia-creator uses `Anaconda `_, +`kickstart `_ and `Lorax +`_ to create bootable media that use the +same install path as a normal system installation. It can be used to make live +isos, bootable (partitioned) disk images, tarfiles, and filesystem images for +use with virtualization and container solutions like libvirt, docker, and +OpenStack. + +The general idea is to use virt-install with kickstart and an Anaconda boot.iso to +install into a disk image and then use the disk image to create the bootable +media. + +livemedia-creator --help will describe all of the options available. At the +minimum you need: + +``--make-iso`` to create a final bootable .iso or one of the other ``--make-*`` options. + +``--iso`` to specify the Anaconda install media to use with virt-install. + +``--ks`` to select the kickstart file describing what to install. + +To use livemedia-creator with virtualization you will need to have virt-install installed. + +If you are going to be using Anaconda directly, with ``--no-virt`` mode, make sure +you have the anaconda-tui package installed. + +Conventions used in this document: + +``lmc`` is an abbreviation for livemedia-creator. + +``builder`` is the system where livemedia-creator is being run + +``image`` is the disk image being created by running livemedia-creator + + +livemedia-creator cmdline arguments +----------------------------------- + +See the output from ``livemedia-creator --help`` for the commandline arguments. + +Quickstart +---------- + +Run this to create a bootable live iso:: + + sudo livemedia-creator --make-iso \ + --iso=/extra/iso/boot.iso --ks=./docs/rhel7-livemedia.ks + +You can run it directly from the lorax git repo like this:: + + sudo PATH=./src/sbin/:$PATH PYTHONPATH=./src/ ./src/sbin/livemedia-creator \ + --make-iso --iso=/extra/iso/boot.iso \ + --ks=./docs/rhel7-livemedia.ks --lorax-templates=./share/ + +You can observe the installation using vnc. The logs will show what port was +chosen, or you can use a specific port by passing it. eg. ``--vnc vnc:127.0.0.1:5`` + +This is usually a good idea when testing changes to the kickstart. lmc tries +to monitor the logs for fatal errors, but may not catch everything. + + +How ISO creation works +---------------------- + +There are 2 stages, the install stage which produces a disk or filesystem image +as its output, and the boot media creation which uses the image as its input. +Normally you would run both stages, but it is possible to stop after the +install stage, by using ``--image-only``, or to skip the install stage and use +a previously created disk image by passing ``--disk-image`` or ``--fs-image`` + +When creating an iso virt-install boots using the passed Anaconda installer iso +and installs the system based on the kickstart. The ``%post`` section of the +kickstart is used to customize the installed system in the same way that +current spin-kickstarts do. + +livemedia-creator monitors the install process for problems by watching the +install logs. They are written to the current directory or to the base +directory specified by the --logfile command. You can also monitor the install +by using a vnc client. This is recommended when first modifying a kickstart, +since there are still places where Anaconda may get stuck without the log +monitor catching it. + +The output from this process is a partitioned disk image. kpartx can be used +to mount and examine it when there is a problem with the install. It can also +be booted using kvm. + +When creating an iso the disk image's / partition is copied into a formatted +filesystem image which is then used as the input to lorax for creation of the +final media. + +The final image is created by lorax, using the templates in /usr/share/lorax/live/ +or the live directory below the directory specified by ``--lorax-templates``. The +templates are written using the Mako template system with some extra commands +added by lorax. + + +Kickstarts +---------- + +The docs/ directory includes several example kickstarts, one to create a live +desktop iso using GNOME, and another to create a minimal disk image. When +creating your own kickstarts you should start with the minimal example, it +includes several needed packages that are not always included by dependencies. + +Or you can use existing spin kickstarts to create live media with a few +changes. Here are the steps I used to convert the Fedora XFCE spin. + +1. Flatten the xfce kickstart using ksflatten +2. Add zerombr so you don't get the disk init dialog +3. Add clearpart --all +4. Add swap partition +5. bootloader target +6. Add shutdown to the kickstart +7. Add network --bootproto=dhcp --activate to activate the network + This works for F16 builds but for F15 and before you need to pass + something on the cmdline that activate the network, like sshd: + + ``livemedia-creator --kernel-args="sshd"`` + +8. Add a root password:: + + rootpw rootme + network --bootproto=dhcp --activate + zerombr + clearpart --all + bootloader --location=mbr + part swap --size=512 + shutdown + +9. In the livesys script section of the %post remove the root password. This + really depends on how the spin wants to work. You could add the live user + that you create to the %wheel group so that sudo works if you wanted to. + + ``passwd -d root > /dev/null`` + +10. Remove /etc/fstab in %post, dracut handles mounting the rootfs + + ``cat /dev/null > /dev/fstab`` + + Do this only for live iso's, the filesystem will be mounted read only if + there is no /etc/fstab + +11. Don't delete initramfs files from /boot in %post +12. When creating live iso's you need to have, at least, these packages in the %package section:: + dracut-config-generic + dracut-live + -dracut-config-rescue + grub-efi + memtest86+ + syslinux + +One drawback to using virt-install is that it pulls the packages from the repo +each time you run it. To speed things up you either need a local mirror of the +packages, or you can use a caching proxy. When using a proxy you pass it to +livemedia-creator like this: + + ``--proxy=http://proxy.yourdomain.com:3128`` + +You also need to use a specific mirror instead of mirrormanager so that the +packages will get cached, so your kickstart url would look like: + + ``url --url="http://dl.fedoraproject.org/pub/fedora/linux/development/rawhide/x86_64/os/"`` + +You can also add an update repo, but don't name it updates. Add --proxy to it +as well. + + +Anaconda image install (no-virt) +-------------------------------- + +You can create images without using virt-install by passing ``--no-virt`` on +the cmdline. This will use Anaconda's directory install feature to handle the +install. There are a couple of things to keep in mind when doing this: + +1. It will be most reliable when building images for the same release that the + host is running. Because Anaconda has expectations about the system it is + running under you may encounter strange bugs if you try to build newer or + older releases. + +2. Make sure selinux is set to permissive or disabled. It won't install + correctly with selinux set to enforcing yet. + +3. It may totally trash your host. So far I haven't had this happen, but the + possibility exists that a bug in Anaconda could result in it operating on + real devices. I recommend running it in a virt or on a system that you can + afford to lose all data from. + +The logs from anaconda will be placed in an ./anaconda/ directory in either +the current directory or in the directory used for --logfile + +Example cmdline: + +``sudo livemedia-creator --make-iso --no-virt --ks=./rhel7-livemedia.ks`` + +.. note:: + Using no-virt to create a partitioned disk image (eg. --make-disk or + --make-vagrant) will only create disks usable on the host platform (BIOS + or UEFI). You can create BIOS partitioned disk images on UEFI by using + virt. + + +AMI Images +---------- + +Amazon EC2 images can be created by using the --make-ami switch and an appropriate +kickstart file. All of the work to customize the image is handled by the kickstart. +The example currently included was modified from the cloud-kickstarts version so +that it would work with livemedia-creator. + +Example cmdline: + +``sudo livemedia-creator --make-ami --iso=/path/to/boot.iso --ks=./docs/rhel7-livemedia-ec2.ks`` + +This will produce an ami-root.img file in the working directory. + +At this time I have not tested the image with EC2. Feedback would be welcome. + + +Appliance Creation +------------------ + +livemedia-creator can now replace appliance-tools by using the --make-appliance +switch. This will create the partitioned disk image and an XML file that can be +used with virt-image to setup a virtual system. + +The XML is generated using the Mako template from +/usr/share/lorax/appliance/libvirt.xml You can use a different template by +passing ``--app-template