Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Functional changes to use the concrete job model where appropriate #1457

Merged
merged 28 commits into from
Mar 11, 2022

Conversation

glennmatthews
Copy link
Contributor

@glennmatthews glennmatthews commented Mar 7, 2022

Relates-to: #1001

REST API changes:

  • Added /api/extras/job-models/ URL pattern; existing /api/extras/jobs/ should be considered deprecated
    • As we've discussed, once we have a decision on how we want to formally begin using REST API versioning, this may change.
  • This viewset supports listing, retrieving, updating, bulk-updating, deleting, and bulk-deleting Job records, but not creating or bulk-creating them (same pattern as the UI)
  • The new /api/extras/job-models/<pk>/run API endpoint accepts the same parameters as the existing job-running API endpoint, but instead of returning a serialized job-class instance (why?) now returns either a serialized JobResult or a serialized ScheduledJob depending on which action resulted from the request.
    • Open to discussion as to whether we should instead split this out into a pair of endpoints, i.e. one for run-immediately and one for schedule/submit-for-approval; the current implementation was basically the lowest-effort way to get a working v2 API endpoint.
  • Also, as GET api/extras/job-models/<pk> returns a serialized JobModel, not a serialized JobClass, this endpoint does not return information about the parameters expected on job submission as that's a JobClass property. To fill this gap, I added an api/extras/job-models/<pk>/variables query endpoint which returns a detailed view into the job variables.
    • I just realized I didn't add any unit test automation for this, whoops!
  • The JobResult and ScheduledJob serializers/API endpoints now include a NestedJobSerializer representation of the associated JobModel.
  • Both the existing JobClass and new JobModel REST API viewsets now properly enforce object-based permissions on JobModel when retrieving, editing, deleting, and running Jobs.
  • I don't think the ScheduledJob approval/dry-run REST API endpoints currently enforce permissions properly, I need to look into this and add some test coverage (opened [1.3] Scheduled-job approval/dry-run REST API doesn't enforce object-based permissions yet #1478 to cover this gap)

UI changes:

  • JobResult and ScheduledJob tables now can be filtered by associated JobModel
  • JobClass.as_form() now derives its commit_default and read_only properties from the JobModel rather than the JobClass
  • Job form view for submitting or scheduling a job (job.html) derives its name, description, read_only and approval_required properties from the JobModel rather than the JobClass.
    • Additionally it warns the user and disables the submit button if the JobModel is not both installed and enabled.
  • Job approval form derives its name, description, read_only properties from the JobModel rather than the JobClass
    • Additionally it warns the user if the JobModel is not both installed and enabled
    • Need to add logic to disable the dry-run and approve buttons if the job is not runnable
  • JobResult detail view pulls grouping, name, and description from the JobModel if available (with a fallback to the JobClass available for now)

Functional changes:

  • run_job refuses to run if the associated JobModel is not both installed and enabled.
  • run_job derives its soft_time_limit, time_limit, and read_only properties from the JobModel rather than the JobClass
  • JobResult.enqueue_job pulls the soft_time_limit and time_limit from the JobModel rather than the JobClass.
  • Added .get_for_class_path convenience method to the JobModel manager.
  • Job and job approval UI views pull information from the JobModel instead of the JobClass as appropriate
    • Object-based permissions are now enforced for the run job UI view
    • Need to enforce and test object-based permissions for approving a scheduled job

Database changes:

  • Added a git_repository foreign-key on JobModel, Jobs derived from a Git repository now have source = "git" and an appropriately set foreign-key, instead of the previous behavior of source = "git.{repository_slug}".
  • Added appropriate schema and data migrations.
  • Added clean logic on JobModel to ensure that field length limits are enforced for the fields that are auto-populated from JobClass code.

Testing changes:

  • All existing JobClass running API tests in test_api.py are now also executed against the new JobModel run API.
    • Added API tests for object-based permissions in both run-job API views
    • Added API tests for running a non-installed JobModel in both API views
    • Added standard/generic API tests for the new list/retrieve/update/delete APIs
    • As noted above I need to add unit tests for the /variables API endpoint still
  • Added logic in test_jobs.py to ensure that the JobModel is enabled when performing various job-running tests.
    • Also did a bunch of refactoring here to eliminate a bunch of repeated boilerplate
  • test_models.py adds some tests on input validation when generating a JobModel.
  • Refactored test_scripts.py because inline declaration of Script (JobClass) classes within a test case don't work so well when we need a JobModel associated with them.
  • test_views.py runs all of its tests against both the existing class-path-based URL patterns as well as the new slug-based URL patterns for jobs
    • Added tests for object-based permissions as well.

Copy link
Contributor

@jathanism jathanism left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know how you do it, but another large body of work that requires little input. I just have a couple comments, but nothing deal-breaking!

nautobot/extras/querysets.py Show resolved Hide resolved
Comment on lines +1206 to +1218
def _get_detail_url(self, instance):
"""
Override default implementation as we're under "jobmodel-detail" rather than "job-detail".
"""
viewname = f"{self._get_view_namespace()}:jobmodel-detail"
return reverse(viewname, kwargs={"pk": instance.pk})

def _get_list_url(self):
"""
Override default implementation as we're under "jobmodel-list" rather than "job-list".
"""
viewname = f"{self._get_view_namespace()}:jobmodel-list"
return reverse(viewname)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These rout names just gave me pause. Are these non-standard URL route names going to break some asserted patterns with helpers/utilities/templatetags/etc. where the singular is expected to match the model name?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quite possibly, yes. The problem is that for the REST API, the job-* URL patterns are already in use by the existing (JobClass-based) API views. I could rename those and reclaim those patterns for the new API views but I was concerned about the possibility of breaking plugins by doing so. Happy to discuss further.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be revisited as part of #1465 I think.

self.assertIn("schedule", response.data)
self.assertIn("job_result", response.data)
self.assertIsNone(response.data["schedule"])
# The urls in a NestedJobResultSerializer depends on the request context, which we don't have
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you fake a request with django.test.RequestFactory? This comment helps but it's not entirely clear why deleting the url fields is necessary otherwise. (It's not a big deal, just more of a curiosity.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll look into this. Thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried some various approaches using DRF's APIRequestFactory but couldn't figure out the magic sauce to make it work - I kept getting 403 Forbidden responses even when explicitly using force_authenticate(request). Leaving this as-is for now.

@glennmatthews
Copy link
Contributor Author

glennmatthews commented Mar 8, 2022

Additional TODOs (opened #1479 for these)

  • the Job documentation needs to be updated to explain how JobModel overrides JobClass
  • the Job documentation needs to be updated for the new REST API

Copy link
Member

@bryanculver bryanculver left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few nits but looks good overall.

nautobot/docs/release-notes/version-1.3.md Outdated Show resolved Hide resolved
nautobot/extras/api/views.py Outdated Show resolved Hide resolved
nautobot/extras/api/views.py Show resolved Hide resolved
job_content_type = ContentType.objects.get(app_label="extras", model="job")

schedule_data = input_serializer.data.get("schedule")
# BUG TODO: it looks like by omitting "schedule" you can immediately run an approval_required job here...
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is the case, worthwhile backporting the fix to this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filed #1476 to investigate this and fix it if needed.

job_content_type,
scheduled_job.user,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should remove this as a bug fix to current release.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened #1475 for this.

nautobot/extras/templates/extras/job.html Outdated Show resolved Hide resolved
@@ -82,22 +92,30 @@ <h1>{{ job }}</h1>
{% endif %}
</div>
<div class="pull-right">
<button type="submit" name="_run" id="id__run" class="btn btn-primary"{% if not perms.extras.run_job %} disabled="disabled"{% endif %}><i class="mdi mdi-play"></i> Run Job Now</button>
<button type="submit" name="_run" id="id__run" class="btn btn-primary"
{% if not perms.extras.run_job or not job_model.installed or not job_model.enabled or job_model.job_class is None %}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe future enhancement:

can_i_run(user):
  return bool, reasons

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now I added a JobModel.runnable property that just aliases (installed and enabled and job_class is not None) as a step in the right direction.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect! Just corroborating the job_model introspection would be better as a template variable than a string of conditions inside the template logic and that solves it concisely.

nautobot/extras/tests/test_api.py Outdated Show resolved Hide resolved
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants