Add API routes for downloading build results
Also fix a bug with the name of the queue status in the status results (it is now 'queue_status' not 'status' which is used for error responses). This adds the following routes: - /compose/metadata/<uuid> to retrieve a .tar of the build metadata - /compose/results/<uuid> to retrieve .tar of all of the build results - /compose/logs/<uuid> to retrieve a .tar of just the logs from the build - /compose/image/<uuid> to retrieve the output image from the build
This commit is contained in:
parent
ed03ac7524
commit
6a95a314e9
@ -24,6 +24,7 @@ import pytoml as toml
|
|||||||
import pwd
|
import pwd
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
|
from subprocess import Popen, PIPE
|
||||||
import time
|
import time
|
||||||
from pykickstart.version import makeVersion, RHEL7
|
from pykickstart.version import makeVersion, RHEL7
|
||||||
from pykickstart.parser import KickstartParser
|
from pykickstart.parser import KickstartParser
|
||||||
@ -281,3 +282,67 @@ def uuid_info(cfg, uuid):
|
|||||||
"compose_type": compose_type,
|
"compose_type": compose_type,
|
||||||
"queue_status": status
|
"queue_status": status
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def uuid_tar(cfg, uuid, metadata=False, image=False, logs=False):
|
||||||
|
"""Return a tar of the build data
|
||||||
|
|
||||||
|
:param cfg: Configuration settings
|
||||||
|
:type cfg: ComposerConfig
|
||||||
|
:param uuid: The UUID of the build
|
||||||
|
:type uuid: str
|
||||||
|
:param metadata: Set to true to include all the metadata needed to reproduce the build
|
||||||
|
:type metadata: bool
|
||||||
|
:param image: Set to true to include the output image
|
||||||
|
:type image: bool
|
||||||
|
:param logs: Set to true to include the logs from the build
|
||||||
|
:type logs: bool
|
||||||
|
:returns: A stream of bytes from tar
|
||||||
|
:rtype: A generator
|
||||||
|
|
||||||
|
This yields an uncompressed tar's data to the caller. It includes
|
||||||
|
the selected data to the caller by returning the Popen stdout from the tar process.
|
||||||
|
"""
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Load the compose configuration
|
||||||
|
cfg_path = joinpaths(uuid_dir, "config.toml")
|
||||||
|
if not os.path.exists(cfg_path):
|
||||||
|
raise RuntimeError("Missing config.toml for %s" % uuid)
|
||||||
|
cfg_dict = toml.loads(open(cfg_path, "r").read())
|
||||||
|
image_name = cfg_dict["image_name"]
|
||||||
|
|
||||||
|
def include_file(f):
|
||||||
|
if f.endswith("/logs"):
|
||||||
|
return logs
|
||||||
|
if f.endswith(image_name):
|
||||||
|
return image
|
||||||
|
return metadata
|
||||||
|
filenames = [os.path.basename(f) for f in glob(joinpaths(uuid_dir, "*")) if include_file(f)]
|
||||||
|
|
||||||
|
tar = Popen(["tar", "-C", uuid_dir, "-cf-"] + filenames, stdout=PIPE)
|
||||||
|
return tar.stdout
|
||||||
|
|
||||||
|
def uuid_image(cfg, uuid):
|
||||||
|
"""Return the filename and full path of the build's image file
|
||||||
|
|
||||||
|
:param cfg: Configuration settings
|
||||||
|
:type cfg: ComposerConfig
|
||||||
|
:param uuid: The UUID of the build
|
||||||
|
:type uuid: str
|
||||||
|
:returns: The image filename and full path
|
||||||
|
:rtype: tuple of strings
|
||||||
|
"""
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Load the compose configuration
|
||||||
|
cfg_path = joinpaths(uuid_dir, "config.toml")
|
||||||
|
if not os.path.exists(cfg_path):
|
||||||
|
raise RuntimeError("Missing config.toml for %s" % uuid)
|
||||||
|
cfg_dict = toml.loads(open(cfg_path, "r").read())
|
||||||
|
image_name = cfg_dict["image_name"]
|
||||||
|
|
||||||
|
return (image_name, joinpaths(uuid_dir, image_name))
|
||||||
|
@ -625,14 +625,14 @@ POST `/api/v0/recipes/tag/<recipe_name>`
|
|||||||
{
|
{
|
||||||
"id": "45502a6d-06e8-48a5-a215-2b4174b3614b",
|
"id": "45502a6d-06e8-48a5-a215-2b4174b3614b",
|
||||||
"recipe": "glusterfs",
|
"recipe": "glusterfs",
|
||||||
"status": "WAITING",
|
"queue_status": "WAITING",
|
||||||
"timestamp": 1517362647.4570868,
|
"timestamp": 1517362647.4570868,
|
||||||
"version": "0.0.6"
|
"version": "0.0.6"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "6d292bd0-bec7-4825-8d7d-41ef9c3e4b73",
|
"id": "6d292bd0-bec7-4825-8d7d-41ef9c3e4b73",
|
||||||
"recipe": "kubernetes",
|
"recipe": "kubernetes",
|
||||||
"status": "WAITING",
|
"queue_status": "WAITING",
|
||||||
"timestamp": 1517362659.0034983,
|
"timestamp": 1517362659.0034983,
|
||||||
"version": "0.0.1"
|
"version": "0.0.1"
|
||||||
}
|
}
|
||||||
@ -641,7 +641,7 @@ POST `/api/v0/recipes/tag/<recipe_name>`
|
|||||||
{
|
{
|
||||||
"id": "745712b2-96db-44c0-8014-fe925c35e795",
|
"id": "745712b2-96db-44c0-8014-fe925c35e795",
|
||||||
"recipe": "glusterfs",
|
"recipe": "glusterfs",
|
||||||
"status": "RUNNING",
|
"queue_status": "RUNNING",
|
||||||
"timestamp": 1517362633.7965999,
|
"timestamp": 1517362633.7965999,
|
||||||
"version": "0.0.6"
|
"version": "0.0.6"
|
||||||
}
|
}
|
||||||
@ -660,14 +660,14 @@ POST `/api/v0/recipes/tag/<recipe_name>`
|
|||||||
{
|
{
|
||||||
"id": "70b84195-9817-4b8a-af92-45e380f39894",
|
"id": "70b84195-9817-4b8a-af92-45e380f39894",
|
||||||
"recipe": "glusterfs",
|
"recipe": "glusterfs",
|
||||||
"status": "FINISHED",
|
"queue_status": "FINISHED",
|
||||||
"timestamp": 1517351003.8210032,
|
"timestamp": 1517351003.8210032,
|
||||||
"version": "0.0.6"
|
"version": "0.0.6"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "e695affd-397f-4af9-9022-add2636e7459",
|
"id": "e695affd-397f-4af9-9022-add2636e7459",
|
||||||
"recipe": "glusterfs",
|
"recipe": "glusterfs",
|
||||||
"status": "FINISHED",
|
"queue_status": "FINISHED",
|
||||||
"timestamp": 1517362289.7193348,
|
"timestamp": 1517362289.7193348,
|
||||||
"version": "0.0.6"
|
"version": "0.0.6"
|
||||||
}
|
}
|
||||||
@ -686,7 +686,7 @@ POST `/api/v0/recipes/tag/<recipe_name>`
|
|||||||
{
|
{
|
||||||
"id": "8c8435ef-d6bd-4c68-9bf1-a2ef832e6b1a",
|
"id": "8c8435ef-d6bd-4c68-9bf1-a2ef832e6b1a",
|
||||||
"recipe": "http-server",
|
"recipe": "http-server",
|
||||||
"status": "RUNNING",
|
"queue_status": "FAILED",
|
||||||
"timestamp": 1517523249.9301329,
|
"timestamp": 1517523249.9301329,
|
||||||
"version": "0.0.2"
|
"version": "0.0.2"
|
||||||
}
|
}
|
||||||
@ -705,14 +705,14 @@ POST `/api/v0/recipes/tag/<recipe_name>`
|
|||||||
{
|
{
|
||||||
"id": "8c8435ef-d6bd-4c68-9bf1-a2ef832e6b1a",
|
"id": "8c8435ef-d6bd-4c68-9bf1-a2ef832e6b1a",
|
||||||
"recipe": "http-server",
|
"recipe": "http-server",
|
||||||
"status": "FINISHED",
|
"queue_status": "FINISHED",
|
||||||
"timestamp": 1517523644.2384307,
|
"timestamp": 1517523644.2384307,
|
||||||
"version": "0.0.2"
|
"version": "0.0.2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "45502a6d-06e8-48a5-a215-2b4174b3614b",
|
"id": "45502a6d-06e8-48a5-a215-2b4174b3614b",
|
||||||
"recipe": "glusterfs",
|
"recipe": "glusterfs",
|
||||||
"status": "FINISHED",
|
"queue_status": "FINISHED",
|
||||||
"timestamp": 1517363442.188399,
|
"timestamp": 1517363442.188399,
|
||||||
"version": "0.0.6"
|
"version": "0.0.6"
|
||||||
}
|
}
|
||||||
@ -782,20 +782,57 @@ DELETE `/api/v0/compose/delete/<uuids>`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
`/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. eg. root.tar.xz or boot.iso.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
log = logging.getLogger("lorax-composer")
|
log = logging.getLogger("lorax-composer")
|
||||||
|
|
||||||
from flask import jsonify, request
|
from flask import jsonify, request, Response, send_file
|
||||||
|
|
||||||
from pylorax.api.compose import start_build, compose_types
|
from pylorax.api.compose import start_build, compose_types
|
||||||
from pylorax.api.crossdomain import crossdomain
|
from pylorax.api.crossdomain import crossdomain
|
||||||
from pylorax.api.projects import projects_list, projects_info, projects_depsolve
|
from pylorax.api.projects import projects_list, projects_info, projects_depsolve
|
||||||
from pylorax.api.projects import modules_list, modules_info, ProjectsError
|
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 queue_status, build_status, uuid_delete, uuid_status, uuid_info
|
||||||
|
from pylorax.api.queue import uuid_tar, uuid_image
|
||||||
from pylorax.api.recipes import list_branch_files, read_recipe_commit, recipe_filename, list_commits
|
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 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.recipes import tag_recipe_commit, recipe_diff
|
||||||
@ -1335,7 +1372,7 @@ def v0_api(api):
|
|||||||
errors = []
|
errors = []
|
||||||
for uuid in [n.strip().lower() for n in uuids.split(",")]:
|
for uuid in [n.strip().lower() for n in uuids.split(",")]:
|
||||||
status = uuid_status(api.config["COMPOSER_CFG"], uuid)
|
status = uuid_status(api.config["COMPOSER_CFG"], uuid)
|
||||||
if status["status"] not in ["FINISHED", "FAILED"]:
|
if status["queue_status"] not in ["FINISHED", "FAILED"]:
|
||||||
errors.append({"uuid":uuid, "msg":"Build not in FINISHED or FAILED."})
|
errors.append({"uuid":uuid, "msg":"Build not in FINISHED or FAILED."})
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
@ -1356,3 +1393,55 @@ def v0_api(api):
|
|||||||
return jsonify(status=False, msg=str(e))
|
return jsonify(status=False, msg=str(e))
|
||||||
|
|
||||||
return jsonify(**info)
|
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["queue_status"] not in ["FINISHED", "FAILED"]:
|
||||||
|
return jsonify({"status":False, "uuid":uuid, "msg":"Build not in FINISHED or FAILED."})
|
||||||
|
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["queue_status"] not in ["FINISHED", "FAILED"]:
|
||||||
|
return jsonify({"status":False, "uuid":uuid, "msg":"Build not in FINISHED or FAILED."})
|
||||||
|
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["queue_status"] not in ["FINISHED", "FAILED"]:
|
||||||
|
return jsonify({"status":False, "uuid":uuid, "msg":"Build not in FINISHED or FAILED."})
|
||||||
|
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["queue_status"] not in ["FINISHED", "FAILED"]:
|
||||||
|
return jsonify({"status":False, "uuid":uuid, "msg":"Build not in FINISHED or FAILED."})
|
||||||
|
else:
|
||||||
|
image_name, image_path = uuid_image(api.config["COMPOSER_CFG"], uuid)
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
Loading…
Reference in New Issue
Block a user