Make sure V0 API doesn't return uploads information

uploads should only be included in the V1 API routes, add `api`
selection to the relevant helper functions and calls to them from v0.py

Add new V1 routes with api=1 to include the uploads information in the
results.

Also add tests to ensure that V0 requests do not include uploads.
This commit is contained in:
Brian C. Lane 2019-08-27 14:13:01 -07:00
parent 2692e8138c
commit 3a453eaad7
5 changed files with 373 additions and 49 deletions

View File

@ -305,11 +305,15 @@ def get_compose_type(results_dir):
raise RuntimeError("Cannot find ks template for build %s" % os.path.basename(results_dir)) raise RuntimeError("Cannot find ks template for build %s" % os.path.basename(results_dir))
return t[0] return t[0]
def compose_detail(cfg, results_dir): def compose_detail(cfg, results_dir, api=1):
"""Return details about the build. """Return details about the build.
:param cfg: Configuration settings (required for api=1)
:type cfg: ComposerConfig
:param results_dir: The directory containing the metadata and results for the build :param results_dir: The directory containing the metadata and results for the build
:type results_dir: str :type results_dir: str
:param api: Select which api version of the dict to return (default 1)
:type api: int
:returns: A dictionary with details about the compose :returns: A dictionary with details about the compose
:rtype: dict :rtype: dict
:raises: IOError if it cannot read the directory, STATUS, or blueprint file. :raises: IOError if it cannot read the directory, STATUS, or blueprint file.
@ -322,6 +326,7 @@ def compose_detail(cfg, results_dir):
* blueprint - Blueprint name * blueprint - Blueprint name
* version - Blueprint version * version - Blueprint version
* image_size - Size of the image, if finished. 0 otherwise. * image_size - Size of the image, if finished. 0 otherwise.
* uploads - For API v1 details about uploading the image are included
Various timestamps are also included in the dict. These are all Unix UTC timestamps. 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 It is possible for these timestamps to not always exist, in which case they will be
@ -345,26 +350,31 @@ def compose_detail(cfg, results_dir):
times = timestamp_dict(results_dir) times = timestamp_dict(results_dir)
upload_uuids = uuid_get_uploads(cfg, build_id) detail = {"id": build_id,
summaries = [upload.summary() for upload in get_uploads(cfg["upload"], upload_uuids)] "queue_status": status,
"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"],
"image_size": image_size,
}
return {"id": build_id, if api == 1:
"queue_status": status, # Get uploads for this build_id
"job_created": times.get(TS_CREATED), upload_uuids = uuid_get_uploads(cfg, build_id)
"job_started": times.get(TS_STARTED), summaries = [upload.summary() for upload in get_uploads(cfg["upload"], upload_uuids)]
"job_finished": times.get(TS_FINISHED), detail["uploads"] = summaries
"compose_type": compose_type, return detail
"blueprint": blueprint["name"],
"version": blueprint["version"],
"image_size": image_size,
"uploads": summaries,
}
def queue_status(cfg): def queue_status(cfg, api=1):
"""Return details about what is in the queue. """Return details about what is in the queue.
:param cfg: Configuration settings :param cfg: Configuration settings
:type cfg: ComposerConfig :type cfg: ComposerConfig
:param api: Select which api version of the dict to return (default 1)
:type api: int
:returns: A list of the new composes, and a list of the running composes :returns: A list of the new composes, and a list of the running composes
:rtype: dict :rtype: dict
@ -378,7 +388,7 @@ def queue_status(cfg):
new_details = [] new_details = []
for n in new_queue: for n in new_queue:
try: try:
d = compose_detail(cfg, n) d = compose_detail(cfg, n, api)
except IOError: except IOError:
continue continue
new_details.append(d) new_details.append(d)
@ -386,7 +396,7 @@ def queue_status(cfg):
run_details = [] run_details = []
for r in run_queue: for r in run_queue:
try: try:
d = compose_detail(cfg, r) d = compose_detail(cfg, r, api)
except IOError: except IOError:
continue continue
run_details.append(d) run_details.append(d)
@ -396,32 +406,36 @@ def queue_status(cfg):
"run": run_details "run": run_details
} }
def uuid_status(cfg, uuid): def uuid_status(cfg, uuid, api=1):
"""Return the details of a specific UUID compose """Return the details of a specific UUID compose
:param cfg: Configuration settings :param cfg: Configuration settings
:type cfg: ComposerConfig :type cfg: ComposerConfig
:param uuid: The UUID of the build :param uuid: The UUID of the build
:type uuid: str :type uuid: str
:param api: Select which api version of the dict to return (default 1)
:type api: int
:returns: Details about the build :returns: Details about the build
:rtype: dict or None :rtype: dict or None
Returns the same dict as `compose_details()` Returns the same dict as `compose_detail()`
""" """
uuid_dir = joinpaths(cfg.get("composer", "lib_dir"), "results", uuid) uuid_dir = joinpaths(cfg.get("composer", "lib_dir"), "results", uuid)
try: try:
return compose_detail(cfg, uuid_dir) return compose_detail(cfg, uuid_dir, api)
except IOError: except IOError:
return None return None
def build_status(cfg, status_filter=None): def build_status(cfg, status_filter=None, api=1):
"""Return the details of finished or failed builds """Return the details of finished or failed builds
:param cfg: Configuration settings :param cfg: Configuration settings
:type cfg: ComposerConfig :type cfg: ComposerConfig
:param status_filter: What builds to return. None == all, "FINISHED", or "FAILED" :param status_filter: What builds to return. None == all, "FINISHED", or "FAILED"
:type status_filter: str :type status_filter: str
:returns: A list of the build details (from compose_details) :param api: Select which api version of the dict to return (default 1)
:type api: int
:returns: A list of the build details (from compose_detail)
:rtype: list of dicts :rtype: list of dicts
This returns a list of build details for each of the matching builds on the This returns a list of build details for each of the matching builds on the
@ -441,7 +455,7 @@ def build_status(cfg, status_filter=None):
try: try:
status = open(joinpaths(build, "STATUS"), "r").read().strip() status = open(joinpaths(build, "STATUS"), "r").read().strip()
if status in status_filter: if status in status_filter:
results.append(compose_detail(cfg, build)) results.append(compose_detail(cfg, build, api))
except IOError: except IOError:
pass pass
return results return results
@ -573,7 +587,7 @@ def uuid_delete(cfg, uuid):
shutil.rmtree(uuid_dir) shutil.rmtree(uuid_dir)
return True return True
def uuid_info(cfg, uuid): def uuid_info(cfg, uuid, api=1):
"""Return information about the composition """Return information about the composition
:param cfg: Configuration settings :param cfg: Configuration settings
@ -614,17 +628,14 @@ def uuid_info(cfg, uuid):
raise RuntimeError("Missing deps.toml for %s" % uuid) raise RuntimeError("Missing deps.toml for %s" % uuid)
deps_dict = toml.loads(open(deps_path, "r").read()) deps_dict = toml.loads(open(deps_path, "r").read())
details = compose_detail(cfg, uuid_dir) details = compose_detail(cfg, uuid_dir, api)
commit_path = joinpaths(uuid_dir, "COMMIT") commit_path = joinpaths(uuid_dir, "COMMIT")
if not os.path.exists(commit_path): if not os.path.exists(commit_path):
raise RuntimeError("Missing commit hash for %s" % uuid) raise RuntimeError("Missing commit hash for %s" % uuid)
commit_id = open(commit_path, "r").read().strip() commit_id = open(commit_path, "r").read().strip()
upload_uuids = uuid_get_uploads(cfg, uuid) info = {"id": uuid,
summaries = [upload.summary() for upload in get_uploads(cfg["upload"], upload_uuids)]
return {"id": uuid,
"config": cfg_dict, "config": cfg_dict,
"blueprint": frozen_dict, "blueprint": frozen_dict,
"commit": commit_id, "commit": commit_id,
@ -632,8 +643,12 @@ def uuid_info(cfg, uuid):
"compose_type": details["compose_type"], "compose_type": details["compose_type"],
"queue_status": details["queue_status"], "queue_status": details["queue_status"],
"image_size": details["image_size"], "image_size": details["image_size"],
"uploads": summaries,
} }
if api == 1:
upload_uuids = uuid_get_uploads(cfg, uuid)
summaries = [upload.summary() for upload in get_uploads(cfg["upload"], upload_uuids)]
info["uploads"] = summaries
return info
def uuid_tar(cfg, uuid, metadata=False, image=False, logs=False): def uuid_tar(cfg, uuid, metadata=False, image=False, logs=False):
"""Return a tar of the build data """Return a tar of the build data

View File

@ -91,6 +91,11 @@ server.register_blueprint(v0_api, url_prefix="/api/v0/")
# Use v0 routes by default # Use v0 routes by default
skip_rules = [ skip_rules = [
"/compose", "/compose",
"/compose/queue",
"/compose/finished",
"/compose/failed",
"/compose/status/<uuids>",
"/compose/info/<uuid>",
"/projects/source/info/<source_names>", "/projects/source/info/<source_names>",
"/projects/source/new", "/projects/source/new",
] ]

View File

@ -1537,7 +1537,7 @@ def v0_compose_queue():
] ]
} }
""" """
return jsonify(queue_status(api.config["COMPOSER_CFG"])) return jsonify(queue_status(api.config["COMPOSER_CFG"], api=0))
@v0_api.route("/compose/finished") @v0_api.route("/compose/finished")
def v0_compose_finished(): def v0_compose_finished():
@ -1572,7 +1572,7 @@ def v0_compose_finished():
] ]
} }
""" """
return jsonify(finished=build_status(api.config["COMPOSER_CFG"], "FINISHED")) return jsonify(finished=build_status(api.config["COMPOSER_CFG"], "FINISHED", api=0))
@v0_api.route("/compose/failed") @v0_api.route("/compose/failed")
def v0_compose_failed(): def v0_compose_failed():
@ -1598,7 +1598,7 @@ def v0_compose_failed():
] ]
} }
""" """
return jsonify(failed=build_status(api.config["COMPOSER_CFG"], "FAILED")) return jsonify(failed=build_status(api.config["COMPOSER_CFG"], "FAILED", api=0))
@v0_api.route("/compose/status", defaults={'uuids': ""}) @v0_api.route("/compose/status", defaults={'uuids': ""})
@v0_api.route("/compose/status/<uuids>") @v0_api.route("/compose/status/<uuids>")
@ -1647,14 +1647,14 @@ def v0_compose_status(uuids):
errors = [] errors = []
if uuids.strip() == '*': if uuids.strip() == '*':
queue_status_dict = queue_status(api.config["COMPOSER_CFG"]) queue_status_dict = queue_status(api.config["COMPOSER_CFG"], api=0)
queue_new = queue_status_dict["new"] queue_new = queue_status_dict["new"]
queue_running = queue_status_dict["run"] queue_running = queue_status_dict["run"]
candidates = queue_new + queue_running + build_status(api.config["COMPOSER_CFG"]) candidates = queue_new + queue_running + build_status(api.config["COMPOSER_CFG"], api=0)
else: else:
candidates = [] candidates = []
for uuid in [n.strip().lower() for n in uuids.split(",")]: for uuid in [n.strip().lower() for n in uuids.split(",")]:
details = uuid_status(api.config["COMPOSER_CFG"], uuid) details = uuid_status(api.config["COMPOSER_CFG"], uuid, api=0)
if details is None: if details is None:
errors.append({"id": UNKNOWN_UUID, "msg": "%s is not a valid build uuid" % uuid}) errors.append({"id": UNKNOWN_UUID, "msg": "%s is not a valid build uuid" % uuid})
else: else:
@ -1695,7 +1695,7 @@ def v0_compose_cancel(uuid):
if VALID_API_STRING.match(uuid) is None: if VALID_API_STRING.match(uuid) is None:
return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in API path"}]), 400 return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in API path"}]), 400
status = uuid_status(api.config["COMPOSER_CFG"], uuid) status = uuid_status(api.config["COMPOSER_CFG"], uuid, api=0)
if status is None: if status is None:
return jsonify(status=False, errors=[{"id": UNKNOWN_UUID, "msg": "%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
@ -1737,7 +1737,7 @@ def v0_compose_delete(uuids):
results = [] results = []
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, api=0)
if status is None: if status is None:
errors.append({"id": UNKNOWN_UUID, "msg": "%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"]: elif status["queue_status"] not in ["FINISHED", "FAILED"]:
@ -1806,7 +1806,7 @@ def v0_compose_info(uuid):
return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in API path"}]), 400 return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in API path"}]), 400
try: try:
info = uuid_info(api.config["COMPOSER_CFG"], uuid) info = uuid_info(api.config["COMPOSER_CFG"], uuid, api=0)
except Exception as e: except Exception as e:
return jsonify(status=False, errors=[{"id": COMPOSE_ERROR, "msg": str(e)}]), 400 return jsonify(status=False, errors=[{"id": COMPOSE_ERROR, "msg": str(e)}]), 400
@ -1835,7 +1835,7 @@ def v0_compose_metadata(uuid):
if VALID_API_STRING.match(uuid) is None: if VALID_API_STRING.match(uuid) is None:
return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in API path"}]), 400 return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in API path"}]), 400
status = uuid_status(api.config["COMPOSER_CFG"], uuid) status = uuid_status(api.config["COMPOSER_CFG"], uuid, api=0)
if status is None: if status is None:
return jsonify(status=False, errors=[{"id": UNKNOWN_UUID, "msg": "%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"]: if status["queue_status"] not in ["FINISHED", "FAILED"]:
@ -1865,7 +1865,7 @@ def v0_compose_results(uuid):
if VALID_API_STRING.match(uuid) is None: if VALID_API_STRING.match(uuid) is None:
return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in API path"}]), 400 return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in API path"}]), 400
status = uuid_status(api.config["COMPOSER_CFG"], uuid) status = uuid_status(api.config["COMPOSER_CFG"], uuid, api=0)
if status is None: if status is None:
return jsonify(status=False, errors=[{"id": UNKNOWN_UUID, "msg": "%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"]: elif status["queue_status"] not in ["FINISHED", "FAILED"]:
@ -1893,7 +1893,7 @@ def v0_compose_logs(uuid):
if VALID_API_STRING.match(uuid) is None: if VALID_API_STRING.match(uuid) is None:
return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in API path"}]), 400 return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in API path"}]), 400
status = uuid_status(api.config["COMPOSER_CFG"], uuid) status = uuid_status(api.config["COMPOSER_CFG"], uuid, api=0)
if status is None: if status is None:
return jsonify(status=False, errors=[{"id": UNKNOWN_UUID, "msg": "%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"]: elif status["queue_status"] not in ["FINISHED", "FAILED"]:
@ -1918,7 +1918,7 @@ def v0_compose_image(uuid):
if VALID_API_STRING.match(uuid) is None: if VALID_API_STRING.match(uuid) is None:
return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in API path"}]), 400 return jsonify(status=False, errors=[{"id": INVALID_CHARS, "msg": "Invalid characters in API path"}]), 400
status = uuid_status(api.config["COMPOSER_CFG"], uuid) status = uuid_status(api.config["COMPOSER_CFG"], uuid, api=0)
if status is None: if status is None:
return jsonify(status=False, errors=[{"id": UNKNOWN_UUID, "msg": "%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"]: elif status["queue_status"] not in ["FINISHED", "FAILED"]:
@ -1975,7 +1975,7 @@ def v0_compose_log_tail(uuid):
except ValueError as e: except ValueError as e:
return jsonify(status=False, errors=[{"id": COMPOSE_ERROR, "msg": str(e)}]), 400 return jsonify(status=False, errors=[{"id": COMPOSE_ERROR, "msg": str(e)}]), 400
status = uuid_status(api.config["COMPOSER_CFG"], uuid) status = uuid_status(api.config["COMPOSER_CFG"], uuid, api=0)
if status is None: if status is None:
return jsonify(status=False, errors=[{"id": UNKNOWN_UUID, "msg": "%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": elif status["queue_status"] == "WAITING":

View File

@ -29,8 +29,10 @@ from pylorax.api.checkparams import checkparams
from pylorax.api.compose import start_build from pylorax.api.compose import start_build
from pylorax.api.errors import BAD_COMPOSE_TYPE, BUILD_FAILED, INVALID_CHARS, MISSING_POST, PROJECTS_ERROR from pylorax.api.errors import BAD_COMPOSE_TYPE, BUILD_FAILED, INVALID_CHARS, MISSING_POST, PROJECTS_ERROR
from pylorax.api.errors import SYSTEM_SOURCE, UNKNOWN_BLUEPRINT, UNKNOWN_SOURCE, UNKNOWN_UUID, UPLOAD_ERROR from pylorax.api.errors import SYSTEM_SOURCE, UNKNOWN_BLUEPRINT, UNKNOWN_SOURCE, UNKNOWN_UUID, UPLOAD_ERROR
from pylorax.api.errors import COMPOSE_ERROR
from pylorax.api.flask_blueprint import BlueprintSkip from pylorax.api.flask_blueprint import BlueprintSkip
from pylorax.api.queue import uuid_status, uuid_schedule_upload, uuid_remove_upload from pylorax.api.queue import queue_status, build_status, uuid_status, uuid_schedule_upload, uuid_remove_upload
from pylorax.api.queue import uuid_info
from pylorax.api.projects import get_repo_sources, repo_to_source from pylorax.api.projects import get_repo_sources, repo_to_source
from pylorax.api.projects import new_repo_source from pylorax.api.projects import new_repo_source
from pylorax.api.regexes import VALID_API_STRING, VALID_BLUEPRINT_NAME from pylorax.api.regexes import VALID_API_STRING, VALID_BLUEPRINT_NAME
@ -115,7 +117,7 @@ def v1_projects_source_info(source_ids):
def v1_projects_source_new(): def v1_projects_source_new():
"""Add a new package source. Or change an existing one """Add a new package source. Or change an existing one
**POST /api/v0/projects/source/new** **POST /api/v1/projects/source/new**
Add (or change) a source for use when depsolving blueprints and composing images. Add (or change) a source for use when depsolving blueprints and composing images.
@ -184,7 +186,7 @@ def v1_compose_start():
compose_type - The type of output to create, from /compose/types 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. branch - Optional, defaults to master, selects the git branch to use for the blueprint.
**POST /api/v0/compose** **POST /api/v1/compose**
Start a compose. The content type should be 'application/json' and the body of the POST Start a compose. The content type should be 'application/json' and the body of the POST
should look like this. The "upload" object is optional. should look like this. The "upload" object is optional.
@ -212,7 +214,7 @@ def v1_compose_start():
} }
Pass it the name of the blueprint, the type of output (from Pass it the name of the blueprint, the type of output (from
'/api/v0/compose/types'), and the blueprint branch to use. 'branch' is '/api/v1/compose/types'), and the blueprint branch to use. 'branch' is
optional and will default to master. It will create a new build and add 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. it to the queue. It returns the build uuid and a status if it succeeds.
If an "upload" is given, it will schedule an upload to run when the build If an "upload" is given, it will schedule an upload to run when the build
@ -222,6 +224,7 @@ def v1_compose_start():
{ {
"build_id": "e6fa6db4-9c81-4b70-870f-a697ca405cdf", "build_id": "e6fa6db4-9c81-4b70-870f-a697ca405cdf",
"upload_uuid": "572eb0d0-5348-4600-9666-14526ba628bb",
"status": true "status": true
} }
""" """
@ -294,8 +297,251 @@ def v1_compose_start():
image_name, image_name,
settings settings
) )
else:
upload_uuid = ""
return jsonify(status=True, build_id=build_id) return jsonify(status=True, build_id=build_id, upload_id=upload_uuid)
@v1_api.route("/compose/queue")
def v1_compose_queue():
"""Return the status of the new and running queues
**/api/v1/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",
"job_created": 1517362647.4570868,
"version": "0.0.6"
},
{
"id": "6d292bd0-bec7-4825-8d7d-41ef9c3e4b73",
"blueprint": "kubernetes",
"queue_status": "WAITING",
"job_created": 1517362659.0034983,
"version": "0.0.1"
}
],
"run": [
{
"id": "745712b2-96db-44c0-8014-fe925c35e795",
"blueprint": "glusterfs",
"queue_status": "RUNNING",
"job_created": 1517362633.7965999,
"job_started": 1517362633.8001345,
"version": "0.0.6"
}
]
}
"""
return jsonify(queue_status(api.config["COMPOSER_CFG"], api=1))
@v1_api.route("/compose/finished")
def v1_compose_finished():
"""Return the list of finished composes
**/api/v1/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",
"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",
"job_created": 1517362289.7193348,
"job_started": 1517362289.9751132,
"job_finished": 1517363500.1234567,
"version": "0.0.6"
}
]
}
"""
return jsonify(finished=build_status(api.config["COMPOSER_CFG"], "FINISHED", api=1))
@v1_api.route("/compose/failed")
def v1_compose_failed():
"""Return the list of failed composes
**/api/v1/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",
"job_created": 1517523249.9301329,
"job_started": 1517523249.9314211,
"job_finished": 1517523255.5623411,
"version": "0.0.2"
}
]
}
"""
return jsonify(failed=build_status(api.config["COMPOSER_CFG"], "FAILED", api=1))
@v1_api.route("/compose/status", defaults={'uuids': ""})
@v1_api.route("/compose/status/<uuids>")
@checkparams([("uuids", "", "no UUIDs given")])
def v1_compose_status(uuids):
"""Return the status of the listed uuids
**/api/v1/compose/status/<uuids>[?blueprint=<blueprint_name>&status=<compose_status>&type=<compose_type>]**
Return the details for each of the comma-separated list of uuids. A uuid of '*' will return
details for all composes.
Example::
{
"uuids": [
{
"id": "8c8435ef-d6bd-4c68-9bf1-a2ef832e6b1a",
"blueprint": "http-server",
"queue_status": "FINISHED",
"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",
"job_created": 1517363442.188399,
"job_started": 1517363442.325324,
"job_finished": 1517363451.653621,
"version": "0.0.6"
}
]
}
"""
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 = []
errors = []
if uuids.strip() == '*':
queue_status_dict = queue_status(api.config["COMPOSER_CFG"], api=1)
queue_new = queue_status_dict["new"]
queue_running = queue_status_dict["run"]
candidates = queue_new + queue_running + build_status(api.config["COMPOSER_CFG"], api=1)
else:
candidates = []
for uuid in [n.strip().lower() for n in uuids.split(",")]:
details = uuid_status(api.config["COMPOSER_CFG"], uuid, api=1)
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)
@v1_api.route("/compose/info", defaults={'uuid': ""})
@v1_api.route("/compose/info/<uuid>")
@checkparams([("uuid", "", "no UUID given")])
def v1_compose_info(uuid):
"""Return detailed info about a compose
**/api/v1/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",
...
}
}
"""
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, api=1)
except Exception as e:
return jsonify(status=False, errors=[{"id": COMPOSE_ERROR, "msg": str(e)}]), 400
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)
@v1_api.route("/compose/uploads/schedule", defaults={'compose_uuid': ""}, methods=["POST"]) @v1_api.route("/compose/uploads/schedule", defaults={'compose_uuid': ""}, methods=["POST"])
@v1_api.route("/compose/uploads/schedule/<compose_uuid>", methods=["POST"]) @v1_api.route("/compose/uploads/schedule/<compose_uuid>", methods=["POST"])

View File

@ -1252,6 +1252,10 @@ class ServerTestCase(unittest.TestCase):
ids = [e["id"] for e in data["new"] + data["run"]] ids = [e["id"] for e in data["new"] + data["run"]]
self.assertEqual(build_id in ids, True, "Failed to add build to the queue") self.assertEqual(build_id in ids, True, "Failed to add build to the queue")
# V0 API should *not* have the uploads details in the results
uploads = any("uploads" in e for e in data["new"] + data["run"])
self.assertFalse(uploads, "V0 API should not include 'uploads' field")
# Wait for it to start # Wait for it to start
self.assertEqual(_wait_for_status(self, build_id, ["RUNNING"]), True, "Failed to start test compose") self.assertEqual(_wait_for_status(self, build_id, ["RUNNING"]), True, "Failed to start test compose")
@ -1270,6 +1274,11 @@ class ServerTestCase(unittest.TestCase):
ids = [e["id"] for e in data["failed"]] ids = [e["id"] for e in data["failed"]]
self.assertEqual(build_id in ids, True, "Failed build not listed by /compose/failed") self.assertEqual(build_id in ids, True, "Failed build not listed by /compose/failed")
# V0 API should *not* have the uploads details in the results
print(data)
uploads = any("uploads" in e for e in data["failed"])
self.assertFalse(uploads, "V0 API should not include 'uploads' field")
# Test the /api/v0/compose/finished route # Test the /api/v0/compose/finished route
resp = self.server.get("/api/v0/compose/finished") resp = self.server.get("/api/v0/compose/finished")
data = json.loads(resp.data) data = json.loads(resp.data)
@ -1283,6 +1292,11 @@ class ServerTestCase(unittest.TestCase):
ids = [(e["id"], e["queue_status"]) for e in data["uuids"]] ids = [(e["id"], e["queue_status"]) for e in data["uuids"]]
self.assertEqual((build_id, "FAILED") in ids, True, "Failed build not listed by /compose/status") self.assertEqual((build_id, "FAILED") in ids, True, "Failed build not listed by /compose/status")
# V0 API should *not* have the uploads details in the results
print(data)
uploads = any("uploads" in e for e in data["uuids"])
self.assertFalse(uploads, "V0 API should not include 'uploads' field")
# Test the /api/v0/compose/cancel/<uuid> route # Test the /api/v0/compose/cancel/<uuid> route
resp = self.server.post("/api/v0/compose?test=1", resp = self.server.post("/api/v0/compose?test=1",
data=json.dumps(test_compose), data=json.dumps(test_compose),
@ -1338,6 +1352,10 @@ class ServerTestCase(unittest.TestCase):
ids = [e["id"] for e in data["new"] + data["run"]] ids = [e["id"] for e in data["new"] + data["run"]]
self.assertEqual(build_id in ids, True, "Failed to add build to the queue") self.assertEqual(build_id in ids, True, "Failed to add build to the queue")
# V0 API should *not* have the uploads details in the results
uploads = any("uploads" in e for e in data["new"] + data["run"])
self.assertFalse(uploads, "V0 API should not include 'uploads' field")
# Wait for it to start # Wait for it to start
self.assertEqual(_wait_for_status(self, build_id, ["RUNNING"]), True, "Failed to start test compose") self.assertEqual(_wait_for_status(self, build_id, ["RUNNING"]), True, "Failed to start test compose")
@ -1356,6 +1374,10 @@ class ServerTestCase(unittest.TestCase):
ids = [e["id"] for e in data["finished"]] ids = [e["id"] for e in data["finished"]]
self.assertEqual(build_id in ids, True, "Finished build not listed by /compose/finished") self.assertEqual(build_id in ids, True, "Finished build not listed by /compose/finished")
# V0 API should *not* have the uploads details in the results
uploads = any("uploads" in e for e in data["finished"])
self.assertFalse(uploads, "V0 API should not include 'uploads' field")
# Test the /api/v0/compose/failed route # Test the /api/v0/compose/failed route
resp = self.server.get("/api/v0/compose/failed") resp = self.server.get("/api/v0/compose/failed")
data = json.loads(resp.data) data = json.loads(resp.data)
@ -1369,6 +1391,10 @@ class ServerTestCase(unittest.TestCase):
ids = [(e["id"], e["queue_status"]) for e in data["uuids"]] ids = [(e["id"], e["queue_status"]) for e in data["uuids"]]
self.assertEqual((build_id, "FINISHED") in ids, True, "Finished build not listed by /compose/status") self.assertEqual((build_id, "FINISHED") in ids, True, "Finished build not listed by /compose/status")
# V0 API should *not* have the uploads details in the results
uploads = any("uploads" in e for e in data["uuids"])
self.assertFalse(uploads, "V0 API should not include 'uploads' field")
# Test the /api/v0/compose/metadata/<uuid> route # Test the /api/v0/compose/metadata/<uuid> route
resp = self.server.get("/api/v0/compose/metadata/%s" % build_id) resp = self.server.get("/api/v0/compose/metadata/%s" % build_id)
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
@ -1439,6 +1465,10 @@ class ServerTestCase(unittest.TestCase):
ids = [e["id"] for e in data["new"] + data["run"]] ids = [e["id"] for e in data["new"] + data["run"]]
self.assertEqual(build_id_fail in ids, True, "Failed to add build to the queue") self.assertEqual(build_id_fail in ids, True, "Failed to add build to the queue")
# V0 API should *not* have the uploads details in the results
uploads = any("uploads" in e for e in data["new"] + data["run"])
self.assertFalse(uploads, "V0 API should not include 'uploads' field")
# Wait for it to start # Wait for it to start
self.assertEqual(_wait_for_status(self, build_id_fail, ["RUNNING"]), True, "Failed to start test compose") self.assertEqual(_wait_for_status(self, build_id_fail, ["RUNNING"]), True, "Failed to start test compose")
@ -1461,6 +1491,10 @@ class ServerTestCase(unittest.TestCase):
ids = [e["id"] for e in data["new"] + data["run"]] ids = [e["id"] for e in data["new"] + data["run"]]
self.assertEqual(build_id_success in ids, True, "Failed to add build to the queue") self.assertEqual(build_id_success in ids, True, "Failed to add build to the queue")
# V0 API should *not* have the uploads details in the results
uploads = any("uploads" in e for e in data["new"] + data["run"])
self.assertFalse(uploads, "V0 API should not include 'uploads' field")
# Wait for it to start # Wait for it to start
self.assertEqual(_wait_for_status(self, build_id_success, ["RUNNING"]), True, "Failed to start test compose") self.assertEqual(_wait_for_status(self, build_id_success, ["RUNNING"]), True, "Failed to start test compose")
@ -1475,6 +1509,10 @@ class ServerTestCase(unittest.TestCase):
self.assertIn(build_id_success, ids, "Finished build not listed by /compose/status/*") self.assertIn(build_id_success, ids, "Finished build not listed by /compose/status/*")
self.assertIn(build_id_fail, ids, "Failed build not listed by /compose/status/*") self.assertIn(build_id_fail, ids, "Failed build not listed by /compose/status/*")
# V0 API should *not* have the uploads details in the results
uploads = any("uploads" in e for e in data["uuids"])
self.assertFalse(uploads, "V0 API should not include 'uploads' field")
# Filter by name # Filter by name
resp = self.server.get("/api/v0/compose/status/*?blueprint=%s" % test_compose_fail["blueprint_name"]) resp = self.server.get("/api/v0/compose/status/*?blueprint=%s" % test_compose_fail["blueprint_name"])
data = json.loads(resp.data) data = json.loads(resp.data)
@ -1483,6 +1521,10 @@ class ServerTestCase(unittest.TestCase):
self.assertIn(build_id_fail, ids, "Failed build not listed by /compose/status blueprint filter") self.assertIn(build_id_fail, ids, "Failed build not listed by /compose/status blueprint filter")
self.assertNotIn(build_id_success, ids, "Finished build listed by /compose/status blueprint filter") self.assertNotIn(build_id_success, ids, "Finished build listed by /compose/status blueprint filter")
# V0 API should *not* have the uploads details in the results
uploads = any("uploads" in e for e in data["uuids"])
self.assertFalse(uploads, "V0 API should not include 'uploads' field")
# Filter by type # Filter by type
resp = self.server.get("/api/v0/compose/status/*?type=tar") resp = self.server.get("/api/v0/compose/status/*?type=tar")
data = json.loads(resp.data) data = json.loads(resp.data)
@ -1491,6 +1533,10 @@ class ServerTestCase(unittest.TestCase):
self.assertIn(build_id_fail, ids, "Failed build not listed by /compose/status type filter") self.assertIn(build_id_fail, ids, "Failed build not listed by /compose/status type filter")
self.assertIn(build_id_success, ids, "Finished build not listed by /compose/status type filter") self.assertIn(build_id_success, ids, "Finished build not listed by /compose/status type filter")
# V0 API should *not* have the uploads details in the results
uploads = any("uploads" in e for e in data["uuids"])
self.assertFalse(uploads, "V0 API should not include 'uploads' field")
resp = self.server.get("/api/v0/compose/status/*?type=snakes") resp = self.server.get("/api/v0/compose/status/*?type=snakes")
data = json.loads(resp.data) data = json.loads(resp.data)
self.assertNotEqual(data, None) self.assertNotEqual(data, None)
@ -1505,6 +1551,10 @@ class ServerTestCase(unittest.TestCase):
self.assertIn(build_id_fail, ids, "Failed build not listed by /compose/status status filter") self.assertIn(build_id_fail, ids, "Failed build not listed by /compose/status status filter")
self.assertNotIn(build_id_success, "Finished build listed by /compose/status status filter") self.assertNotIn(build_id_success, "Finished build listed by /compose/status status filter")
# V0 API should *not* have the uploads details in the results
uploads = any("uploads" in e for e in data["uuids"])
self.assertFalse(uploads, "V0 API should not include 'uploads' field")
def test_compose_14_kernel_append(self): def test_compose_14_kernel_append(self):
"""Test the /api/v0/compose with kernel append customization""" """Test the /api/v0/compose with kernel append customization"""
test_compose = {"blueprint_name": "example-append", test_compose = {"blueprint_name": "example-append",
@ -1527,6 +1577,10 @@ class ServerTestCase(unittest.TestCase):
ids = [e["id"] for e in data["new"] + data["run"]] ids = [e["id"] for e in data["new"] + data["run"]]
self.assertEqual(build_id in ids, True, "Failed to add build to the queue") self.assertEqual(build_id in ids, True, "Failed to add build to the queue")
# V0 API should *not* have the uploads details in the results
uploads = any("uploads" in e for e in data["new"] + data["run"])
self.assertFalse(uploads, "V0 API should not include 'uploads' field")
# Wait for it to start # Wait for it to start
self.assertEqual(_wait_for_status(self, build_id, ["RUNNING"]), True, "Failed to start test compose") self.assertEqual(_wait_for_status(self, build_id, ["RUNNING"]), True, "Failed to start test compose")
@ -2177,6 +2231,10 @@ class GitRPMBlueprintTestCase(unittest.TestCase):
ids = [e["id"] for e in data["new"] + data["run"]] ids = [e["id"] for e in data["new"] + data["run"]]
self.assertEqual(build_id in ids, True, "Failed to add build to the queue") self.assertEqual(build_id in ids, True, "Failed to add build to the queue")
# V0 API should *not* have the uploads details in the results
uploads = any("uploads" in e for e in data["new"] + data["run"])
self.assertFalse(uploads, "V0 API should not include 'uploads' field")
# Wait for it to start # Wait for it to start
self.assertEqual(_wait_for_status(self, build_id, ["RUNNING"]), True, "Failed to start test compose") self.assertEqual(_wait_for_status(self, build_id, ["RUNNING"]), True, "Failed to start test compose")