Skip to content

fix: handle static DynamicGroups in job filter and repo sync - #1116

Draft
xtinalang wants to merge 9 commits into
nautobot:developfrom
xtinalang:xtinalang-fix-1029-static-group-job
Draft

fix: handle static DynamicGroups in job filter and repo sync#1116
xtinalang wants to merge 9 commits into
nautobot:developfrom
xtinalang:xtinalang-fix-1029-static-group-job

Conversation

@xtinalang

Copy link
Copy Markdown

Why:

  • Nautobot supports two DynamicGroup types: dynamic filter (query-based)
    and static (manually assigned members)
  • generate_query() raises RuntimeError on static groups because they
    have no filter expression to evaluate
  • This caused ALL Golden Config jobs to crash when any GoldenConfigSetting
    was linked to a static DynamicGroup

What changed:

  • jobs.py (get_refreshed_repos): replaced generate_query() with
    group.members.values_list("pk", flat=True) to safely check device
    membership for both group types
  • helper.py (get_job_filter): same fix applied where the base device
    queryset is built from all GoldenConfigSettings
  • Added regression tests for both code paths

Fixes #1029

Closes: #

What was changed

  • jobs.py (get_refreshed_repos): replaced generate_query() with
    group.members.values_list("pk", flat=True) — works correctly for both
    static and dynamic group types
  • helper.py (get_job_filter): same fix applied where the base device
    queryset is built across all GoldenConfigSettings

To Do

Screenshot 2026-05-06 at 10 21 48 AM Screenshot 2026-05-06 at 11 46 45 AM
  • Explanation of Change(s)
  • Added change log fragment(s)
  • Attached Screenshots, Payload Example
  • Unit, Integration Tests
  • Documentation Updates (when adding/changing features)
  • Outline Remaining Work, Constraints from Design

…ilter

Calling generate_query() on a static DynamicGroup raises a RuntimeError
because static groups have no filter expression to generate. This fix
replaces generate_query() with group.members.values_list("pk", flat=True)
in both get_refreshed_repos (jobs.py) and get_job_filter (helper.py),
which works correctly for both static and dynamic group types.

Fixes nautobot#1029

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions

github-actions Bot commented May 6, 2026

Copy link
Copy Markdown

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  nautobot_golden_config
  jobs.py
  nautobot_golden_config/utilities
  helper.py
Project Total  

This report was generated by python-coverage-comment-action

@itdependsnetworks

Copy link
Copy Markdown
Contributor

I see your point... but perhaps this should be fixed in core or core may already have a method that handles this already, @glennmatthews any thoughts?

@glennmatthews

Copy link
Copy Markdown
Contributor

Checking against .members instead of .generate_query seems correct in general to me, as .members already returns a queryset; I'm not familiar enough with the two specific uses here to understand exactly what they're trying to do, and be able to say whether they could be implemented more efficiently.

Christina Lang and others added 2 commits May 7, 2026 08:59
…ic DynamicGroups

The previous implementation used a Subquery filtering on static_group_associations
which only worked for static DynamicGroups and broke for filter-based groups.
Replaces with GoldenConfigSettingManager.get_for_device which handles both group
types via device.dynamic_groups and correctly applies weight ordering.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…icGroup

Adds two tests to HelpersTestStaticGroup verifying that
get_device_to_settings_map correctly resolves a device's GoldenConfigSetting
when the setting uses a static DynamicGroup, and that weight ordering is
respected when a device matches both static and filter-based groups.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@xtinalang

Copy link
Copy Markdown
Author

I initially marked this as a draft because I wasn't confident the implementation was complete or correct — I wanted to validate the approach before requesting a full review. After testing and getting feedback, I've updated the implementation to avoid .members entirely. Instead of looping through GC settings and checking .members, I'm now using the existing GoldenConfigSettingManager.get_for_device() method which, as I understand it, traverses device.dynamic_groups at the ORM level — a single DB query per device regardless of how many GC settings exist. I'm hoping this handles both static and filter-based groups correctly and more efficiently. Happy to take a different approach if needed.

Christina Lang and others added 3 commits May 7, 2026 09:24
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…Groups

Static DynamicGroups raise RuntimeError when .members is accessed because
.members internally calls generate_query(), which is not implemented for
static groups. Branch on group_type and query static_group_associations
directly for static groups; keep .members for filter-based groups.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nches

The previous fix added an if/else inside get_job_filter, pushing the
function from 12 to 14 branches and tripping pylint's R0912
too-many-branches check in CI. Extract the static-vs-filter dispatch
into a small helper so get_job_filter stays at the original branch
count and the dispatch logic is testable in isolation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
gcs = {gc.id: gc for gc in models.GoldenConfigSetting.objects.all()}
return {device.id: gcs[device.gc_settings] for device in annotated_queryset}
device_to_settings = {}
for device in queryset.all():

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This would essentially undo #941

@itdependsnetworks

Copy link
Copy Markdown
Contributor

Checking against .members instead of .generate_query seems correct in general to me, as .members already returns a queryset; I'm not familiar enough with the two specific uses here to understand exactly what they're trying to do, and be able to say whether they could be implemented more efficiently.

@glennmatthews the crux of the question is should the API for the DG be different if it is using static? Specfically, in regards to generate_query. It would seem likely that we can just have the static method respect generate_query and return a queryset, and the rest of the workflow would be fine.

@glennmatthews

Copy link
Copy Markdown
Contributor

Maybe I'm missing something but it seems to me at a glance that members and generate_query are if anything a bit redundant with one another.

Christina Lang and others added 3 commits May 8, 2026 08:59
…pe branching

DynamicGroup.members already handles both static and dynamic-filter groups
uniformly via static_group_associations as the underlying lookup, so no
app-side branching is needed. Removes the _dynamic_group_device_pks
helper introduced in the previous commits — the call site reduces to a
single .members.values_list(...) line.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Identical setup, identical assertion to test_device_to_settings_map_static_group_weight_wins —
both verified that the static-group setting maps to the device. Keeping the surviving variant
since one regression test for the static-group resolution path is enough.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…equires

Drop the dynamic-filter group and dynamic_filter_setting from setUp — the
bug is purely about static groups, and the mixed-mode coverage isn't
requested by the issue. Mixed-mode resolution is already exercised by the
integration test in test_jobs.py.

Rename test_device_to_settings_map_static_group_weight_wins to
test_device_to_settings_map_with_static_group since it no longer asserts
weight precedence (only the static-group resolution path).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@xtinalang

Copy link
Copy Markdown
Author

Thank you @glennmatthews and @itdependsnetworks. I am grateful for the time you both took to look at this and for your knowledge of Nautobot. You pointed me in the right direction, and I missed the real intent. You were right, and I was wrong. I am humbled by this experience.

Going back to look at DynamicGroup in Core more carefully, .members does work for both static and dynamic-filter groups. The implementation is the same in 3.0, 3.1, and develop. The RuntimeError was coming from a direct call to obj.dynamic_group.generate_query(), not .members.

I pushed a better refactor (834fdfd), and the helper is gone. Hopefully this is right. The helper is gone, and get_job_filter reduces to a one-liner using obj.dynamic_group.members.values_list("pk", flat=True). The app should no longer have its own logic for static vs dynamic filter groups. Like you both were saying, the app does not need to handle the static and dynamic-filter groups differently. If there is a better way, please let me know.

Again, I am humbled -- big lesson learned. It is great having you guys here to work with as I learn the ropes. Note to self: When I am working on a plugin like Golden Config, I need to read the core source for the APIs I am using, not just react to the symptom in the app. Thank you for walking me through this.

This is still a work in progress and I am still open to suggestions or if I am going in the wrong direction.

@itdependsnetworks

Copy link
Copy Markdown
Contributor

In general we can likely close this out, depending on how urgent this is (see work around below for more details.) We can/should update generate_query() to be part of the static group assignment in core. It does not seem like the issue with settings is actually a challenge, but at a min, should not be looped in the way it was being suggested.

If it is urgent, this should work in the two places that it would be relevant.

        for obj in models.GoldenConfigSetting.objects.all():
            if dynamic_group__group_type=DynamicGroupTypeChoices.TYPE_STATIC:
                raw_qs = raw_qs | Q(pk__in=obj.dynamic_group.members.values_list("pk", flat=True))
            else:
                raw_qs = raw_qs | obj.dynamic_group.generate_query()

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.

A device being assigned to a static group causes jobs to fail

3 participants