freezer-api/freezer_api/api/v1/jobs.py

299 lines
10 KiB
Python

"""
Copyright 2015 Hewlett-Packard
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.
"""
import falcon
from six import iteritems
import uuid
from freezer_api.api.common import resource
from freezer_api.common import exceptions as freezer_api_exc
class JobsBaseResource(resource.BaseResource):
"""
Base class able to create actions contained in a job document
"""
def __init__(self, storage_driver):
self.db = storage_driver
def get_action(self, user_id, action_id):
found_action = None
try:
found_action = self.db.get_action(
user_id=user_id, action_id=action_id)
except freezer_api_exc.DocumentNotFound:
pass
return found_action
def update_actions_in_job(self, user_id, job_doc):
"""
Looks into a job document and creates actions in the db.
Actions are given an action_id if they don't have one yet
"""
job = Job(job_doc)
for action in job.actions():
if action.action_id:
# action has action_id, let's see if it's in the db
found_action_doc = self.get_action(
user_id=user_id, action_id=action.action_id)
if found_action_doc:
if action == Action(found_action_doc):
# action already present in the db, do nothing
continue
else:
# action is different, generate new action_id
action.action_id = ''
# action not found in db, leave current action_id
self.db.add_action(user_id=user_id, doc=action.doc)
class JobsCollectionResource(JobsBaseResource):
"""
Handler for endpoint: /v1/jobs
"""
def on_get(self, req, resp):
# GET /v1/jobs(?limit,offset) Lists jobs
user_id = req.get_header('X-User-ID')
offset = req.get_param_as_int('offset') or 0
limit = req.get_param_as_int('limit') or 10
search = self.json_body(req)
obj_list = self.db.search_job(user_id=user_id, offset=offset,
limit=limit, search=search)
resp.body = {'jobs': obj_list}
def on_post(self, req, resp):
# POST /v1/jobs Creates job entry
try:
job = Job(self.json_body(req))
except KeyError:
raise freezer_api_exc.BadDataFormat(
message='Missing request body')
user_id = req.get_header('X-User-ID')
self.update_actions_in_job(user_id, job.doc)
job_id = self.db.add_job(user_id=user_id, doc=job.doc)
resp.status = falcon.HTTP_201
resp.body = {'job_id': job_id}
class JobsResource(JobsBaseResource):
"""
Handler for endpoint: /v1/jobs/{job_id}
"""
def on_get(self, req, resp, job_id):
# GET /v1/jobs/{job_id} retrieves the specified job
# search in body
user_id = req.get_header('X-User-ID') or ''
obj = self.db.get_job(user_id=user_id, job_id=job_id)
if obj:
resp.body = obj
else:
resp.status = falcon.HTTP_404
def on_delete(self, req, resp, job_id):
# DELETE /v1/jobs/{job_id} Deletes the specified job
user_id = req.get_header('X-User-ID')
self.db.delete_job(user_id=user_id, job_id=job_id)
resp.body = {'job_id': job_id}
resp.status = falcon.HTTP_204
def on_patch(self, req, resp, job_id):
# PATCH /v1/jobs/{job_id} updates the specified job
user_id = req.get_header('X-User-ID') or ''
job = Job(self.json_body(req))
self.update_actions_in_job(user_id, job.doc)
new_version = self.db.update_job(user_id=user_id,
job_id=job_id,
patch_doc=job.doc)
resp.body = {'job_id': job_id, 'version': new_version}
def on_post(self, req, resp, job_id):
# PUT /v1/jobs/{job_id} creates/replaces the specified job
user_id = req.get_header('X-User-ID') or ''
job = Job(self.json_body(req))
self.update_actions_in_job(user_id, job.doc)
new_version = self.db.replace_job(user_id=user_id,
job_id=job_id,
doc=job.doc)
resp.status = falcon.HTTP_201
resp.body = {'job_id': job_id, 'version': new_version}
class JobsEvent(resource.BaseResource):
"""
Handler for endpoint: /v1/jobs/{job_id}/event
Actions are passed in the body, for example:
{
"start": null
}
"""
def __init__(self, storage_driver):
self.db = storage_driver
def on_post(self, req, resp, job_id):
# POST /v1/jobs/{job_id}/event
# requests an event on the specified job
user_id = req.get_header('X-User-ID') or ''
doc = self.json_body(req)
try:
event, params = next(iteritems(doc))
except Exception:
raise freezer_api_exc.BadDataFormat("Bad event request format")
job_doc = self.db.get_job(user_id=user_id,
job_id=job_id)
job = Job(job_doc)
result = job.execute_event(event, params)
if job.need_update:
self.db.replace_job(user_id=user_id,
job_id=job_id,
doc=job.doc)
resp.status = falcon.HTTP_202
resp.body = {'result': result}
class Action(object):
def __init__(self, doc):
self.doc = doc
@property
def action_id(self):
return self.doc.get('action_id', '')
@action_id.setter
def action_id(self, value):
self.doc['action_id'] = value
def create_new_action_id(self):
self.doc['action_id'] = uuid.uuid4().hex
def __eq__(self, other):
# return self.doc == other.doc
dont_care_keys = ['_version', 'user_id']
lh = self.doc.get('freezer_action', None)
rh = other.doc.get('freezer_action', None)
diffkeys = [k for k in lh if lh[k] != rh.get(k)]
diffkeys += [k for k in rh if rh[k] != lh.get(k)]
for k in diffkeys:
if k not in dont_care_keys:
return False
return True
def __ne__(self, other):
return not (self.__eq__(other))
class Job(object):
"""
A class with knowledge of the inner working of a job data structure.
Responibilities:
- manage the events that can be sent to a job. The result of handling
an event is a modification of the information contained in the
job document
- extract actions from a job (usage example: to be used to create actions)
"""
def __init__(self, doc):
self.doc = doc
if self.doc.get("action_defaults") is not None:
self.expand_default_properties()
self.event_result = ''
self.need_update = False
if 'job_schedule' not in doc:
doc['job_schedule'] = {}
self.job_schedule = doc['job_schedule']
self.event_handlers = {'start': self.start,
'stop': self.stop,
'abort': self.abort}
def execute_event(self, event, params):
handler = self.event_handlers.get(event, None)
if not handler:
raise freezer_api_exc.BadDataFormat("Bad Action Method")
try:
self.event_result = handler(params)
except freezer_api_exc.BadDataFormat:
raise
except Exception as e:
raise freezer_api_exc.FreezerAPIException(e)
return self.event_result
@property
def job_status(self):
return self.job_schedule.get('status', '')
@job_status.setter
def job_status(self, value):
self.job_schedule['status'] = value
def start(self, params=None):
if self.job_status in ["scheduled", "running"]:
return 'already active'
if self.job_status in ["completed", "stop", ""]:
# completed jobs are not acquired by the scheduler
self.job_status = 'stop'
self.job_schedule['event'] = 'start'
self.job_schedule['result'] = ''
self.need_update = True
return 'success'
else:
raise freezer_api_exc.BadDataFormat("unable to start a {0} job"
.format(self.job_status))
def stop(self, params=None):
if self.job_status in ["scheduled", "running", ""]:
self.job_schedule['event'] = 'stop'
self.need_update = True
return 'success'
else:
return 'already stopped'
def abort(self, params=None):
if self.job_status in ["scheduled", "running", ""]:
self.job_schedule['event'] = 'abort'
self.need_update = True
return 'success'
else:
return 'already stopped'
def actions(self):
"""
Generator to iterate over the actions contained in a job
:return: yields Action objects
"""
for action_doc in self.doc.get('job_actions', []):
yield Action(action_doc)
def expand_default_properties(self):
action_defaults = self.doc.pop("action_defaults")
if isinstance(action_defaults, dict):
for key, val in iteritems(action_defaults):
for action in self.doc.get("job_actions"):
if action["freezer_action"].get(key) is None:
action["freezer_action"][key] = val
else:
raise freezer_api_exc.BadDataForma(message="action_defaults should"
"be a dictionary")