# No shebang line, this module is meant to be imported
#
# Copyright 2013 Oliver Palmer
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Agents
------
Contained within this module are an API handling functions which can
manage or query agents using JSON.
"""
import re
import uuid
from datetime import datetime
import json
try:
from httplib import (
NOT_FOUND, NO_CONTENT, OK, CREATED, BAD_REQUEST, CONFLICT,
INTERNAL_SERVER_ERROR)
except ImportError: # pragma: no cover
from http.client import (
NOT_FOUND, NO_CONTENT, OK, CREATED, BAD_REQUEST, CONFLICT,
INTERNAL_SERVER_ERROR)
from flask import request, g
from flask.views import MethodView
from sqlalchemy import or_, not_
from pyfarm.core.logger import getLogger
from pyfarm.core.enums import WorkState, AgentState, _AgentState, STRING_TYPES
from pyfarm.scheduler.tasks import (
assign_tasks, update_agent, assign_tasks_to_agent, send_tasks_to_agent)
from pyfarm.models.agent import (
Agent, AgentMacAddress, AgentSoftwareVersionAssociation)
from pyfarm.models.gpu import GPU
from pyfarm.models.task import Task
from pyfarm.models.software import Software, SoftwareVersion
from pyfarm.master.config import config
from pyfarm.models.tag import Tag
from pyfarm.models.disk import AgentDisk
from pyfarm.master.application import db
from pyfarm.master.utility import (
jsonify, validate_with_model, get_ipaddr_argument, get_integer_argument,
get_hostname_argument, get_port_argument, isuuid)
logger = getLogger("api.agents")
MAC_RE = re.compile("^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$")
OUR_FARM_NAME = config.get("farm_name")
[docs]def fail_missing_assignments(agent, current_assignments):
known_task_ids = []
for assignment in current_assignments.values():
for task in assignment["tasks"]:
known_task_ids.append(task["id"])
tasks_query = Task.query.filter(Task.agent == agent,
or_(Task.state == None,
~Task.state.in_(
[WorkState.FAILED, WorkState.DONE])))
if known_task_ids:
tasks_query = tasks_query.filter(not_(Task.id.in_(known_task_ids)))
failed_tasks = []
unsent_tasks = False
for task in tasks_query:
if task.sent_to_agent:
task.state = WorkState.FAILED
db.session.add(task)
failed_tasks.append(task)
logger.warning("Task %s (frame %s from job %r (%s)) was not in the "
"current assignments of agent %r (id %s) when it "
"should be. Marking it as failed.",
task.id, task.frame, task.job.title,
task.job_id, agent.hostname, agent.id)
else:
unsent_tasks = True
if unsent_tasks:
send_tasks_to_agent.delay(agent.id)
return failed_tasks
[docs]def schema():
"""
Returns the basic schema of :class:`.Agent`
.. http:get:: /api/v1/agents/schema HTTP/1.1
**Request**
.. sourcecode:: http
GET /api/v1/agents/schema HTTP/1.1
Accept: application/json
**Response**
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json
{
"ram": "INTEGER",
"free_ram": "INTEGER",
"time_offset": "INTEGER",
"use_address": "INTEGER",
"hostname": "VARCHAR(255)",
"cpus": "INTEGER",
"port": "INTEGER",
"state": "INTEGER",
"ram_allocation": "FLOAT",
"cpu_allocation": "FLOAT",
"id": "UUIDType",
"remote_ip": "IPv4Address"
}
:statuscode 200: no error
"""
return jsonify(Agent.to_schema())
[docs]class AgentIndexAPI(MethodView):
@validate_with_model(Agent, ignore=("current_assignments", "id",
"farm_name"))
[docs] def post(self):
"""
A ``POST`` to this endpoint will either create or update an existing
agent. The ``port`` and ``id`` columns will determine if an
agent already exists.
* If an agent is found matching the ``port`` and ``id``
columns from the request the existing model will be updated and
the resulting data and the ``OK`` code will be returned.
* If we don't find an agent matching the ``port`` and ``id``
however a new agent will be created and the resulting data and the
``CREATED`` code will be returned.
.. note::
The ``remote_ip`` field is not required and should typically
not be included in a request. When not provided ``remote_ip``
is be populated by the server based off of the ip of the
incoming request. Providing ``remote_ip`` in your request
however will override this behavior.
.. http:post:: /api/v1/agents/ HTTP/1.1
**Request**
.. sourcecode:: http
POST /api/v1/agents/ HTTP/1.1
Accept: application/json
{
"cpu_allocation": 1.0,
"cpus": 14,
"free_ram": 133,
"hostname": "agent1",
"id": "6a0c11df-660f-4c1e-9fb4-5fe2b8cd2437",
"remote_ip": "10.196.200.115",
"port": 64994,
"ram": 2157,
"ram_allocation": 0.8,
"state": 8
}
**Response (agent created)**
.. sourcecode:: http
HTTP/1.1 201 CREATED
Content-Type: application/json
{
"cpu_allocation": 1.0,
"cpus": 14,
"use_address": "remote",
"free_ram": 133,
"time_offset": 0,
"hostname": "agent1",
"id": "6a0c11df-660f-4c1e-9fb4-5fe2b8cd2437",
"port": 64994,
"ram": 2157,
"ram_allocation": 0.8,
"state": "online",
"remote_ip": "10.196.200.115"
}
**Response (existing agent updated)**
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json
{
"cpu_allocation": 1.0,
"cpus": 14,
"use_address": "remote",
"free_ram": 133,
"time_offset": 0,
"hostname": "agent1",
"id": "6a0c11df-660f-4c1e-9fb4-5fe2b8cd2437",
"port": 64994,
"ram": 2157,
"ram_allocation": 0.8,
"state": "online",
"remote_ip": "10.196.200.115"
}
:statuscode 201:
a new agent was created
:statuscode 200:
an existing agent is updated with data from the request
:statuscode 400:
there was something wrong with the request (such as
invalid columns being included)
"""
# Read in and convert the id field
try:
g.json["id"] = uuid.UUID(g.json["id"])
except KeyError:
return jsonify(error="`id` not provided"), BAD_REQUEST
# If the remote user agent is not "PyFarm/1.0 (agent)", this is not an
# announce by the agent itself. It could be some other client editing
# the agent.
if request.headers.get("User-Agent", "") == "PyFarm/1.0 (agent)":
# Set remote_ip if it did not come in with the request
g.json.setdefault("remote_ip", request.remote_addr)
farm_name = g.json.pop("farm_name", None)
if farm_name and farm_name != OUR_FARM_NAME:
return jsonify(error="Wrong farm name"), BAD_REQUEST
current_assignments = g.json.pop("current_assignments", None)
mac_addresses = g.json.pop("mac_addresses", None)
# TODO return BAD_REQUEST on bad mac addresses
if mac_addresses is not None:
mac_addresses = [x.lower() for x in mac_addresses if MAC_RE.match(x)]
gpus = g.json.pop("gpus", None)
disks = g.json.pop("disks", None)
state = g.json.pop("state", None)
agent = Agent.query.filter_by(
port=g.json["port"], id=g.json["id"]).first()
if agent is None:
try:
agent = Agent(**g.json)
# There may be something wrong with one of the fields
# that's causing our sqlalchemy model raise a ValueError.
except ValueError as e:
return jsonify(error=str(e)), BAD_REQUEST
default_versions = SoftwareVersion.query.filter_by(default=True)
for version in default_versions:
agent.software_versions.append(version)
if mac_addresses is not None:
for address in mac_addresses:
mac_address = AgentMacAddress(agent=agent,
mac_address=address)
db.session.add(mac_address)
if gpus is not None:
for gpu_name in gpus:
gpu = GPU.query.filter_by(
fullname=gpu_name).first()
if not gpu:
gpu = GPU(fullname=gpu_name)
db.session.add(gpu)
agent.gpus.append(gpu)
if disks is not None:
for disk_dict in disks:
disk = AgentDisk(agent=agent,
mountpoint=disk_dict["mountpoint"],
size=disk_dict["size"],
free=disk_dict["free"])
db.session.add(disk)
if state is not None:
agent.state = state
db.session.add(agent)
try:
db.session.commit()
except Exception as e:
e = e.args[0].lower()
error = "Unhandled error: %s. This is often an issue " \
"with the agent's data for `ip`, `hostname` and/or " \
"`port` not being unique enough. In other cases " \
"this can sometimes happen if the underlying " \
"database driver is either non-compliant with " \
"expectations or we've encountered a database error " \
"that we don't know how to handle yet. If the " \
"latter is the case, please report this as a bug." % e
return jsonify(error=error), INTERNAL_SERVER_ERROR
else:
agent_data = agent.to_dict(unpack_relationships=["tags"])
logger.info("Created agent %r: %r", agent.id, agent_data)
assign_tasks.delay()
return jsonify(agent_data), CREATED
else:
updated = False
for key in g.json.copy():
value = g.json.pop(key)
if not hasattr(agent, key):
return jsonify(
error="Agent has no such column `%s`" % key), \
BAD_REQUEST
if getattr(agent, key) != value:
try:
setattr(agent, key, value)
except Exception as e:
return jsonify(
error="Error while setting `%s`: %s" % (key, e)), \
BAD_REQUEST
else:
updated = True
if mac_addresses is not None:
updated = True
for existing_address in agent.mac_addresses:
if existing_address.mac_address.lower() not in mac_addresses:
logger.debug("Existing address %s is not in supplied "
"mac addresses, for agent %s, removing it.",
existing_address.mac_address,
agent.hostname)
agent.mac_addresses.remove(existing_address)
else:
mac_addresses.remove(
existing_address.mac_address.lower())
for new_address in mac_addresses:
mac_address = AgentMacAddress(
agent=agent, mac_address=new_address)
db.session.add(mac_address)
if gpus is not None:
updated = True
for existing_gpu in agent.gpus:
if existing_gpu.fullname not in gpus:
logger.debug("Existing gpu %s is not in supplied "
"gpus, for agent %s, removing it.",
existing_address.mac_address,
agent.hostname)
agent.gpus.remove(existing_gpu)
else:
gpus.remove(existing_gpu.fullname)
for gpu_name in gpus:
gpu = GPU.query.filter_by(fullname=gpu_name).first()
if not gpu:
gpu = GPU(fullname=gpu_name)
db.session.add(gpu)
agent.gpus.append(gpu)
if disks is not None:
for old_disk in agent.disks:
db.session.delete(old_disk)
for disk_dict in disks:
disk = AgentDisk(agent=agent,
mountpoint=disk_dict["mountpoint"],
size=disk_dict["size"],
free=disk_dict["free"])
db.session.add(disk)
if state is not None and agent.state != _AgentState.DISABLED:
agent.state = state
# TODO Only do that if this is really the agent speaking to us.
failed_tasks = []
if (current_assignments is not None and
agent.state != AgentState.OFFLINE):
fail_missing_assignments(agent, current_assignments)
if updated or failed_tasks:
agent.last_heard_from = datetime.utcnow()
db.session.add(agent)
try:
db.session.commit()
except Exception as e:
return jsonify(error="Unhandled error: %s" % e), \
INTERNAL_SERVER_ERROR
else:
agent_data = agent.to_dict(unpack_relationships=["tags"])
logger.info("Updated agent %r: %r", agent.id, agent_data)
for task in failed_tasks:
task.job.update_state()
db.session.commit()
assign_tasks.delay()
return jsonify(agent_data), OK
[docs] def get(self):
"""
A ``GET`` to this endpoint will return a list of known agents, with id
and name.
.. http:get:: /api/v1/agents/ HTTP/1.1
**Request**
.. sourcecode:: http
GET /api/v1/agents/ HTTP/1.1
Accept: application/json
**Response**
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"hostname": "agent1",
"id": "dd0c6da2-0c91-42cf-a82f-6d503aae43d3"
},
{
"hostname": "agent2",
"id": "8326779e-90b5-447c-8da8-1eaa154771d9"
},
{
"hostname": "agent3.local",
"id": "14b28230-64a1-4b62-803e-5fd1baa209e4"
}
]
**Request (with filters)**
.. sourcecode:: http
GET /api/v1/agents/?min_ram=4096&min_cpus=4 HTTP/1.1
Accept: application/json
**Response**
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"hostname": "foobar",
"port": 50000,
"remote_ip": "127.0.0.1",
"id": "e20bae92-6472-442e-98a8-0ea4c9ee41cd"
}
]
:qparam min_ram:
If set, list only agents with ``min_ram`` ram or more
:qparam max_ram:
If set, list only agents with ``max_ram`` ram or less
:qparam min_cpus:
If set, list only agents with ``min_cpus`` cpus or more
:qparam max_cpus:
If set, list only agents with ``max_cpus`` cpus or less
:qparam hostname:
If set, list only agents matching ``hostname``
:qparam remote_ip:
If set, list only agents matching ``remote_ip``
:qparam port:
If set, list only agents matching ``port``.
:statuscode 200:
no error, host may or may not have been found
"""
query = db.session.query(
Agent.id, Agent.hostname, Agent.port, Agent.remote_ip)
# parse url arguments
min_ram = get_integer_argument("min_ram")
max_ram = get_integer_argument("max_ram")
min_cpus = get_integer_argument("min_cpus")
max_cpus = get_integer_argument("max_cpus")
hostname = get_hostname_argument("hostname")
remote_ip = get_ipaddr_argument("remote_ip")
port = get_port_argument("port")
# construct query
if min_ram is not None:
query = query.filter(Agent.ram >= min_ram)
if max_ram is not None:
query = query.filter(Agent.ram <= max_ram)
if min_cpus is not None:
query = query.filter(Agent.cpus >= min_cpus)
if max_cpus is not None:
query = query.filter(Agent.cpus <= max_cpus)
if hostname is not None:
query = query.filter(Agent.hostname == hostname)
if remote_ip is not None:
query = query.filter(Agent.remote_ip == remote_ip)
if port is not None:
query = query.filter(Agent.port == port)
# run query and convert the results
output = []
for host in query:
host = dict(zip(host.keys(), host))
# convert the IPAddress object, if set
if host["remote_ip"] is not None:
host["remote_ip"] = str(host["remote_ip"])
output.append(host)
return jsonify(output), OK
[docs]class SingleAgentAPI(MethodView):
"""
API view which is used for retrieving information about and updating
single agents.
"""
[docs] def get(self, agent_id):
"""
Return basic information about a single agent
.. http:get:: /api/v1/agents/(str:agent_id) HTTP/1.1
**Request (agent exists)**
.. sourcecode:: http
GET /api/v1/agents/4eefca76-1127-4c17-a3df-c1a7de685541 HTTP/1.1
Accept: application/json
**Response**
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json
{
"cpu_allocation": 1.0,
"cpus": 14,
"use_address": 311,
"free_ram": 133,
"time_offset": 0,
"hostname": "agent1",
"id": "322360ad-976f-4103-9acc-a811d43fd24d",
"ip": "10.196.200.115",
"port": 64994,
"ram": 2157,
"ram_allocation": 0.8,
"state": 202,
"remote_ip": "10.196.200.115"
}
**Request (no such agent)**
.. sourcecode:: http
GET /api/v1/agents/4eefca76-1127-4c17-a3df-c1a7de685541 HTTP/1.1
Accept: application/json
**Response**
.. sourcecode:: http
HTTP/1.1 404 NOT FOUND
Content-Type: application/json
{"error": "Agent `4eefca76-1127-4c17-a3df-c1a7de685541` not "
"found"}
:statuscode 200: no error
:statuscode 400: something within the request is invalid
:statuscode 404: no agent could be found using the given id
"""
agent = Agent.query.filter_by(id=agent_id).first()
if agent is not None:
return jsonify(agent.to_dict(
unpack_relationships=["tags", "gpus", "disks"]))
else:
return jsonify(error="Agent %s not found" % agent_id), NOT_FOUND
@validate_with_model(
Agent,
type_checks={"id": isuuid},
ignore=("current_assignments", "farm_name"),
ignore_missing=(
"ram", "cpus", "port", "free_ram", "hostname"))
[docs] def post(self, agent_id):
"""
Update an agent's columns with new information by merging the provided
data with the agent's current definition in the database.
.. http:post:: /api/v1/agents/(str:agent_id) HTTP/1.1
**Request**
.. sourcecode:: http
POST /api/v1/agents/29d466a5-34f8-408a-b613-e6c2715077a0 HTTP/1.1
Accept: application/json
{"ram": 1234}
**Response**
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json
{
"cpu_allocation": 1.0,
"cpus": 14,
"use_address": 311,
"free_ram": 133,
"time_offset": 0,
"hostname": "agent1",
"id": "29d466a5-34f8-408a-b613-e6c2715077a0",
"ip": "10.196.200.115",
"port": 64994,
"ram": 1234,
"ram_allocation": 0.8,
"state": "running",
"remote_ip": "10.196.200.115"
}
:statuscode 200:
no error
:statuscode 400:
something within the request is invalid
:statuscode 404:
no agent could be found using the given id
"""
agent = Agent.query.filter_by(id=agent_id).first()
if agent is None:
return jsonify(error="Agent %s not found" % agent_id), NOT_FOUND
if ("remote_ip" not in g.json and
request.headers.get("User-Agent", "") == "PyFarm/1.0 (agent)"):
g.json["remote_ip"] = request.remote_addr
farm_name = g.json.pop("farm_name", None)
if farm_name and farm_name != OUR_FARM_NAME:
return jsonify(error="Wrong farm name"), BAD_REQUEST
current_assignments = g.json.pop("current_assignments", None)
mac_addresses = g.json.pop("mac_addresses", None)
# TODO return BAD_REQUEST on bad mac addresses
if mac_addresses is not None:
mac_addresses = [x.lower() for x in mac_addresses if MAC_RE.match(x)]
gpus = g.json.pop("gpus", None)
disks = g.json.pop("disks", None)
tags = g.json.pop("tags", None)
modified = {}
try:
items = g.json.iteritems
except AttributeError:
items = g.json.items
state = g.json.pop("state", None)
if state and agent.state != _AgentState.DISABLED:
agent.state = state
modified["state"] = state
for key, value in items():
if value != getattr(agent, key):
try:
setattr(agent, key, value)
# There may be something wrong with one of the fields
# that's causing our sqlalchemy model to raise a ValueError.
except ValueError as e:
return jsonify(error=str(e)), BAD_REQUEST
modified[key] = value
agent.last_heard_from = datetime.utcnow()
if "upgrade_to" in modified:
update_agent.delay(agent.id)
if mac_addresses is not None:
modified["mac_addresses"] = mac_addresses
for existing_address in agent.mac_addresses:
if existing_address.mac_address.lower() not in mac_addresses:
logger.debug("Existing address %s is not in supplied "
"mac addresses, for agent %s, removing it.",
existing_address.mac_address,
agent.hostname)
agent.mac_addresses.remove(existing_address)
else:
mac_addresses.remove(
existing_address.mac_address.lower())
for new_address in mac_addresses:
mac_address = AgentMacAddress(
agent=agent, mac_address=new_address)
db.session.add(mac_address)
if gpus is not None:
modified["gpus"] = gpus
for existing_gpu in agent.gpus:
if existing_gpu.fullname not in gpus:
logger.debug("Existing gpu %s is not in supplied "
"gpus, for agent %s, removing it.",
existing_gpu.fullname,
agent.hostname)
agent.gpus.remove(existing_gpu)
else:
gpus.remove(existing_gpu.fullname)
for gpu_name in gpus:
gpu = GPU.query.filter_by(fullname=gpu_name).first()
if not gpu:
gpu = GPU(fullname=gpu_name)
db.session.add(gpu)
agent.gpus.append(gpu)
if disks is not None:
for old_disk in agent.disks:
db.session.delete(old_disk)
for disk_dict in disks:
disk = AgentDisk(agent=agent,
mountpoint=disk_dict["mountpoint"],
size=disk_dict["size"],
free=disk_dict["free"])
db.session.add(disk)
if tags is not None:
modified["tags"] = tags
for existing_tag in agent.tags:
if existing_tag.tag not in tags:
logger.debug("Existing tag %s is not in supplied "
"tags, for agent %s, removing it.",
existing_tag.tag, agent.hostname)
agent.tags.remove(existing_tag)
else:
tags.remove(existing_tag.tag)
for tag_name in tags:
tag = Tag.query.filter_by(tag=tag_name).first()
if not tag:
tag = Tag(tag=tag_name)
db.session.add(tag)
agent.tags.append(tag)
# TODO Only do that if this is really the agent speaking to us.
failed_tasks = []
if (current_assignments is not None and
agent.state != AgentState.OFFLINE):
failed_tasks = fail_missing_assignments(agent, current_assignments)
logger.debug(
"Updated agent %r: %r", agent.id, modified)
db.session.add(agent)
db.session.commit()
for task in failed_tasks:
task.job.update_state()
if agent.state == _AgentState.OFFLINE:
for task in agent.tasks.filter(Task.state != WorkState.DONE,
Task.state != WorkState.FAILED):
task.agent = None
task.state = None
task.job.update_state()
db.session.add(task)
db.session.commit()
assign_tasks_to_agent.delay(agent_id)
return jsonify(agent.to_dict(unpack_relationships=["tags"])), OK
[docs] def delete(self, agent_id):
"""
Delete a single agent
.. http:delete:: /api/v1/agents/(uuid:agent_id) HTTP/1.1
**Request (agent exists)**
.. sourcecode:: http
DELETE /api/v1/agents/b25ee7eb-9586-439a-b131-f5d022e0d403 HTTP/1.1
Accept: application/json
**Response**
.. sourcecode:: http
HTTP/1.1 204 NO CONTENT
Content-Type: application/json
:statuscode 204: the agent was deleted or did not exist
"""
agent = Agent.query.filter_by(id=agent_id).first()
if agent is None:
return jsonify(None), NO_CONTENT
else:
db.session.delete(agent)
db.session.commit()
assign_tasks.delay()
return jsonify(None), NO_CONTENT
[docs]class TasksInAgentAPI(MethodView):
[docs] def get(self, agent_id):
"""
A ``GET`` to this endpoint will return a list of all tasks assigned to
this agent.
.. http:get:: /api/v1/agents/<str:agent_id>/tasks/ HTTP/1.1
**Request**
.. sourcecode:: http
GET /api/v1/agents/bbf55143-f2b1-4c15-9d41-139bd8057931/tasks/ HTTP/1.1
Accept: application/json
**Response**
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"state": "assign",
"priority": 0,
"job": {
"jobtype": "TestJobType",
"id": 1,
"title": "Test Job",
"jobtype_version": 1,
"jobtype_id": 1
},
"hidden": false,
"time_started": null,
"project_id": null,
"frame": 2.0
"agent_id": "bbf55143-f2b1-4c15-9d41-139bd8057931",
"id": 2,
"attempts": 2,
"project": null,
"time_finished": null,
"time_submitted": "2014-03-06T15:40:58.338904",
"job_id": 1
}
]
:statuscode 200: no error
:statuscode 404: agent not found
"""
agent = Agent.query.filter_by(id=agent_id).first()
if agent is None:
return jsonify(error="Agent %r not found" % agent_id), NOT_FOUND
out = []
for task in agent.tasks:
task_dict = task.to_dict(unpack_relationships=False)
task_dict["job"] = {
"id": task.job.id,
"title": task.job.title,
"jobtype": task.job.jobtype_version.jobtype.name,
"jobtype_id": task.job.jobtype_version.jobtype_id,
"jobtype_version": task.job.jobtype_version.version
}
out.append(task_dict)
return jsonify(out), OK
[docs] def post(self, agent_id):
"""
A ``POST`` to this endpoint will assign am existing task to the agent.
.. http:post:: /api/v1/agents/<str:agent_id>/tasks/ HTTP/1.1
**Request**
.. sourcecode:: http
POST /api/v1/agents/238d7334-8ca5-4469-9f54-e76c66614a43/tasks/ HTTP/1.1
Accept: application/json
{
"id": 2
}
**Response**
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json
{
"agent_id": 1,
"parents": [],
"attempts": 2,
"children": [],
"job": {
"title": "Test Job",
"id": 1
},
"project_id": null,
"agent": {
"ip": null,
"hostname": "agent1",
"port": 50000,
"id": "238d7334-8ca5-4469-9f54-e76c66614a43"
},
"hidden": false,
"job_id": 1,
"time_submitted": "2014-03-06T15:40:58.338904",
"frame": 2.0,
"priority": 0,
"state": "assign",
"time_finished": null,
"id": 2,
"project": null,
"time_started": null
}
:statuscode 200:
no error
:statuscode 404:
agent not found
"""
if "id" not in g.json:
return jsonify(error="No id given for task"), BAD_REQUEST
if len(g.json) > 1:
return jsonify(error="Unknown keys in request"), BAD_REQUEST
agent = Agent.query.filter_by(id=agent_id).first()
if agent is None:
return jsonify(error="Agent %r not found" % agent_id), NOT_FOUND
task = Task.query.filter_by(id=g.json["id"]).first()
if not task:
return jsonify(error="Task not found"), NOT_FOUND
task.agent = agent
db.session.add(task)
db.session.commit()
logger.info("Assigned task %s (frame %s, job %s) to agent %s (%s)",
task.id, task.frame, task.job.title,
agent.id, agent.hostname)
assign_tasks.delay()
return jsonify(task.to_dict()), OK
[docs]class SoftwareInAgentIndexAPI(MethodView):
[docs] def get(self, agent_id):
"""
A ``GET`` to this endpoint will return a list of all software versions
available on this agent.
.. http:get:: /api/v1/agents/<str:agent_id>/software/ HTTP/1.1
**Request**
.. sourcecode:: http
GET /api/v1/agents/bbf55143-f2b1-4c15-9d41-139bd8057931/software/ HTTP/1.1
Accept: application/json
**Response**
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"software": "Blender",
"version": "2.72"
}
]
:statuscode 200: no error
:statuscode 404: agent not found
"""
agent = Agent.query.filter_by(id=agent_id).first()
if agent is None:
return jsonify(error="Agent %r not found" % agent_id), NOT_FOUND
out = []
for version in agent.software_versions:
software_dict = {"software": version.software.software,
"version": version.version}
out.append(software_dict)
return jsonify(out), OK
[docs] def post(self, agent_id):
"""
A ``POST`` to this endpoint will mark the given version of the given
software as available on this agent.
.. http:post:: /api/v1/agents/<str:agent_id>/software/ HTTP/1.1
**Request**
.. sourcecode:: http
POST /api/v1/agents/bbf55143-f2b1-4c15-9d41-139bd8057931/software/ HTTP/1.1
Accept: application/json
{
"software": "Blender",
"version": "2.72"
}
**Response**
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json
{
"software": "Blender",
"version": "2.72"
}
:statuscode 200: no error
:statuscode 400: the request contained unknown keys or required keys
were missing.
:statuscode 404: agent not found
"""
agent = Agent.query.filter_by(id=agent_id).first()
if agent is None:
return jsonify(error="Agent %r not found" % agent_id), NOT_FOUND
software_name = g.json.pop("software", None)
if not software_name:
return jsonify(error="`Software` not specified"), BAD_REQUEST
if not isinstance(software_name, STRING_TYPES):
return jsonify(error="`Software` is not a string"), BAD_REQUEST
version_name = g.json.pop("version", None)
if not version_name:
return jsonify(error="`Version` not specified"), BAD_REQUEST
if not isinstance(version_name, STRING_TYPES):
return jsonify(error="`Version` is not a string"), BAD_REQUEST
software = Software.query.filter_by(software=software_name).first()
if not software:
return (jsonify(error="Software %s not found" % software_name),
NOT_FOUND)
version = SoftwareVersion.query.filter_by(
software=software, version=version_name).first()
if not version:
return (jsonify(error="Version %s not found" % version_name),
NOT_FOUND)
agent.software_versions.append(version)
db.session.commit()
out = {"software": software.software,
"version": version.version}
return jsonify(out), OK
[docs]class SingleSoftwareInAgentAPI(MethodView):
[docs] def delete(self, agent_id, software_name, version_name):
"""
A ``DELETE`` to this endpoint will remove the specified software version
from the list of supported software in this agent
.. http:delete:: /api/v1/agents/<str:agent_id>/software/<str:software>/versions/<str:version> HTTP/1.1
**Request**
.. sourcecode:: http
DELETE /api/v1/agents/bbf55143-f2b1-4c15-9d41-139bd8057931/software/Blender/versions/2.72 HTTP/1.1
Accept: application/json
**Response**
.. sourcecode:: http
HTTP/1.1 200 NO CONTENT
:statuscode 204: the software version has been removed from the
supported versions on this agent or has not been on the
list in the first place
:statuscode 404: agent not found
"""
agent = Agent.query.filter_by(id=agent_id).first()
if agent is None:
return jsonify(error="Agent %r not found" % agent_id), NOT_FOUND
software = Software.query.filter_by(software=software_name).first()
if not software:
return (jsonify(error="Software %s not found" % software_name),
NOT_FOUND)
version = SoftwareVersion.query.filter_by(
software=software, version=version_name).first()
if not version:
return (jsonify(error="Version %s not found" % version_name),
NOT_FOUND)
if version not in agent.software_versions:
return jsonify(), NO_CONTENT
agent.software_versions.remove(version)
db.session.add(agent)
db.session.commit()
return jsonify(), NO_CONTENT