Пример #1
0
    def retrieve_deprecated(self, request, class_path):
        """
        Get details of a Job as identified by its class-path.

        This API endpoint is deprecated; it is recommended to use the extras_jobs_read endpoint instead.
        """
        if not request.user.has_perm("extras.view_job"):
            raise PermissionDenied("This user does not have permission to view jobs.")
        try:
            job_model = Job.objects.restrict(request.user, "view").get_for_class_path(class_path)
        except Job.DoesNotExist:
            raise Http404
        if not job_model.installed or job_model.job_class is None:
            raise Http404
        job_content_type = get_job_content_type()
        job = job_model.job_class()  # TODO: why do we need to instantiate the job_class?
        job.result = JobResult.objects.filter(
            obj_type=job_content_type,
            name=job.class_path,
            status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES,
        ).first()

        serializer = serializers.JobClassDetailSerializer(job, context={"request": request})

        return Response(serializer.data)
Пример #2
0
    def list(self, request, *args, **kwargs):
        """List all known Jobs."""
        if request.major_version > 1 or request.minor_version >= 3:
            # API version 1.3 or later - standard model-based response
            return super().list(request, *args, **kwargs)

        # API version 1.2 or earlier - serialize JobClass records
        if not request.user.has_perm("extras.view_job"):
            raise PermissionDenied("This user does not have permission to view jobs.")
        job_content_type = get_job_content_type()
        results = {
            r.name: r
            for r in JobResult.objects.filter(
                obj_type=job_content_type,
                status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES,
            )
            .defer("data")
            .order_by("created")
        }

        job_models = Job.objects.restrict(request.user, "view")
        jobs_list = [
            job_model.job_class()  # TODO: why do we need to instantiate the job_class?
            for job_model in job_models
            if job_model.installed and job_model.job_class is not None
        ]
        for job_instance in jobs_list:
            job_instance.result = results.get(job_instance.class_path, None)

        serializer = serializers.JobClassSerializer(jobs_list, many=True, context={"request": request})

        return Response(serializer.data)
Пример #3
0
def run_job_for_testing(job,
                        data=None,
                        commit=True,
                        username="******",
                        request=None):
    """Provide a common interface to run Nautobot jobs as part of unit tests.

    Args:
      job (Job): Job model instance (not Job class) to run
      data (dict): Input data values for any Job variables.
      commit (bool): Whether to commit changes to the database or rollback when done.
      username (str): Username of existing or to-be-created User account to own the JobResult. Ignored if `request.user`
        exists.
      request (HttpRequest): Existing request (if any) to own the JobResult.

    Returns:
      JobResult: representing the executed job
    """
    if data is None:
        data = {}

    # If the request has a user, ignore the username argument and use that user.
    if request and request.user:
        user_instance = request.user
    else:
        User = get_user_model()
        user_instance, _ = User.objects.get_or_create(username=username,
                                                      defaults={
                                                          "is_superuser": True,
                                                          "password":
                                                          "******"
                                                      })
    job_result = JobResult.objects.create(
        name=job.class_path,
        obj_type=get_job_content_type(),
        user=user_instance,
        job_model=job,
        job_id=uuid.uuid4(),
    )

    @contextmanager
    def _web_request_context(user):
        if request:
            yield request
        else:
            yield web_request_context(user=user)

    with _web_request_context(user=user_instance) as request:
        run_job(data=data,
                request=request,
                commit=commit,
                job_result_pk=job_result.pk)
    return job_result
Пример #4
0
    def related_object(self):
        """Get the related object, if any, identified by the `obj_type`, `name`, and/or `job_id` fields.

        If `obj_type` is extras.Job, then the `name` is used to look up an extras.jobs.Job subclass based on the
        `class_path` of the Job subclass.
        Note that this is **not** the extras.models.Job model class nor an instance thereof.

        Else, if the the model class referenced by `obj_type` has a `name` field, our `name` field will be used
        to look up a corresponding model instance. This is used, for example, to look up a related `GitRepository`;
        more generally it can be used by any model that 1) has a unique `name` field and 2) needs to have a many-to-one
        relationship between JobResults and model instances.

        Else, the `obj_type` and `job_id` will be used together as a quasi-GenericForeignKey to look up a model
        instance whose PK corresponds to the `job_id`. This behavior is currently unused in the Nautobot core,
        but may be of use to plugin developers wishing to create JobResults that have a one-to-one relationship
        to plugin model instances.

        This method is potentially rather slow as get_job() may need to actually load the Job class from disk;
        consider carefully whether you actually need to use it.
        """
        from nautobot.extras.jobs import get_job  # needed here to avoid a circular import issue

        if self.obj_type == get_job_content_type():
            # Related object is an extras.Job subclass, our `name` matches its `class_path`
            return get_job(self.name)

        model_class = self.obj_type.model_class()

        if model_class is not None:
            if hasattr(model_class, "name"):
                # See if we have a many-to-one relationship from JobResult to model_class record, based on `name`
                try:
                    return model_class.objects.get(name=self.name)
                except model_class.DoesNotExist:
                    pass

            # See if we have a one-to-one relationship from JobResult to model_class record based on `job_id`
            try:
                return model_class.objects.get(id=self.job_id)
            except model_class.DoesNotExist:
                pass

        return None
Пример #5
0
    def dry_run(self, request, pk):
        scheduled_job = get_object_or_404(ScheduledJob, pk=pk)
        job_model = scheduled_job.job_model
        if job_model is None or not job_model.runnable:
            raise MethodNotAllowed("This job cannot be dry-run at this time.")
        if not Job.objects.check_perms(request.user, instance=job_model, action="run"):
            raise PermissionDenied("You do not have permission to run this job.")

        # Immediately enqueue the job with commit=False
        job_content_type = get_job_content_type()
        job_result = JobResult.enqueue_job(
            run_job,
            job_model.class_path,
            job_content_type,
            request.user,
            data=scheduled_job.kwargs.get("data", {}),
            request=copy_safe_request(request),
            commit=False,  # force a dry-run
        )
        serializer = serializers.JobResultSerializer(job_result, context={"request": request})

        return Response(serializer.data)
Пример #6
0
def _run_job(request, job_model, legacy_response=False):
    """An internal function providing logic shared between JobModelViewSet.run() and JobViewSet.run()."""
    if not request.user.has_perm("extras.run_job"):
        raise PermissionDenied("This user does not have permission to run jobs.")
    if not job_model.enabled:
        raise PermissionDenied("This job is not enabled to be run.")
    if not job_model.installed:
        raise MethodNotAllowed(request.method, detail="This job is not presently installed and cannot be run")

    job_class = job_model.job_class
    if job_class is None:
        raise MethodNotAllowed(request.method, detail="This job's source code could not be located and cannot be run")
    job = job_class()

    input_serializer = serializers.JobInputSerializer(data=request.data)
    input_serializer.is_valid(raise_exception=True)

    data = input_serializer.data["data"] or {}
    commit = input_serializer.data["commit"]
    if commit is None:
        commit = job_model.commit_default

    try:
        job.validate_data(data)
    except FormsValidationError as e:
        # message_dict can only be accessed if ValidationError got a dict
        # in the constructor (saved as error_dict). Otherwise we get a list
        # of errors under messages
        return Response({"errors": e.message_dict if hasattr(e, "error_dict") else e.messages}, status=400)

    if not get_worker_count():
        raise CeleryWorkerNotRunningException()

    job_content_type = get_job_content_type()
    schedule_data = input_serializer.data.get("schedule")

    # Default to a null JobResult.
    job_result = None

    # Assert that a job with `approval_required=True` has a schedule that enforces approval and
    # executes immediately.
    if schedule_data is None and job_model.approval_required:
        schedule_data = {"interval": JobExecutionType.TYPE_IMMEDIATELY}

    # Try to create a ScheduledJob, or...
    if schedule_data:
        schedule = _create_schedule(schedule_data, data, commit, job, job_model, request)
    else:
        schedule = None

    # ... If we can't create one, create a JobResult instead.
    if schedule is None:
        job_result = JobResult.enqueue_job(
            run_job,
            job.class_path,
            job_content_type,
            request.user,
            data=data,
            request=copy_safe_request(request),
            commit=commit,
        )
        job.result = job_result

    if legacy_response:
        # Old-style JobViewSet response - serialize the Job class in the response for some reason?
        serializer = serializers.JobClassDetailSerializer(job, context={"request": request})
        return Response(serializer.data)
    else:
        # New-style JobModelViewSet response - serialize the schedule or job_result as appropriate
        data = {"schedule": None, "job_result": None}
        if schedule:
            data["schedule"] = nested_serializers.NestedScheduledJobSerializer(
                schedule, context={"request": request}
            ).data
        if job_result:
            data["job_result"] = nested_serializers.NestedJobResultSerializer(
                job_result, context={"request": request}
            ).data
        return Response(data, status=status.HTTP_201_CREATED)
Пример #7
0
    def handle(self, *args, **options):
        if "/" not in options["job"]:
            raise CommandError(
                'Job must be specified in the form "grouping_name/module_name/JobClassName"'
            )
        job_class = get_job(options["job"])
        if not job_class:
            raise CommandError('Job "%s" not found' % options["job"])

        user = None
        request = None
        if options["commit"] and not options["username"]:
            # Job execution with commit=True uses change_logging(), which requires a user as the author of any changes
            raise CommandError("--username is mandatory when --commit is used")

        if options["username"]:
            User = get_user_model()
            try:
                user = User.objects.get(username=options["username"])
            except User.DoesNotExist as exc:
                raise CommandError("No such user") from exc

            request = RequestFactory().request(
                SERVER_NAME="nautobot_server_runjob")
            request.id = uuid.uuid4()
            request.user = user

        job_content_type = get_job_content_type()

        # Run the job and create a new JobResult
        self.stdout.write("[{:%H:%M:%S}] Running {}...".format(
            timezone.now(), job_class.class_path))

        job_result = JobResult.enqueue_job(
            run_job,
            job_class.class_path,
            job_content_type,
            user,
            data=
            {},  # TODO: parsing CLI args into a data dictionary is not currently implemented
            request=copy_safe_request(request) if request else None,
            commit=options["commit"],
        )

        # Wait on the job to finish
        while job_result.status not in JobResultStatusChoices.TERMINAL_STATE_CHOICES:
            time.sleep(1)
            job_result = JobResult.objects.get(pk=job_result.pk)

        # Report on success/failure
        groups = set(
            JobLogEntry.objects.filter(job_result=job_result).values_list(
                "grouping", flat=True))
        for group in sorted(groups):
            logs = JobLogEntry.objects.filter(job_result__pk=job_result.pk,
                                              grouping=group)
            success_count = logs.filter(
                log_level=LogLevelChoices.LOG_SUCCESS).count()
            info_count = logs.filter(
                log_level=LogLevelChoices.LOG_INFO).count()
            warning_count = logs.filter(
                log_level=LogLevelChoices.LOG_WARNING).count()
            failure_count = logs.filter(
                log_level=LogLevelChoices.LOG_FAILURE).count()

            self.stdout.write(
                "\t{}: {} success, {} info, {} warning, {} failure".format(
                    group,
                    success_count,
                    info_count,
                    warning_count,
                    failure_count,
                ))

            for log_entry in logs:
                status = log_entry.log_level
                if status == "success":
                    status = self.style.SUCCESS(status)
                elif status == "info":
                    status = status
                elif status == "warning":
                    status = self.style.WARNING(status)
                elif status == "failure":
                    status = self.style.NOTICE(status)

                if log_entry.log_object:
                    self.stdout.write(
                        f"\t\t{status}: {log_entry.log_object}: {log_entry.message}"
                    )
                else:
                    self.stdout.write(f"\t\t{status}: {log_entry.message}")

        if job_result.data["output"]:
            self.stdout.write(job_result.data["output"])

        if job_result.status == JobResultStatusChoices.STATUS_FAILED:
            status = self.style.ERROR("FAILED")
        elif job_result.status == JobResultStatusChoices.STATUS_ERRORED:
            status = self.style.ERROR("ERRORED")
        else:
            status = self.style.SUCCESS("SUCCESS")
        self.stdout.write("[{:%H:%M:%S}] {}: {}".format(
            timezone.now(), job_class.class_path, status))

        # Wrap things up
        self.stdout.write("[{:%H:%M:%S}] {}: Duration {}".format(
            timezone.now(), job_class.class_path, job_result.duration))
        self.stdout.write("[{:%H:%M:%S}] Finished".format(timezone.now()))