Source code for pyfarm.master.api.tags

# No shebang line, this module is meant to be imported
#
# Copyright 2013 Oliver Palmer
# Copyright 2014 Ambient Entertainment GmbH & Co. KG
#
# 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.

"""
Tag
===

Contained within this module are an API handling functions which can
manage or query tags using JSON.
"""

from uuid import UUID

try:
    from httplib import NOT_FOUND, NO_CONTENT, OK, CREATED, BAD_REQUEST
except ImportError:  # pragma: no cover
    from http.client import NOT_FOUND, NO_CONTENT, OK, CREATED, BAD_REQUEST

from flask import url_for, g
from flask.views import MethodView

from pyfarm.core.logger import getLogger
from pyfarm.core.enums import STRING_TYPES
from pyfarm.models.agent import Agent
from pyfarm.models.job import Job
from pyfarm.models.tag import Tag
from pyfarm.master.application import db
from pyfarm.master.utility import jsonify, validate_with_model

logger = getLogger("api.tags")


[docs]def schema(): """ Returns the basic schema of :class:`.Tag` .. http:get:: /api/v1/tags/schema/ HTTP/1.1 **Request** .. sourcecode:: http GET /api/v1/tags/schema/ HTTP/1.1 Accept: application/json **Response** .. sourcecode:: http HTTP/1.1 200 OK Content-Type: application/json { "id": "INTEGER", "tag": "VARCHAR(64)" } :statuscode 200: no error """ return jsonify(Tag.to_schema())
[docs]class TagIndexAPI(MethodView): @validate_with_model(Tag)
[docs] def post(self): """ A ``POST`` to this endpoint will do one of two things: * create a new tag and return the row * return the row for an existing tag Tags only have one column, the tag name. Two tags are automatically considered equal if the tag names are equal. .. http:post:: /api/v1/tags/ HTTP/1.1 **Request** .. sourcecode:: http POST /api/v1/tags/ HTTP/1.1 Accept: application/json { "tag": "interesting" } **Response (new tag create)** .. sourcecode:: http HTTP/1.1 201 CREATED Content-Type: application/json { "id": 1, "tag": "interesting" } **Request** .. sourcecode:: http POST /api/v1/tags/ HTTP/1.1 Accept: application/json { "tag": "interesting" } **Response (existing tag returned)** .. sourcecode:: http HTTP/1.1 200 OK Content-Type: application/json { "id": 1, "tag": "interesting" } :statuscode 200: an existing tag was found and returned :statuscode 201: a new tag was created :statuscode 400: there was something wrong with the request (such as invalid columns being included) """ existing_tag = Tag.query.filter_by(tag=g.json["tag"]).first() if existing_tag: # No update needed, because Tag only has that one column return jsonify(existing_tag.to_dict()), OK else: new_tag = Tag(**g.json) db.session.add(new_tag) db.session.commit() tag_data = new_tag.to_dict(unpack_relationships=("agents", "jobs")) logger.info("created tag %s: %r", new_tag.id, tag_data) return jsonify(tag_data), CREATED
[docs] def get(self): """ A ``GET`` to this endpoint will return a list of known tags, with id. Associated agents and jobs are included for every tag :rtype : object .. http:get:: /api/v1/tags/ HTTP/1.1 **Request** .. sourcecode:: http GET /api/v1/tags/ HTTP/1.1 Accept: application/json **Response** .. sourcecode:: http HTTP/1.1 200 OK Content-Type: application/json [ { "agents": [ 1 ], "jobs": [], "id": 1, "tag": "interesting" }, { "agents": [], "jobs": [], "id": 2, "tag": "boring" } ] :statuscode 200: no error """ out = [] for tag in Tag.query.all(): out.append(tag.to_dict(unpack_relationships=("agents", "jobs"))) return jsonify(out), OK
[docs]class SingleTagAPI(MethodView):
[docs] def get(self, tagname=None): """ A ``GET`` to this endpoint will return the referenced tag, either by name or id, including a list of agents and jobs associated with it. .. http:get:: /api/v1/tags/<str:tagname> HTTP/1.1 **Request** .. sourcecode:: http GET /api/v1/tags/interesting HTTP/1.1 Accept: application/json **Response** .. sourcecode:: http HTTP/1.1 200 OK Content-Type: application/json { "agents": [{ "hostname": "agent3", "href": "/api/v1/agents/94522b7e-817b-4358-95da-670b31aad624", "id": 1 }], "id": 1, "jobs": [], "tag": "interesting" } :statuscode 200: no error :statuscode 404: tag not found """ if isinstance(tagname, STRING_TYPES): tag = Tag.query.filter_by(tag=tagname).first() else: tag = Tag.query.filter_by(id=tagname).first() if tag is None: return jsonify(error="tag `%s` not found" % tagname), NOT_FOUND tag_dict = tag.to_dict(unpack_relationships=("agents", "jobs")) return jsonify(tag_dict), OK
@validate_with_model(Tag, ignore=("tag", ), disallow=("id", ))
[docs] def put(self, tagname=None): """ A ``PUT`` to this endpoint will create a new tag under the given URI. If a tag already exists under that URI, it will be deleted, then recreated. Note that when overwriting a tag like that, all relations that are not explicitly specified here will be deleted You can optionally specify a list of agents or jobs relations as integers in the request data. You should only call this by id for overwriting an existing tag or if you have a reserved tag id. There is currently no way to reserve a tag id. .. http:put:: /api/v1/tags/<str:tagname> HTTP/1.1 **Request** .. sourcecode:: http PUT /api/v1/tags/interesting HTTP/1.1 Accept: application/json { "tag": "interesting" } **Response** .. sourcecode:: http HTTP/1.1 201 CREATED Content-Type: application/json { "id": 1, "tag": "interesting" } **Request** .. sourcecode:: http PUT /api/v1/tags/interesting HTTP/1.1 Accept: application/json { "tag": "interesting", "agents": [1] "jobs": [] } **Response** .. sourcecode:: http HTTP/1.1 201 CREATED Content-Type: application/json { "id": 1, "tag": "interesting" } :statuscode 201: a new tag was created :statuscode 400: there was something wrong with the request (such as invalid columns being included) :statuscode 404: a referenced agent or job does not exist """ if isinstance(tagname, int): tag = Tag.query.filter_by(id=tagname).first() if "tag" in g.json and g.json["tag"] != tag.tag: error = "tag name retrieved for %s does not match tag " \ "name in request" % tagname return jsonify(error=error), BAD_REQUEST g.json.setdefault("tag", tag.tag) elif isinstance(tagname, STRING_TYPES): g.json.setdefault("tag", tagname) if g.json["tag"] != tagname: return jsonify(error="`tag` in data must be equal to the " "tag in the requested url"), BAD_REQUEST tag = Tag.query.filter_by(tag=g.json["tag"]).first() # If tag exists, delete it before recreating it if tag: logger.debug( "tag %s will be replaced with %r on commit", tag.tag, g.json) db.session.delete(tag) db.session.flush() agents = [] if "agents" in g.json: agent_ids = g.json.pop("agents", []) if not isinstance(agent_ids, list): return jsonify(error="agents must be a list"), BAD_REQUEST try: agent_ids = list(map(UUID, agent_ids)) except (ValueError, AttributeError): return jsonify(error="All agent ids must be UUIDs"), BAD_REQUEST # find all models matching the request id(s) agents = Agent.query.filter(Agent.id.in_(agent_ids)).all() # make sure all those ids were actually found missing_agents = set(agent_ids) - set(agent.id for agent in agents) if missing_agents: return jsonify( error="agent(s) not found: %s" % missing_agents), NOT_FOUND jobs = [] if "jobs" in g.json: job_ids = g.json.pop("jobs", []) if not isinstance(job_ids, list): return jsonify(error="jobs must be a list"), BAD_REQUEST # make sure all ids provided are ints if not all(isinstance(job_id, int) for job_id in job_ids): return jsonify( error="all job ids must be integers"), BAD_REQUEST # find all models matching the request id(s) jobs = Job.query.filter(Agent.id.in_(job_ids)).all() # make sure all those ids were actually found missing_jobs = set(job_ids) - set(job.id for job in jobs) if missing_jobs: return jsonify( error="job(s) not found: %s" % missing_jobs), NOT_FOUND new_tag = Tag(**g.json) if isinstance(tagname, int): new_tag.id = tagname new_tag.agents = agents new_tag.jobs = jobs logger.info("creating tag %s: %r", new_tag.tag, new_tag.to_dict()) db.session.add(new_tag) db.session.commit() return (jsonify(new_tag.to_dict(unpack_relationships=("agents", "jobs"))), CREATED)
[docs] def delete(self, tagname=None): """ A ``DELETE`` to this endpoint will delete the tag under this URI, including all relations to tags or jobs. .. http:delete:: /api/v1/tags/<str:tagname> HTTP/1.1 **Request** .. sourcecode:: http DELETE /api/v1/tags/interesting HTTP/1.1 Accept: application/json **Response** .. sourcecode:: http HTTP/1.1 201 CREATED Content-Type: application/json { "id": 1, "tag": "interesting" } :statuscode 204: the tag was deleted or did not exist in the first place """ if isinstance(tagname, STRING_TYPES): tag = Tag.query.filter_by(tag=tagname).first() else: tag = Tag.query.filter_by(id=tagname).first() if tag is None: return jsonify(None), NO_CONTENT db.session.delete(tag) db.session.commit() logger.info("deleted tag %s", tag.tag) return jsonify(None), NO_CONTENT
[docs]class AgentsInTagIndexAPI(MethodView):
[docs] def post(self, tagname=None): """ A ``POST`` will add an agent to the list of agents tagged with this tag The tag can be given as a string or as an integer (its id). .. http:post:: /api/v1/tags/<str:tagname>/agents/ HTTP/1.1 **Request** .. sourcecode:: http POST /api/v1/tags/interesting/agents/ HTTP/1.1 Accept: application/json { "agent_id": "dd0c6da2-0c91-42cf-a82f-6d503aae43d3" } **Response (agent newly tagged)** .. sourcecode:: http HTTP/1.1 201 CREATED Content-Type: application/json { "href": "/api/v1/agents/1", "id": 1 } **Request** .. sourcecode:: http POST /api/v1/tags/interesting/agents/ HTTP/1.1 Accept: application/json { "agent_id": "dd0c6da2-0c91-42cf-a82f-6d503aae43d3" } **Response (agent already had that tag)** .. sourcecode:: http HTTP/1.1 200 OK Content-Type: application/json { "href": "/api/v1/agents/1", "id": 1 } :statuscode 200: an existing tag was found and returned :statuscode 201: a new tag was created :statuscode 400: there was something wrong with the request (such as invalid columns being included) :statuscode 404: either the tag or the referenced agent does not exist """ if isinstance(tagname, STRING_TYPES): tag = Tag.query.filter_by(tag=tagname).first() else: tag = Tag.query.filter_by(id=tagname).first() if tag is None: return jsonify(error="tag %s not found" % tagname), NOT_FOUND if not isinstance(g.json, dict): return jsonify(error="expected a json dictionary"), BAD_REQUEST request_fields = set(g.json) extra_fields = request_fields - set(["agent_id"]) if extra_fields: return jsonify(error="unsupported fields for " "this request: %s" % extra_fields), BAD_REQUEST if "agent_id" not in request_fields: return jsonify(error="field `agent_id` is missing"), BAD_REQUEST if not isinstance(g.json["agent_id"], STRING_TYPES): return jsonify( error="Expected a string for `agent_id`"), BAD_REQUEST agent = Agent.query.filter_by(id=g.json["agent_id"]).first() if agent is None: return jsonify( error="agent %s does not exist" % g.json["agent_id"]), NOT_FOUND if agent not in tag.agents: tag.agents.append(agent) db.session.commit() logger.debug( "added agent %s (%s) to tag %s", agent.id, agent.hostname, tag.tag) return jsonify( id=agent.id, href=url_for(".single_agent_api", agent_id=agent.id)), CREATED else: return jsonify( id=agent.id, href=url_for(".single_agent_api", agent_id=agent.id)), OK
[docs] def get(self, tagname=None): """ A ``GET`` to this endpoint will list all agents associated with this tag. .. http:get:: /api/v1/tags/<str:tagname>/agents/ HTTP/1.1 **Request** .. sourcecode:: http GET /api/v1/tags/interesting/agents/ HTTP/1.1 Accept: application/json **Response** .. sourcecode:: http HTTP/1.1 201 CREATED Content-Type: application/json [ { "hostname": "agent3", "id": 1, "href": "/api/v1/agents/1 } ] :statuscode 200: the list of agents associated with this tag is returned :statuscode 404: the tag specified does not exist """ if isinstance(tagname, STRING_TYPES): tag = Tag.query.filter_by(tag=tagname).first() else: tag = Tag.query.filter_by(id=tagname).first() if tag is None: return jsonify(error="tag %s not found" % tagname), NOT_FOUND out = [] for agent in tag.agents: out.append({ "id": agent.id, "hostname": agent.hostname, "href": url_for(".single_agent_api", agent_id=agent.id)}) return jsonify(out), OK