""" 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 uuid import falcon from freezer_api.api.common import resource from freezer_api.common import exceptions as freezer_api_exc from freezer_api import policy 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 """ @policy.enforce('jobs:get_all') 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', min=0) or 0 limit = req.get_param_as_int('limit', min=1) 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} @policy.enforce('jobs:create') 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} """ @policy.enforce('jobs:get') 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 @policy.enforce('jobs:delete') 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') obj = self.db.get_job(user_id=user_id, job_id=job_id) if not obj: raise freezer_api_exc.DocumentNotFound( message='No Job found with ID:{0}'. format(job_id)) else: self.db.delete_job(user_id=user_id, job_id=job_id) resp.body = {'job_id': job_id} resp.status = falcon.HTTP_204 @policy.enforce('jobs:update') 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} @policy.enforce('jobs:create') 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 @policy.enforce('jobs:event:create') 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(iter(doc.items())) 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_schedule.get('event') != 'start': self.job_schedule['event'] = 'start' self.job_schedule['status'] = '' self.job_schedule['result'] = '' self.need_update = True return 'success' return 'start already requested' def stop(self, params=None): if self.job_schedule.get('event') != 'stop': self.job_schedule['event'] = 'stop' self.need_update = True return 'success' return 'stop already requested' def abort(self, params=None): if self.job_schedule.get('event') != 'abort': self.job_schedule['event'] = 'abort' self.need_update = True return 'success' return 'abort already requested' 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 action_defaults.items(): 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.BadDataFormat( message="action_defaults shouldbe a dictionary" )