+#
+# Copyright (C) 2017-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/>.
+#
+""" Setup v0 of the API server
+
+v0_api() must be called to setup the API routes for Flask
+
+Status Responses
+----------------
+
+Some requests only return a status/error response.
+
+ The response will be a status response with `status` set to true, or an
+ error response with it set to false and an error message included.
+
+ Example response::
+
+ {
+ "status": true
+ }
+
+ Error response::
+
+ {
+ "errors": ["ggit-error: Failed to remove entry. File isn't in the tree - jboss.toml (-1)"]
+ "status": false
+ }
+
+API Routes
+----------
+
+All of the blueprints routes support the optional `branch` argument. If it is not
+used then the API will use the `master` branch for blueprints. If you want to create
+a new branch use the `new` or `workspace` routes with ?branch=<branch-name> to
+store the new blueprint on the new branch.
+
+`/api/v0/blueprints/list`
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ List the available blueprints::
+
+ { "limit": 20,
+ "offset": 0,
+ "blueprints": [
+ "atlas",
+ "development",
+ "glusterfs",
+ "http-server",
+ "jboss",
+ "kubernetes" ],
+ "total": 6 }
+
+`/api/v0/blueprints/info/<blueprint_names>`
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Return the JSON representation of the blueprint. This includes 3 top level
+ objects. `changes` which lists whether or not the workspace is different from
+ the most recent commit. `blueprints` which lists the JSON representation of the
+ blueprint, and `errors` which will list any errors, like non-existant blueprints.
+
+ Example::
+
+ {
+ "changes": [
+ {
+ "changed": false,
+ "name": "glusterfs"
+ }
+ ],
+ "errors": [],
+ "blueprints": [
+ {
+ "description": "An example GlusterFS server with samba",
+ "modules": [
+ {
+ "name": "glusterfs",
+ "version": "3.7.*"
+ },
+ {
+ "name": "glusterfs-cli",
+ "version": "3.7.*"
+ }
+ ],
+ "name": "glusterfs",
+ "packages": [
+ {
+ "name": "2ping",
+ "version": "3.2.1"
+ },
+ {
+ "name": "samba",
+ "version": "4.2.*"
+ }
+ ],
+ "version": "0.0.6"
+ }
+ ]
+ }
+
+ Error example::
+
+ {
+ "changes": [],
+ "errors": ["ggit-error: the path 'missing.toml' does not exist in the given tree (-3)"]
+ "blueprints": []
+ }
+
+`/api/v0/blueprints/changes/<blueprint_names>[?offset=0&limit=20]`
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Return the commits to a blueprint. By default it returns the first 20 commits, this
+ can be changed by passing `offset` and/or `limit`. The response will include the
+ commit hash, summary, timestamp, and optionally the revision number. The commit
+ hash can be passed to `/api/v0/blueprints/diff/` to retrieve the exact changes.
+
+ Example::
+
+ {
+ "errors": [],
+ "limit": 20,
+ "offset": 0,
+ "blueprints": [
+ {
+ "changes": [
+ {
+ "commit": "e083921a7ed1cf2eec91ad12b9ad1e70ef3470be",
+ "message": "blueprint glusterfs, version 0.0.6 saved.",
+ "revision": null,
+ "timestamp": "2017-11-23T00:18:13Z"
+ },
+ {
+ "commit": "cee5f4c20fc33ea4d54bfecf56f4ad41ad15f4f3",
+ "message": "blueprint glusterfs, version 0.0.5 saved.",
+ "revision": null,
+ "timestamp": "2017-11-11T01:00:28Z"
+ },
+ {
+ "commit": "29b492f26ed35d80800b536623bafc51e2f0eff2",
+ "message": "blueprint glusterfs, version 0.0.4 saved.",
+ "revision": null,
+ "timestamp": "2017-11-11T00:28:30Z"
+ },
+ {
+ "commit": "03374adbf080fe34f5c6c29f2e49cc2b86958bf2",
+ "message": "blueprint glusterfs, version 0.0.3 saved.",
+ "revision": null,
+ "timestamp": "2017-11-10T23:15:52Z"
+ },
+ {
+ "commit": "0e08ecbb708675bfabc82952599a1712a843779d",
+ "message": "blueprint glusterfs, version 0.0.2 saved.",
+ "revision": null,
+ "timestamp": "2017-11-10T23:14:56Z"
+ },
+ {
+ "commit": "3e11eb87a63d289662cba4b1804a0947a6843379",
+ "message": "blueprint glusterfs, version 0.0.1 saved.",
+ "revision": null,
+ "timestamp": "2017-11-08T00:02:47Z"
+ }
+ ],
+ "name": "glusterfs",
+ "total": 6
+ }
+ ]
+ }
+
+POST `/api/v0/blueprints/new`
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Create a new blueprint, or update an existing blueprint. This supports both JSON and TOML
+ for the blueprint format. The blueprint should be in the body of the request with the
+ `Content-Type` header set to either `application/json` or `text/x-toml`.
+
+ The response will be a status response with `status` set to true, or an
+ error response with it set to false and an error message included.
+
+DELETE `/api/v0/blueprints/delete/<blueprint_name>`
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Delete a blueprint. The blueprint is deleted from the branch, and will no longer
+ be listed by the `list` route. A blueprint can be undeleted using the `undo` route
+ to revert to a previous commit.
+
+ The response will be a status response with `status` set to true, or an
+ error response with it set to false and an error message included.
+
+POST `/api/v0/blueprints/workspace`
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Write a blueprint to the temporary workspace. This works exactly the same as `new` except
+ that it does not create a commit. JSON and TOML bodies are supported.
+
+ The workspace is meant to be used as a temporary blueprint storage for clients.
+ It will be read by the `info` and `diff` routes if it is different from the
+ most recent commit.
+
+ The response will be a status response with `status` set to true, or an
+ error response with it set to false and an error message included.
+
+DELETE `/api/v0/blueprints/workspace/<blueprint_name>`
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Remove the temporary workspace copy of a blueprint. The `info` route will now
+ return the most recent commit of the blueprint. Any changes that were in the
+ workspace will be lost.
+
+ The response will be a status response with `status` set to true, or an
+ error response with it set to false and an error message included.
+
+POST `/api/v0/blueprints/undo/<blueprint_name>/<commit>`
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ This will revert the blueprint to a previous commit. The commit hash from the `changes`
+ route can be used in this request.
+
+ The response will be a status response with `status` set to true, or an
+ error response with it set to false and an error message included.
+
+POST `/api/v0/blueprints/tag/<blueprint_name>`
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Tag a blueprint as a new release. This uses git tags with a special format.
+ `refs/tags/<branch>/<filename>/r<revision>`. Only the most recent blueprint commit
+ can be tagged. Revisions start at 1 and increment for each new tag
+ (per-blueprint). If the commit has already been tagged it will return false.
+
+ The response will be a status response with `status` set to true, or an
+ error response with it set to false and an error message included.
+
+`/api/v0/blueprints/diff/<blueprint_name>/<from_commit>/<to_commit>`
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Return the differences between two commits, or the workspace. The commit hash
+ from the `changes` response can be used here, or several special strings:
+
+ - NEWEST will select the newest git commit. This works for `from_commit` or `to_commit`
+ - WORKSPACE will select the workspace copy. This can only be used in `to_commit`
+
+ eg. `/api/v0/blueprints/diff/glusterfs/NEWEST/WORKSPACE` will return the differences
+ between the most recent git commit and the contents of the workspace.
+
+ Each entry in the response's diff object contains the old blueprint value and the new one.
+ If old is null and new is set, then it was added.
+ If new is null and old is set, then it was removed.
+ If both are set, then it was changed.
+
+ The old/new entries will have the name of the blueprint field that was changed. This
+ can be one of: Name, Description, Version, Module, or Package.
+ The contents for these will be the old/new values for them.
+
+ In the example below the version was changed and the ping package was added.
+
+ Example::
+
+ {
+ "diff": [
+ {
+ "new": {
+ "Version": "0.0.6"
+ },
+ "old": {
+ "Version": "0.0.5"
+ }
+ },
+ {
+ "new": {
+ "Package": {
+ "name": "ping",
+ "version": "3.2.1"
+ }
+ },
+ "old": null
+ }
+ ]
+ }
+
+`/api/v0/blueprints/freeze/<blueprint_names>`
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Return a JSON representation of the blueprint with the package and module versions set
+ to the exact versions chosen by depsolving the blueprint.
+
+ Example::
+
+ {
+ "errors": [],
+ "blueprints": [
+ {
+ "blueprint": {
+ "description": "An example GlusterFS server with samba",
+ "modules": [
+ {
+ "name": "glusterfs",
+ "version": "3.8.4-18.4.el7.x86_64"
+ },
+ {
+ "name": "glusterfs-cli",
+ "version": "3.8.4-18.4.el7.x86_64"
+ }
+ ],
+ "name": "glusterfs",
+ "packages": [
+ {
+ "name": "ping",
+ "version": "2:3.2.1-2.el7.noarch"
+ },
+ {
+ "name": "samba",
+ "version": "4.6.2-8.el7.x86_64"
+ }
+ ],
+ "version": "0.0.6"
+ }
+ }
+ ]
+ }
+
+`/api/v0/blueprints/depsolve/<blueprint_names>`
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Depsolve the blueprint using yum, return the blueprint used, and the NEVRAs of the packages
+ chosen to satisfy the blueprint's requirements. The response will include a list of results,
+ with the full dependency list in `dependencies`, the NEVRAs for the blueprint's direct modules
+ and packages in `modules`, and any error will be in `errors`.
+
+ Example::
+
+ {
+ "errors": [],
+ "blueprints": [
+ {
+ "dependencies": [
+ {
+ "arch": "noarch",
+ "epoch": "0",
+ "name": "2ping",
+ "release": "2.el7",
+ "version": "3.2.1"
+ },
+ {
+ "arch": "x86_64",
+ "epoch": "0",
+ "name": "acl",
+ "release": "12.el7",
+ "version": "2.2.51"
+ },
+ {
+ "arch": "x86_64",
+ "epoch": "0",
+ "name": "audit-libs",
+ "release": "3.el7",
+ "version": "2.7.6"
+ },
+ {
+ "arch": "x86_64",
+ "epoch": "0",
+ "name": "avahi-libs",
+ "release": "17.el7",
+ "version": "0.6.31"
+ },
+ ...
+ ],
+ "modules": [
+ {
+ "arch": "noarch",
+ "epoch": "0",
+ "name": "2ping",
+ "release": "2.el7",
+ "version": "3.2.1"
+ },
+ {
+ "arch": "x86_64",
+ "epoch": "0",
+ "name": "glusterfs",
+ "release": "18.4.el7",
+ "version": "3.8.4"
+ },
+ ...
+ ],
+ "blueprint": {
+ "description": "An example GlusterFS server with samba",
+ "modules": [
+ {
+ "name": "glusterfs",
+ "version": "3.7.*"
+ },
+ ...
+ }
+ }
+ ]
+ }
+
+`/api/v0/projects/list[?offset=0&limit=20]`
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ List all of the available projects. By default this returns the first 20 items,
+ but this can be changed by setting the `offset` and `limit` arguments.
+
+ Example::
+
+ {
+ "limit": 20,
+ "offset": 0,
+ "projects": [
+ {
+ "description": "0 A.D. (pronounced \"zero ey-dee\") is a ...",
+ "homepage": "http://play0ad.com",
+ "name": "0ad",
+ "summary": "Cross-Platform RTS Game of Ancient Warfare",
+ "upstream_vcs": "UPSTREAM_VCS"
+ },
+ ...
+ ],
+ "total": 21770
+ }
+
+`/api/v0/projects/info/<project_names>`
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Return information about the comma-separated list of projects. It includes the description
+ of the package along with the list of available builds.
+
+ Example::
+
+ {
+ "projects": [
+ {
+ "builds": [
+ {
+ "arch": "x86_64",
+ "build_config_ref": "BUILD_CONFIG_REF",
+ "build_env_ref": "BUILD_ENV_REF",
+ "build_time": "2017-03-01T08:39:23",
+ "changelog": "- restore incremental backups correctly, files ...",
+ "epoch": "2",
+ "metadata": {},
+ "release": "32.el7",
+ "source": {
+ "license": "GPLv3+",
+ "metadata": {},
+ "source_ref": "SOURCE_REF",
+ "version": "1.26"
+ }
+ }
+ ],
+ "description": "The GNU tar program saves many ...",
+ "homepage": "http://www.gnu.org/software/tar/",
+ "name": "tar",
+ "summary": "A GNU file archiving program",
+ "upstream_vcs": "UPSTREAM_VCS"
+ }
+ ]
+ }
+
+`/api/v0/projects/depsolve/<project_names>`
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Depsolve the comma-separated list of projects and return the list of NEVRAs needed
+ to satisfy the request.
+
+ Example::
+
+ {
+ "projects": [
+ {
+ "arch": "noarch",
+ "epoch": "0",
+ "name": "basesystem",
+ "release": "7.el7",
+ "version": "10.0"
+ },
+ {
+ "arch": "x86_64",
+ "epoch": "0",
+ "name": "bash",
+ "release": "28.el7",
+ "version": "4.2.46"
+ },
+ {
+ "arch": "x86_64",
+ "epoch": "0",
+ "name": "filesystem",
+ "release": "21.el7",
+ "version": "3.2"
+ },
+ ...
+ ]
+ }
+
+`/api/v0/modules/list[?offset=0&limit=20]`
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Return a list of all of the available modules. This includes the name and the
+ group_type, which is always "rpm" for lorax-composer. By default this returns
+ the first 20 items. This can be changed by setting the `offset` and `limit`
+ arguments.
+
+ Example::
+
+ {
+ "limit": 20,
+ "modules": [
+ {
+ "group_type": "rpm",
+ "name": "0ad"
+ },
+ {
+ "group_type": "rpm",
+ "name": "0ad-data"
+ },
+ {
+ "group_type": "rpm",
+ "name": "0install"
+ },
+ {
+ "group_type": "rpm",
+ "name": "2048-cli"
+ },
+ ...
+ ]
+ "total": 21770
+ }
+
+`/api/v0/modules/list/<module_names>[?offset=0&limit=20]`
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Return the list of comma-separated modules. Output is the same as `/modules/list`
+
+ Example::
+
+ {
+ "limit": 20,
+ "modules": [
+ {
+ "group_type": "rpm",
+ "name": "tar"
+ }
+ ],
+ "offset": 0,
+ "total": 1
+ }
+
+`/api/v0/modules/info/<module_names>`
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Return the module's dependencies, and the information about the module.
+
+ Example::
+
+ {
+ "modules": [
+ {
+ "dependencies": [
+ {
+ "arch": "noarch",
+ "epoch": "0",
+ "name": "basesystem",
+ "release": "7.el7",
+ "version": "10.0"
+ },
+ {
+ "arch": "x86_64",
+ "epoch": "0",
+ "name": "bash",
+ "release": "28.el7",
+ "version": "4.2.46"
+ },
+ ...
+ ],
+ "description": "The GNU tar program saves ...",
+ "homepage": "http://www.gnu.org/software/tar/",
+ "name": "tar",
+ "summary": "A GNU file archiving program",
+ "upstream_vcs": "UPSTREAM_VCS"
+ }
+ ]
+ }
+
+POST `/api/v0/compose`
+^^^^^^^^^^^^^^^^^^^^^^
+
+ Start a compose. The content type should be 'application/json' and the body of the POST
+ should look like this::
+
+ {
+ "blueprint_name": "http-server",
+ "compose_type": "tar",
+ "branch": "master"
+ }
+
+ Pass it the name of the blueprint, the type of output (from '/api/v0/compose/types'), and the
+ blueprint branch to use. 'branch' is optional and will default to master. It will create a new
+ build and add it to the queue. It returns the build uuid and a status if it succeeds::
+
+ {
+ "build_id": "e6fa6db4-9c81-4b70-870f-a697ca405cdf",
+ "status": true
+ }
+
+`/api/v0/compose/types`
+^^^^^^^^^^^^^^^^^^^^^^^
+
+ Returns the list of supported output types that are valid for use with 'POST /api/v0/compose'
+
+ {
+ "types": [
+ {
+ "enabled": true,
+ "name": "tar"
+ }
+ ]
+ }
+
+`/api/v0/compose/queue`
+^^^^^^^^^^^^^^^^^^^^^^^
+
+ Return the status of the build queue. It includes information about the builds waiting,
+ and the build that is running.
+
+ Example::
+
+ {
+ "new": [
+ {
+ "id": "45502a6d-06e8-48a5-a215-2b4174b3614b",
+ "blueprint": "glusterfs",
+ "queue_status": "WAITING",
+ "timestamp": 1517362647.4570868,
+ "version": "0.0.6"
+ },
+ {
+ "id": "6d292bd0-bec7-4825-8d7d-41ef9c3e4b73",
+ "blueprint": "kubernetes",
+ "queue_status": "WAITING",
+ "timestamp": 1517362659.0034983,
+ "version": "0.0.1"
+ }
+ ],
+ "run": [
+ {
+ "id": "745712b2-96db-44c0-8014-fe925c35e795",
+ "blueprint": "glusterfs",
+ "queue_status": "RUNNING",
+ "timestamp": 1517362633.7965999,
+ "version": "0.0.6"
+ }
+ ]
+ }
+
+`/api/v0/compose/finished`
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Return the details on all of the finished composes on the system.
+
+ Example::
+
+ {
+ "finished": [
+ {
+ "id": "70b84195-9817-4b8a-af92-45e380f39894",
+ "blueprint": "glusterfs",
+ "queue_status": "FINISHED",
+ "timestamp": 1517351003.8210032,
+ "version": "0.0.6"
+ },
+ {
+ "id": "e695affd-397f-4af9-9022-add2636e7459",
+ "blueprint": "glusterfs",
+ "queue_status": "FINISHED",
+ "timestamp": 1517362289.7193348,
+ "version": "0.0.6"
+ }
+ ]
+ }
+
+`/api/v0/compose/failed`
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Return the details on all of the failed composes on the system.
+
+ Example::
+
+ {
+ "failed": [
+ {
+ "id": "8c8435ef-d6bd-4c68-9bf1-a2ef832e6b1a",
+ "blueprint": "http-server",
+ "queue_status": "FAILED",
+ "timestamp": 1517523249.9301329,
+ "version": "0.0.2"
+ }
+ ]
+ }
+
+`/api/v0/compose/status/<uuids>`
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Return the details for each of the comma-separated list of uuids.
+
+ Example::
+
+ {
+ "uuids": [
+ {
+ "id": "8c8435ef-d6bd-4c68-9bf1-a2ef832e6b1a",
+ "blueprint": "http-server",
+ "queue_status": "FINISHED",
+ "timestamp": 1517523644.2384307,
+ "version": "0.0.2"
+ },
+ {
+ "id": "45502a6d-06e8-48a5-a215-2b4174b3614b",
+ "blueprint": "glusterfs",
+ "queue_status": "FINISHED",
+ "timestamp": 1517363442.188399,
+ "version": "0.0.6"
+ }
+ ]
+ }
+
+DELETE `/api/v0/blueprints/cancel/<uuid>`
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Cancel the build, if it is not finished, and delete the results. It will return a
+ status of True if it is successful.
+
+ Example::
+
+ {
+ "status": true,
+ "uuid": "03397f8d-acff-4cdb-bd31-f629b7a948f5"
+ }
+
+DELETE `/api/v0/compose/delete/<uuids>`
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Delete the list of comma-separated uuids from the compose results.
+
+ Example::
+
+ {
+ "errors": [],
+ "uuids": [
+ {
+ "status": true,
+ "uuid": "ae1bf7e3-7f16-4c9f-b36e-3726a1093fd0"
+ }
+ ]
+ }
+
+`/api/v0/compose/info/<uuid>`
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Get detailed information about the compose. The returned JSON string will
+ contain the following information:
+
+ * id - The uuid of the comoposition
+ * config - containing the configuration settings used to run Anaconda
+ * blueprint - The depsolved blueprint used to generate the kickstart
+ * commit - The (local) git commit hash for the blueprint used
+ * deps - The NEVRA of all of the dependencies used in the composition
+ * compose_type - The type of output generated (tar, iso, etc.)
+ * queue_status - The final status of the composition (FINISHED or FAILED)
+
+ Example::
+
+ {
+ "commit": "7078e521a54b12eae31c3fd028680da7a0815a4d",
+ "compose_type": "tar",
+ "config": {
+ "anaconda_args": "",
+ "armplatform": "",
+ "compress_args": [],
+ "compression": "xz",
+ "image_name": "root.tar.xz",
+ ...
+ },
+ "deps": {
+ "packages": [
+ {
+ "arch": "x86_64",
+ "epoch": "0",
+ "name": "acl",
+ "release": "14.el7",
+ "version": "2.2.51"
+ }
+ ]
+ },
+ "id": "c30b7d80-523b-4a23-ad52-61b799739ce8",
+ "queue_status": "FINISHED",
+ "blueprint": {
+ "description": "An example kubernetes master",
+ ...
+ }
+ }
+
+`/api/v0/compose/metadata/<uuid>`
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Returns a .tar of the metadata used for the build. This includes all the
+ information needed to reproduce the build, including the final kickstart
+ populated with repository and package NEVRA.
+
+ The mime type is set to 'application/x-tar' and the filename is set to
+ UUID-metadata.tar
+
+ The .tar is uncompressed, but is not large.
+
+`/api/v0/compose/results/<uuid>`
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Returns a .tar of the metadata, logs, and output image of the build. This
+ includes all the information needed to reproduce the build, including the
+ final kickstart populated with repository and package NEVRA. The output image
+ is already in compressed form so the returned tar is not compressed.
+
+ The mime type is set to 'application/x-tar' and the filename is set to
+ UUID.tar
+
+`/api/v0/compose/logs/<uuid>`
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Returns a .tar of the anaconda build logs. The tar is not compressed, but is
+ not large.
+
+ The mime type is set to 'application/x-tar' and the filename is set to
+ UUID-logs.tar
+
+`/api/v0/compose/image/<uuid>`
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Returns the output image from the build. The filename is set to the filename
+ from the build with the UUID as a prefix. eg. UUID-root.tar.xz or UUID-boot.iso.
+
+`/api/v0/compose/log/<uuid>[?size=kbytes]`
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Returns the end of the anaconda.log. The size parameter is optional and defaults to 1Mbytes
+ if it is not included. The returned data is raw text from the end of the logfile, starting on
+ a line boundry.
+
+ Example::
+
+ 12:59:24,222 INFO anaconda: Running Thread: AnaConfigurationThread (140629395244800)
+ 12:59:24,223 INFO anaconda: Configuring installed system
+ 12:59:24,912 INFO anaconda: Configuring installed system
+ 12:59:24,912 INFO anaconda: Creating users
+ 12:59:24,913 INFO anaconda: Clearing libuser.conf at /tmp/libuser.Dyy8Gj
+ 12:59:25,154 INFO anaconda: Creating users
+ 12:59:25,155 INFO anaconda: Configuring addons
+ 12:59:25,155 INFO anaconda: Configuring addons
+ 12:59:25,155 INFO anaconda: Generating initramfs
+ 12:59:49,467 INFO anaconda: Generating initramfs
+ 12:59:49,467 INFO anaconda: Running post-installation scripts
+ 12:59:49,467 INFO anaconda: Running kickstart %%post script(s)
+ 12:59:50,782 INFO anaconda: All kickstart %%post script(s) have been run
+ 12:59:50,782 INFO anaconda: Running post-installation scripts
+ 12:59:50,784 INFO anaconda: Thread Done: AnaConfigurationThread (140629395244800)
+
+"""
+
+import logging
+log = logging.getLogger("lorax-composer")
+
+import os
+from flask import jsonify, request, Response, send_file
+
+from pylorax.api.compose import start_build, compose_types
+from pylorax.api.crossdomain import crossdomain
+from pylorax.api.projects import projects_list, projects_info, projects_depsolve
+from pylorax.api.projects import modules_list, modules_info, ProjectsError
+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 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.workspace import workspace_read, workspace_write, workspace_delete
+
+# The API functions don't actually get called by any code here
+# pylint: disable=unused-variable
+
+[docs]def take_limits(iterable, offset, limit):
+
""" Apply offset and limit to an iterable object
+
+
:param iterable: The object to limit
+
:type iterable: iter
+
:param offset: The number of items to skip
+
:type offset: int
+
:param limit: The total number of items to return
+
:type limit: int
+
:returns: A subset of the iterable
+
"""
+
return iterable[offset:][:limit]
+
+[docs]def v0_api(api):
+
# Note that Sphinx will not generate documentations for any of these.
+
@api.route("/api/v0/blueprints/list")
+
@crossdomain(origin="*")
+
def v0_blueprints_list():
+
"""List the available blueprints on a branch."""
+
branch = request.args.get("branch", "master")
+
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
+
+
with api.config["GITLOCK"].lock:
+
blueprints = take_limits([f[:-5] for f in 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/<blueprint_names>")
+
@crossdomain(origin="*")
+
def v0_blueprints_info(blueprint_names):
+
"""Return the contents of the blueprint, or a list of blueprints"""
+
branch = request.args.get("branch", "master")
+
out_fmt = request.args.get("format", "json")
+
blueprints = []
+
changes = []
+
errors = []
+
for blueprint_name in [n.strip() for n in blueprint_names.split(",")]:
+
exceptions = []
+
# Get the workspace version (if it exists)
+
try:
+
with api.config["GITLOCK"].lock:
+
ws_blueprint = workspace_read(api.config["GITLOCK"].repo, branch, blueprint_name)
+
except Exception as e:
+
ws_blueprint = None
+
exceptions.append(str(e))
+
log.error("(v0_blueprints_info) %s", str(e))
+
+
# Get the git version (if it exists)
+
try:
+
with api.config["GITLOCK"].lock:
+
git_blueprint = read_recipe_commit(api.config["GITLOCK"].repo, branch, blueprint_name)
+
except Exception as e:
+
git_blueprint = None
+
exceptions.append(str(e))
+
log.error("(v0_blueprints_info) %s", str(e))
+
+
if not ws_blueprint and not git_blueprint:
+
# Neither blueprint, return an error
+
errors.append("%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})
+
blueprints.append(ws_blueprint)
+
elif not ws_blueprint and git_blueprint:
+
# No workspace blueprint, no change, return the git blueprint
+
changes.append({"name":blueprint_name, "changed":False})
+
blueprints.append(git_blueprint)
+
else:
+
# Both exist, maybe changed, return the workspace blueprint
+
changes.append({"name":blueprint_name, "changed":ws_blueprint != git_blueprint})
+
blueprints.append(ws_blueprint)
+
+
# 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.
+
return "\n\n".join([r.toml() for r in blueprints])
+
else:
+
return jsonify(changes=changes, blueprints=blueprints, errors=errors)
+
+
@api.route("/api/v0/blueprints/changes/<blueprint_names>")
+
@crossdomain(origin="*")
+
def v0_blueprints_changes(blueprint_names):
+
"""Return the changes to a blueprint or list of blueprints"""
+
branch = request.args.get("branch", "master")
+
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
+
+
blueprints = []
+
errors = []
+
for blueprint_name in [n.strip() for n in blueprint_names.split(",")]:
+
filename = recipe_filename(blueprint_name)
+
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)))
+
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)
+
+
@api.route("/api/v0/blueprints/new", methods=["POST"])
+
@crossdomain(origin="*")
+
def v0_blueprints_new():
+
"""Commit a new blueprint"""
+
branch = request.args.get("branch", "master")
+
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))
+
+
with api.config["GITLOCK"].lock:
+
commit_recipe(api.config["GITLOCK"].repo, branch, blueprint)
+
+
# Read the blueprint with new version and write it to the workspace
+
blueprint = read_recipe_commit(api.config["GITLOCK"].repo, branch, blueprint["name"])
+
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
+
else:
+
return jsonify(status=True)
+
+
@api.route("/api/v0/blueprints/delete/<blueprint_name>", methods=["DELETE"])
+
@crossdomain(origin="*")
+
def v0_blueprints_delete(blueprint_name):
+
"""Delete a blueprint from git"""
+
branch = request.args.get("branch", "master")
+
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
+
else:
+
return jsonify(status=True)
+
+
@api.route("/api/v0/blueprints/workspace", methods=["POST"])
+
@crossdomain(origin="*")
+
def v0_blueprints_workspace():
+
"""Write a blueprint to the workspace"""
+
branch = request.args.get("branch", "master")
+
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))
+
+
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
+
else:
+
return jsonify(status=True)
+
+
@api.route("/api/v0/blueprints/workspace/<blueprint_name>", methods=["DELETE"])
+
@crossdomain(origin="*")
+
def v0_blueprints_delete_workspace(blueprint_name):
+
"""Delete a blueprint from the workspace"""
+
branch = request.args.get("branch", "master")
+
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
+
else:
+
return jsonify(status=True)
+
+
@api.route("/api/v0/blueprints/undo/<blueprint_name>/<commit>", methods=["POST"])
+
@crossdomain(origin="*")
+
def v0_blueprints_undo(blueprint_name, commit):
+
"""Undo changes to a blueprint by reverting to a previous commit."""
+
branch = request.args.get("branch", "master")
+
try:
+
with api.config["GITLOCK"].lock:
+
revert_recipe(api.config["GITLOCK"].repo, branch, blueprint_name, commit)
+
+
# Read the new recipe and write it to the workspace
+
blueprint = read_recipe_commit(api.config["GITLOCK"].repo, branch, blueprint_name)
+
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
+
else:
+
return jsonify(status=True)
+
+
@api.route("/api/v0/blueprints/tag/<blueprint_name>", methods=["POST"])
+
@crossdomain(origin="*")
+
def v0_blueprints_tag(blueprint_name):
+
"""Tag a blueprint's latest blueprint commit as a 'revision'"""
+
branch = request.args.get("branch", "master")
+
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
+
else:
+
return jsonify(status=True)
+
+
@api.route("/api/v0/blueprints/diff/<blueprint_name>/<from_commit>/<to_commit>")
+
@crossdomain(origin="*")
+
def v0_blueprints_diff(blueprint_name, from_commit, to_commit):
+
"""Return the differences between two commits of a blueprint"""
+
branch = request.args.get("branch", "master")
+
try:
+
if from_commit == "NEWEST":
+
with api.config["GITLOCK"].lock:
+
old_blueprint = read_recipe_commit(api.config["GITLOCK"].repo, branch, blueprint_name)
+
else:
+
with api.config["GITLOCK"].lock:
+
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
+
+
try:
+
if to_commit == "WORKSPACE":
+
with api.config["GITLOCK"].lock:
+
new_blueprint = workspace_read(api.config["GITLOCK"].repo, branch, blueprint_name)
+
# If there is no workspace, use the newest commit instead
+
if not new_blueprint:
+
with api.config["GITLOCK"].lock:
+
new_blueprint = read_recipe_commit(api.config["GITLOCK"].repo, branch, blueprint_name)
+
elif to_commit == "NEWEST":
+
with api.config["GITLOCK"].lock:
+
new_blueprint = read_recipe_commit(api.config["GITLOCK"].repo, branch, blueprint_name)
+
else:
+
with api.config["GITLOCK"].lock:
+
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
+
+
diff = recipe_diff(old_blueprint, new_blueprint)
+
return jsonify(diff=diff)
+
+
@api.route("/api/v0/blueprints/freeze/<blueprint_names>")
+
@crossdomain(origin="*")
+
def v0_blueprints_freeze(blueprint_names):
+
"""Return the blueprint with the exact modules and packages selected by depsolve"""
+
branch = request.args.get("branch", "master")
+
out_fmt = request.args.get("format", "json")
+
blueprints = []
+
errors = []
+
for blueprint_name in [n.strip() for n in sorted(blueprint_names.split(","), key=lambda n: n.lower())]:
+
# get the blueprint
+
# Get the workspace version (if it exists)
+
blueprint = None
+
try:
+
with api.config["GITLOCK"].lock:
+
blueprint = workspace_read(api.config["GITLOCK"].repo, branch, blueprint_name)
+
except Exception:
+
pass
+
+
if not blueprint:
+
# No workspace version, get the git version (if it exists)
+
try:
+
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)))
+
log.error("(v0_blueprints_freeze) %s", str(e))
+
+
# No blueprint found, skip it.
+
if not blueprint:
+
errors.append("%s: blueprint_not_found" % (blueprint_name))
+
continue
+
+
# Combine modules and packages and depsolve the list
+
# TODO include the version/glob in the depsolving
+
module_names = blueprint.module_names
+
package_names = blueprint.package_names
+
projects = sorted(set(module_names+package_names), key=lambda n: n.lower())
+
deps = []
+
try:
+
with api.config["DNFLOCK"].lock:
+
deps = projects_depsolve(api.config["DNFLOCK"].dbo, projects)
+
except ProjectsError as e:
+
errors.append("%s: %s" % (blueprint_name, str(e)))
+
log.error("(v0_blueprints_freeze) %s", str(e))
+
+
blueprints.append({"blueprint": blueprint.freeze(deps)})
+
+
if out_fmt == "toml":
+
# With TOML output we just want to dump the raw blueprint, skipping the rest.
+
return "\n\n".join([e["blueprint"].toml() for e in blueprints])
+
else:
+
return jsonify(blueprints=blueprints, errors=errors)
+
+
@api.route("/api/v0/blueprints/depsolve/<blueprint_names>")
+
@crossdomain(origin="*")
+
def v0_blueprints_depsolve(blueprint_names):
+
"""Return the dependencies for a blueprint"""
+
branch = request.args.get("branch", "master")
+
blueprints = []
+
errors = []
+
for blueprint_name in [n.strip() for n in sorted(blueprint_names.split(","), key=lambda n: n.lower())]:
+
# get the blueprint
+
# Get the workspace version (if it exists)
+
blueprint = None
+
try:
+
with api.config["GITLOCK"].lock:
+
blueprint = workspace_read(api.config["GITLOCK"].repo, branch, blueprint_name)
+
except Exception:
+
pass
+
+
if not blueprint:
+
# No workspace version, get the git version (if it exists)
+
try:
+
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)))
+
log.error("(v0_blueprints_depsolve) %s", str(e))
+
+
# No blueprint found, skip it.
+
if not blueprint:
+
errors.append("%s: blueprint not found" % blueprint_name)
+
continue
+
+
# Combine modules and packages and depsolve the list
+
# TODO include the version/glob in the depsolving
+
module_names = [m["name"] for m in blueprint["modules"] or []]
+
package_names = [p["name"] for p in blueprint["packages"] or []]
+
projects = sorted(set(module_names+package_names), key=lambda n: n.lower())
+
deps = []
+
try:
+
with api.config["DNFLOCK"].lock:
+
deps = projects_depsolve(api.config["DNFLOCK"].dbo, projects)
+
except ProjectsError as e:
+
errors.append("%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"
+
modules = []
+
for dep in deps:
+
if dep["name"] in projects:
+
modules.append(dep)
+
modules = sorted(modules, key=lambda m: m["name"].lower())
+
+
blueprints.append({"blueprint":blueprint, "dependencies":deps, "modules":modules})
+
+
return jsonify(blueprints=blueprints, errors=errors)
+
+
@api.route("/api/v0/projects/list")
+
@crossdomain(origin="*")
+
def v0_projects_list():
+
"""List all of the available projects/packages"""
+
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
+
+
try:
+
with api.config["DNFLOCK"].lock:
+
available = projects_list(api.config["DNFLOCK"].dbo)
+
except ProjectsError as e:
+
log.error("(v0_projects_list) %s", str(e))
+
return jsonify(status=False, errors=[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/<project_names>")
+
@crossdomain(origin="*")
+
def v0_projects_info(project_names):
+
"""Return detailed information about the listed projects"""
+
try:
+
with api.config["DNFLOCK"].lock:
+
projects = projects_info(api.config["DNFLOCK"].dbo, 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(projects=projects)
+
+
@api.route("/api/v0/projects/depsolve/<project_names>")
+
@crossdomain(origin="*")
+
def v0_projects_depsolve(project_names):
+
"""Return detailed information about the listed projects"""
+
try:
+
with api.config["DNFLOCK"].lock:
+
deps = projects_depsolve(api.config["DNFLOCK"].dbo, 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(projects=deps)
+
+
@api.route("/api/v0/modules/list")
+
@api.route("/api/v0/modules/list/<module_names>")
+
@crossdomain(origin="*")
+
def v0_modules_list(module_names=None):
+
"""List available modules, filtering by module_names"""
+
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
+
+
if module_names:
+
module_names = module_names.split(",")
+
+
try:
+
with api.config["DNFLOCK"].lock:
+
available = modules_list(api.config["DNFLOCK"].dbo, module_names)
+
except ProjectsError as e:
+
log.error("(v0_modules_list) %s", str(e))
+
return jsonify(status=False, errors=[str(e)]), 400
+
+
modules = take_limits(available, offset, limit)
+
return jsonify(modules=modules, offset=offset, limit=limit, total=len(available))
+
+
@api.route("/api/v0/modules/info/<module_names>")
+
@crossdomain(origin="*")
+
def v0_modules_info(module_names):
+
"""Return detailed information about the listed modules"""
+
try:
+
with api.config["DNFLOCK"].lock:
+
modules = modules_info(api.config["DNFLOCK"].dbo, 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(modules=modules)
+
+
@api.route("/api/v0/compose", methods=["POST"])
+
@crossdomain(origin="*")
+
def v0_compose_start():
+
"""Start a compose
+
+
The body of the post should have these fields:
+
blueprint_name - The blueprint name from /blueprints/list/
+
compose_type - The type of output to create, from /compose/types
+
branch - Optional, defaults to master, selects the git branch to use for the blueprint.
+
"""
+
# Passing ?test=1 will generate a fake FAILED compose.
+
# Passing ?test=2 will generate a fake FINISHED compose.
+
try:
+
test_mode = int(request.args.get("test", "0"))
+
except ValueError:
+
test_mode = 0
+
+
compose = request.get_json(cache=False)
+
+
errors = []
+
if not compose:
+
return jsonify(status=False, errors=["Missing POST body"]), 400
+
+
if "blueprint_name" not in compose:
+
errors.append("No 'blueprint_name' in the JSON request")
+
else:
+
blueprint_name = compose["blueprint_name"]
+
+
if "branch" not in compose or not compose["branch"]:
+
branch = "master"
+
else:
+
branch = compose["branch"]
+
+
if "compose_type" not in compose:
+
errors.append("No 'compose_type' in the JSON request")
+
else:
+
compose_type = compose["compose_type"]
+
+
if errors:
+
return jsonify(status=False, errors=errors), 400
+
+
try:
+
build_id = start_build(api.config["COMPOSER_CFG"], api.config["DNFLOCK"], api.config["GITLOCK"],
+
branch, blueprint_name, compose_type, test_mode)
+
except Exception as e:
+
return jsonify(status=False, errors=[str(e)]), 400
+
+
return jsonify(status=True, build_id=build_id)
+
+
@api.route("/api/v0/compose/types")
+
@crossdomain(origin="*")
+
def v0_compose_types():
+
"""Return the list of enabled output types
+
+
(only enabled types are returned)
+
"""
+
share_dir = api.config["COMPOSER_CFG"].get("composer", "share_dir")
+
return jsonify(types=[{"name": k, "enabled": True} for k in compose_types(share_dir)])
+
+
@api.route("/api/v0/compose/queue")
+
@crossdomain(origin="*")
+
def v0_compose_queue():
+
"""Return the status of the new and running queues"""
+
return jsonify(queue_status(api.config["COMPOSER_CFG"]))
+
+
@api.route("/api/v0/compose/finished")
+
@crossdomain(origin="*")
+
def v0_compose_finished():
+
"""Return the list of finished composes"""
+
return jsonify(finished=build_status(api.config["COMPOSER_CFG"], "FINISHED"))
+
+
@api.route("/api/v0/compose/failed")
+
@crossdomain(origin="*")
+
def v0_compose_failed():
+
"""Return the list of failed composes"""
+
return jsonify(failed=build_status(api.config["COMPOSER_CFG"], "FAILED"))
+
+
@api.route("/api/v0/compose/status/<uuids>")
+
@crossdomain(origin="*")
+
def v0_compose_status(uuids):
+
"""Return the status of the listed uuids"""
+
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)
+
+
return jsonify(uuids=results)
+
+
@api.route("/api/v0/compose/cancel/<uuid>", methods=["DELETE"])
+
@crossdomain(origin="*")
+
def v0_compose_cancel(uuid):
+
"""Cancel a running compose and delete its results directory"""
+
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
+
+
if status["queue_status"] not in ["WAITING", "RUNNING"]:
+
return jsonify(status=False, errors=["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
+
else:
+
return jsonify(status=True, uuid=uuid)
+
+
@api.route("/api/v0/compose/delete/<uuids>", methods=["DELETE"])
+
@crossdomain(origin="*")
+
def v0_compose_delete(uuids):
+
"""Delete the compose results for the listed uuids"""
+
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)
+
elif status["queue_status"] not in ["FINISHED", "FAILED"]:
+
errors.append("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)))
+
else:
+
results.append({"uuid":uuid, "status":True})
+
return jsonify(uuids=results, errors=errors)
+
+
@api.route("/api/v0/compose/info/<uuid>")
+
@crossdomain(origin="*")
+
def v0_compose_info(uuid):
+
"""Return detailed info about a compose"""
+
try:
+
info = uuid_info(api.config["COMPOSER_CFG"], uuid)
+
except Exception as e:
+
return jsonify(status=False, errors=[str(e)]), 400
+
+
return jsonify(**info)
+
+
@api.route("/api/v0/compose/metadata/<uuid>")
+
@crossdomain(origin="*")
+
def v0_compose_metadata(uuid):
+
"""Return a tar of the metadata for the build"""
+
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
+
if status["queue_status"] not in ["FINISHED", "FAILED"]:
+
return jsonify(status=False, errors=["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/<uuid>")
+
@crossdomain(origin="*")
+
def v0_compose_results(uuid):
+
"""Return a tar of the metadata and the results for the build"""
+
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
+
elif status["queue_status"] not in ["FINISHED", "FAILED"]:
+
return jsonify(status=False, errors=["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/<uuid>")
+
@crossdomain(origin="*")
+
def v0_compose_logs(uuid):
+
"""Return a tar of the metadata for the build"""
+
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
+
elif status["queue_status"] not in ["FINISHED", "FAILED"]:
+
return jsonify(status=False, errors=["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/<uuid>")
+
@crossdomain(origin="*")
+
def v0_compose_image(uuid):
+
"""Return the output image for the build"""
+
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
+
elif status["queue_status"] not in ["FINISHED", "FAILED"]:
+
return jsonify(status=False, errors=["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
+
+
# 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/<uuid>")
+
@crossdomain(origin="*")
+
def v0_compose_log_tail(uuid):
+
"""Return the end of the main anaconda.log, defaults to 1Mbytes"""
+
try:
+
size = int(request.args.get("size", "1024"))
+
except ValueError as e:
+
return jsonify(status=False, errors=[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
+
elif status["queue_status"] == "WAITING":
+
return jsonify(status=False, errors=["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
+