Coverage for netbox_interface_name_rules / jobs.py: 97.40%
65 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-10-16 10:13 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-10-16 10:13 +0000
1# SPDX-License-Identifier: Apache-2.0
2# Copyright (C) 2025 Marcin Zieba <marcinpsk@gmail.com>
3"""Background jobs for bulk rule application."""
5import contextvars
6import functools
7import logging
8from abc import abstractmethod
9from contextlib import ExitStack
11from django.http import HttpRequest
12from netbox.jobs import JobRunner
13from netbox.registry import registry
15from . import branching
16from .transactions import write_alias, write_scope
19def rule_job_kwargs(rule_id):
20 """Return the kwargs of a job on the rule *rule_id* in the branch of the current request, derived on the server."""
21 return {"rule_id": rule_id, "branch_schema_id": branching.branch_identity(), "expected_alias": write_alias()}
24def _run_under_request_processors(request, body):
25 """Call *body* inside every registered request processor; unlike NetBox, a processor that fails to enter raises."""
26 with ExitStack() as stack:
27 for request_processor in registry["request_processors"]:
28 stack.enter_context(request_processor(request))
29 return body()
32def run_as_job_user(job, body, *, branch_schema_id):
33 """Call *body*, which takes no arguments, as a request of the user who enqueued *job*.
35 NetBox writes a change log only while a request is current, and a worker has none. The request
36 carries the job ID, so the change log lists the changes of one job under one request ID. The
37 request is in the branch *branch_schema_id*, or on main for None.
38 """
39 if job.user is None:
40 raise ValueError(f"Job {job.pk} has no user, and the change log must name the user of each change.")
41 # A Django request has each attribute that NetBox copies when an event rule acts on a change.
42 request = HttpRequest()
43 request.method = "POST"
44 request.user = job.user
45 request.id = job.job_id
46 branching.activate_on(request, branch_schema_id)
47 # The copy discards what the processors set; before NetBox 4.7, event_tracking keeps it when the body raises.
48 return contextvars.copy_context().run(_run_under_request_processors, request, body)
51class RuleJobRunner(JobRunner):
52 """A background job over the rule named by rule_id, run as a request of the user who enqueued it.
54 Its kwargs are those of ``rule_job_kwargs``. A job enqueued without them fails.
55 """
57 def __init__(self, job):
58 super().__init__(job)
59 # NetBox 4.4 added this logger, which also writes the job log; NetBox 4.3 has none.
60 if not hasattr(self, "logger"): 60 ↛ 61line 60 didn't jump to line 61 because the condition on line 60 was never true
61 self.logger = logging.getLogger(f"netbox.jobs.{type(self).__name__}")
63 def run(self, *, rule_id, branch_schema_id, expected_alias):
64 """Run the job on the rule *rule_id* in the branch it was enqueued from; a missing rule is a warning."""
65 run_as_job_user(
66 self.job,
67 functools.partial(self._run_on_rule_id, rule_id, expected_alias),
68 branch_schema_id=branch_schema_id,
69 )
71 def _run_on_rule_id(self, rule_id, expected_alias):
72 from .models import InterfaceNameRule
74 # A branch that is not ready does not activate, so the scope raises before a read on main.
75 with write_scope(expected_alias=expected_alias):
76 rule = InterfaceNameRule.objects.filter(pk=rule_id).first()
77 if rule is None:
78 self.logger.warning("InterfaceNameRule with pk=%s does not exist; skipping.", rule_id)
79 return
80 try:
81 self.run_on_rule(rule)
82 except Exception:
83 self.logger.exception("%s failed on rule '%s'", self.name, rule_id)
84 raise
86 @abstractmethod
87 def run_on_rule(self, rule):
88 """Run the job on *rule* and log what it did."""
91class ApplyRuleJob(RuleJobRunner):
92 """Apply an InterfaceNameRule retroactively to all matching installed modules."""
94 class Meta:
95 name = "Apply Interface Name Rule"
97 def run_on_rule(self, rule):
98 """Apply *rule* to all matching interfaces."""
99 from .engine import apply_rule_to_existing
101 outcome = apply_rule_to_existing(rule)
102 self.logger.info("Renamed %d interface(s) using rule '%s'", outcome.changed_count, rule)
103 if outcome.skipped_members:
104 self.logger.warning("%d interface(s) skipped. The plugin log names each one.", len(outcome.skipped_members))
107class ConvertFlatFamiliesJob(RuleJobRunner):
108 """Convert the flat breakout families a rule's modules still carry to the channelized topology."""
110 class Meta:
111 name = "Convert Flat Interface Families"
113 def run_on_rule(self, rule):
114 """Convert every convertible flat family of *rule*."""
115 from .engine import convert_flat_families
117 outcome = convert_flat_families(rule)
118 self.logger.info("Converted %d interface family(ies) using rule '%s'", len(outcome.changed_families), rule)
119 if outcome.blocked_families:
120 self.logger.warning("%d family(ies) skipped. The plugin log names each one.", len(outcome.blocked_families))