text: # 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 pecan
  from pecan import expose
  from pecan import rest
  import six
  
  from oslo_log import log as logging
  from oslo_utils import timeutils
  
  from tricircle.common import constants
  import tricircle.common.context as t_context
  import tricircle.common.exceptions as t_exc
  from tricircle.common.i18n import _
  from tricircle.common import policy
  from tricircle.common import utils
  from tricircle.common import xrpcapi
  from tricircle.db import api as db_api
  
  LOG = logging.getLogger(__name__)
  
  
  class AsyncJobController(rest.RestController):
      # with AsyncJobController, admin can create, show, delete and
      # redo asynchronous jobs
  
      def __init__(self):
          self.xjob_handler = xrpcapi.XJobAPI()
  
      @expose(generic=True, template='json')
      def post(self, **kw):
          context = t_context.extract_context_from_environ()
          job_resource_map = constants.job_resource_map
  
          if not policy.enforce(context, policy.ADMIN_API_JOB_CREATE):
              return utils.format_api_error(
                  403, _("Unauthorized to create a job"))
  
          if 'job' not in kw:
              return utils.format_api_error(
                  400, _("Request body not found"))
  
          job = kw['job']
  
          for field in ('type', 'project_id'):
              value = job.get(field)
              if value is None:
                  return utils.format_api_error(
                      400, _("%(field)s isn't provided in request body") % {
                          'field': field})
              elif len(value.strip()) == 0:
                  return utils.format_api_error(
                      400, _("%(field)s can't be empty") % {'field': field})
  
          if job['type'] not in job_resource_map.keys():
              return utils.format_api_error(
                  400, _('There is no such job type: %(job_type)s') % {
                      'job_type': job['type']})
  
          job_type = job['type']
          project_id = job['project_id']
  
          if 'resource' not in job:
              return utils.format_api_error(
                  400, _('Failed to create job, because the resource is not'
                         ' specified'))
  
          # verify that all given resources are exactly needed
          request_fields = set(job['resource'].keys())
          require_fields = set([resource_id
                                for resource_type, resource_id in
                                job_resource_map[job_type]])
          missing_fields = require_fields - request_fields
          redundant_fields = request_fields - require_fields
  
          if missing_fields:
                  return utils.format_api_error(
                      400, _('Some required fields are not specified:'
                             ' %(field)s') % {'field': missing_fields})
          if redundant_fields:
                  return utils.format_api_error(
                      400, _('Some fields are redundant: %(field)s') % {
                          'field': redundant_fields})
  
          # validate whether the project id is legal
          resource_type_1, resource_id_1 = (
              constants.job_primary_resource_map[job_type])
          if resource_type_1 is not None:
              filter = [{'key': 'project_id', 'comparator': 'eq',
                         'value': project_id},
                        {'key': 'resource_type', 'comparator': 'eq',
                         'value': resource_type_1},
                        {'key': 'top_id', 'comparator': 'eq',
                         'value': job['resource'][resource_id_1]}]
  
              routings = db_api.list_resource_routings(context, filter)
              if not routings:
                  msg = (_("%(resource)s %(resource_id)s doesn't belong to the"
                           " project %(project_id)s") %
                         {'resource': resource_type_1,
                          'resource_id': job['resource'][resource_id_1],
                          'project_id': project_id})
                  return utils.format_api_error(400, msg)
  
          # if job_type = seg_rule_setup, we should ensure the project id
          # is equal to the one from resource.
          if job_type in (constants.JT_SEG_RULE_SETUP,
                          constants.JT_RESOURCE_RECYCLE):
              if job['project_id'] != job['resource']['project_id']:
                  msg = (_("Specified project_id %(project_id_1)s and resource's"
                           " project_id %(project_id_2)s are different") %
                         {'project_id_1': job['project_id'],
                          'project_id_2': job['resource']['project_id']})
                  return utils.format_api_error(400, msg)
  
          # combine uuid into target resource id
          resource_id = '#'.join([job['resource'][resource_id]
                                  for resource_type, resource_id
                                  in job_resource_map[job_type]])
  
          try:
              # create a job and put it into execution immediately
              self.xjob_handler.invoke_method(context, project_id,
                                              constants.job_handles[job_type],
                                              job_type, resource_id)
          except Exception as e:
              LOG.exception('Failed to create job: '
                            '%(exception)s ', {'exception': e})
              return utils.format_api_error(
                  500, _('Failed to create a job'))
  
          new_job = db_api.get_latest_job(context, constants.JS_New, job_type,
                                          resource_id)
          return {'job': self._get_more_readable_job(new_job)}
  
      @expose(generic=True, template='json')
      def get_one(self, id, **kwargs):
          """the return value may vary according to the value of id
  
          :param id: 1) if id = 'schemas', return job schemas
                     2) if id = 'detail', return all jobs
                     3) if id = $job_id, return detailed single job info
          :return: return value is decided by id parameter
          """
          context = t_context.extract_context_from_environ()
          job_resource_map = constants.job_resource_map
  
          if not policy.enforce(context, policy.ADMIN_API_JOB_SCHEMA_LIST):
              return utils.format_api_error(
                  403, _('Unauthorized to show job information'))
  
          if id == 'schemas':
              job_schemas = []
              for job_type in job_resource_map.keys():
                  job = {}
                  resource = []
                  for resource_type, resource_id in job_resource_map[job_type]:
                      resource.append(resource_id)
  
                  job['resource'] = resource
                  job['type'] = job_type
                  job_schemas.append(job)
  
              return {'schemas': job_schemas}
  
          if id == 'detail':
              return self.get_all(**kwargs)
  
          try:
              job = db_api.get_job(context, id)
              return {'job': self._get_more_readable_job(job)}
          except Exception:
              try:
                  job = db_api.get_job_from_log(context, id)
                  return {'job': self._get_more_readable_job(job)}
              except t_exc.ResourceNotFound:
                  return utils.format_api_error(
                      404, _('Resource not found'))
  
      @expose(generic=True, template='json')
      def get_all(self, **kwargs):
          """Get all the jobs. Using filters, only get a subset of jobs.
  
          :param kwargs: job filters
          :return: a list of jobs
          """
          context = t_context.extract_context_from_environ()
  
          if not policy.enforce(context, policy.ADMIN_API_JOB_LIST):
              return utils.format_api_error(
                  403, _('Unauthorized to show all jobs'))
  
          # check limit and marker, default value -1 means no pagination
          _limit = kwargs.pop('limit', -1)
  
          try:
              limit = int(_limit)
              limit = utils.get_pagination_limit(limit)
          except ValueError as e:
              LOG.exception('Failed to convert pagination limit to an integer: '
                            '%(exception)s ', {'exception': e})
              msg = (_("Limit should be an integer or a valid literal "
                       "for int() rather than '%s'") % _limit)
              return utils.format_api_error(400, msg)
  
          marker = kwargs.pop('marker', None)
  
          sorts = [('timestamp', 'desc'), ('id', 'desc')]
          is_valid_filter, filters = self._get_filters(kwargs)
  
          if not is_valid_filter:
              msg = (_('Unsupported filter type: %(filters)s') % {
                  'filters': ', '.join(
                      [filter_name for filter_name in filters])
              })
              return utils.format_api_error(400, msg)
  
          # project ID from client should be equal to the one from
          # context, since only the project ID in which the user
          # is authorized will be used as the filter.
          filters['project_id'] = context.project_id
          filters = [{'key': key, 'comparator': 'eq', 'value': value}
                     for key, value in six.iteritems(filters)]
  
          try:
              if marker is not None:
                  try:
                      # verify whether the marker is effective
                      db_api.get_job(context, marker)
                      jobs = db_api.list_jobs(context, filters,
                                              sorts, limit, marker)
                      jobs_from_log = []
                      if len(jobs) < limit:
                          jobs_from_log = db_api.list_jobs_from_log(
                              context, filters, sorts, limit - len(jobs), None)
                      job_collection = jobs + jobs_from_log
                  except t_exc.ResourceNotFound:
                      try:
                          db_api.get_job_from_log(context, marker)
                          jobs_from_log = db_api.list_jobs_from_log(
                              context, filters, sorts, limit, marker)
                          job_collection = jobs_from_log
                      except t_exc.ResourceNotFound:
                          msg = (_('Invalid marker: %(marker)s')
                                 % {'marker': marker})
                          return utils.format_api_error(400, msg)
              else:
                  jobs = db_api.list_jobs(context, filters,
                                          sorts, limit, marker)
                  jobs_from_log = []
                  if len(jobs) < limit:
                      jobs_from_log = db_api.list_jobs_from_log(
                          context, filters, sorts, limit - len(jobs), None)
                  job_collection = jobs + jobs_from_log
              # add link
              links = []
              if len(job_collection) >= limit:
                  marker = job_collection[-1]['id']
                  base = constants.JOB_PATH
                  link = "%s?limit=%s&marker=%s" % (base, limit, marker)
                  links.append({"rel": "next",
                                "href": link})
  
              result = {'jobs': [self._get_more_readable_job(job)
                                 for job in job_collection]}
              if links:
                  result['jobs_links'] = links
              return result
          except Exception as e:
              LOG.exception('Failed to show all asynchronous jobs: '
                            '%(exception)s ', {'exception': e})
              return utils.format_api_error(
                  500, _('Failed to show all asynchronous jobs'))
  
      # make the job status and resource id more human readable. Split
      # resource id into several member uuid(s) to provide more detailed resource
      # information. If job entry is from job table, then remove resource id
      # and extra id from job attributes. If job entry is from job log table,
      # only remove resource id from job attributes.
      def _get_more_readable_job(self, job):
          job_resource_map = constants.job_resource_map
  
          if 'status' in job:
              job['status'] = constants.job_status_map[job['status']]
          else:
              job['status'] = constants.job_status_map[constants.JS_Success]
  
          job['resource'] = dict(zip([resource_id
                                      for resource_type, resource_id
                                      in job_resource_map[job['type']]],
                                     job['resource_id'].split('#')))
          job.pop('resource_id')
  
          if "extra_id" in job:
              job.pop('extra_id')
  
          return job
  
      def _get_filters(self, params):
          """Return a dictionary of query param filters from the request.
  
          :param params: the URI params coming from the wsgi layer
          :return: (flag, filters), flag indicates whether the filters are valid,
          and the filters denote a list of key-value pairs.
          """
          filters = {}
          unsupported_filters = {}
          for filter_name in params:
              if filter_name in constants.JOB_LIST_SUPPORTED_FILTERS:
                  # map filter name
                  if filter_name == 'status':
                      job_status_in_db = self._get_job_status_in_db(
                          params.get(filter_name))
                      filters[filter_name] = job_status_in_db
                      continue
                  filters[filter_name] = params.get(filter_name)
              else:
                  unsupported_filters[filter_name] = params.get(filter_name)
  
          if unsupported_filters:
              return False, unsupported_filters
          return True, filters
  
      # map user input job status to job status stored in database
      def _get_job_status_in_db(self, job_status):
          job_status_map = {
              'fail': constants.JS_Fail,
              'success': constants.JS_Success,
              'running': constants.JS_Running,
              'new': constants.JS_New
          }
          job_status_lower = job_status.lower()
          if job_status_lower in job_status_map:
              return job_status_map[job_status_lower]
          return job_status
  
      @expose(generic=True, template='json')
      def delete(self, job_id):
          # delete a job from the database. If the job is running, the delete
          # operation will fail. In other cases, job will be deleted directly.
          context = t_context.extract_context_from_environ()
  
          if not policy.enforce(context, policy.ADMIN_API_JOB_DELETE):
              return utils.format_api_error(
                  403, _('Unauthorized to delete a job'))
  
          try:
              db_api.get_job_from_log(context, job_id)
              return utils.format_api_error(
                  400, _('Job %(job_id)s is from job log') % {'job_id': job_id})
          except Exception:
              try:
                  job = db_api.get_job(context, job_id)
              except t_exc.ResourceNotFound:
                  return utils.format_api_error(
                      404, _('Job %(job_id)s not found') % {'job_id': job_id})
          try:
              # if job status = RUNNING, notify user this new one, delete
              # operation fails.
              if job['status'] == constants.JS_Running:
                  return utils.format_api_error(
                      400, (_('Failed to delete the running job %(job_id)s') %
                            {"job_id": job_id}))
              # if job status = SUCCESS, move the job entry to job log table,
              # then delete it from job table.
              elif job['status'] == constants.JS_Success:
                  db_api.finish_job(context, job_id, True, timeutils.utcnow())
                  pecan.response.status = 200
                  return {}
  
              db_api.delete_job(context, job_id)
              pecan.response.status = 200
              return {}
          except Exception as e:
              LOG.exception('Failed to delete the job: '
                            '%(exception)s ', {'exception': e})
              return utils.format_api_error(
                  500, _('Failed to delete the job'))
  
      @expose(generic=True, template='json')
      def put(self, job_id):
          # we use HTTP/HTTPS PUT method to redo a job. Regularly PUT method
          # requires a request body, but considering the job redo operation
          # doesn't need more information other than job id, we will issue
          # this request without a request body.
          context = t_context.extract_context_from_environ()
  
          if not policy.enforce(context, policy.ADMIN_API_JOB_REDO):
              return utils.format_api_error(
                  403, _('Unauthorized to redo a job'))
  
          try:
              db_api.get_job_from_log(context, job_id)
              return utils.format_api_error(
                  400, _('Job %(job_id)s is from job log') % {'job_id': job_id})
          except Exception:
              try:
                  job = db_api.get_job(context, job_id)
              except t_exc.ResourceNotFound:
                  return utils.format_api_error(
                      404, _('Job %(job_id)s not found') % {'job_id': job_id})
  
          try:
              # if status = RUNNING, notify user this new one and then exit
              if job['status'] == constants.JS_Running:
                  return utils.format_api_error(
                      400, (_("Can't redo job %(job_id)s which is running") %
                            {'job_id': job['id']}))
              # if status = SUCCESS, notify user this new one and then exit
              elif job['status'] == constants.JS_Success:
                  msg = (_("Can't redo job %(job_id)s which had run successfully"
                           ) % {'job_id': job['id']})
                  return utils.format_api_error(400, msg)
              # if job status =  FAIL or job status = NEW, redo it immediately
              self.xjob_handler.invoke_method(context, job['project_id'],
                                              constants.job_handles[job['type']],
                                              job['type'], job['resource_id'])
          except Exception as e:
              LOG.exception('Failed to redo the job: '
                            '%(exception)s ', {'exception': e})
              return utils.format_api_error(
                  500, _('Failed to redo the job'))
  