From 73e2dc19867dcbf7894b567b2a81812eab0aade1 Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Mon, 25 Nov 2024 09:18:45 -0800 Subject: [PATCH 01/12] 148 add message to script run form in NetBox --- netbox_branching/template_content.py | 7 +++++++ .../templates/netbox_branching/inc/script_branch.html | 8 ++++++++ 2 files changed, 15 insertions(+) create mode 100644 netbox_branching/templates/netbox_branching/inc/script_branch.html diff --git a/netbox_branching/template_content.py b/netbox_branching/template_content.py index 063a9df..9be3b8d 100644 --- a/netbox_branching/template_content.py +++ b/netbox_branching/template_content.py @@ -1,4 +1,5 @@ from django.contrib.contenttypes.models import ContentType +from extras.models.scripts import Script from netbox.plugins import PluginTemplateExtension from .choices import BranchStatusChoices @@ -34,6 +35,12 @@ class BranchNotification(PluginTemplateExtension): def alerts(self): if not (instance := self.context['object']): return '' + + if type(instance) == Script: + return self.render('netbox_branching/inc/script_branch.html', extra_context={ + 'active_branch': active_branch.get(), + }) + ct = ContentType.objects.get_for_model(instance) relevant_changes = ChangeDiff.objects.filter( object_type=ct, diff --git a/netbox_branching/templates/netbox_branching/inc/script_branch.html b/netbox_branching/templates/netbox_branching/inc/script_branch.html new file mode 100644 index 0000000..c92ff05 --- /dev/null +++ b/netbox_branching/templates/netbox_branching/inc/script_branch.html @@ -0,0 +1,8 @@ +{% load i18n %} +{% if active_branch %} + +{% endif %} From b87679f689e37c81dcc2fd8b61ae0126c65cd084 Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Mon, 2 Dec 2024 12:56:14 -0800 Subject: [PATCH 02/12] 148 Add BranchingBackend to support running scripts --- netbox_branching/backends.py | 8 +++++++ netbox_branching/middleware.py | 43 +++------------------------------- netbox_branching/utilities.py | 40 +++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 40 deletions(-) create mode 100644 netbox_branching/backends.py diff --git a/netbox_branching/backends.py b/netbox_branching/backends.py new file mode 100644 index 0000000..b42ee49 --- /dev/null +++ b/netbox_branching/backends.py @@ -0,0 +1,8 @@ +from .utilities import activate_branch, get_active_branch + +class BranchingBackend: + def activate_branch(self, branch): + return activate_branch(branch) + + def get_active_branch(self, request): + return get_active_branch(request) diff --git a/netbox_branching/middleware.py b/netbox_branching/middleware.py index a79e994..a64954f 100644 --- a/netbox_branching/middleware.py +++ b/netbox_branching/middleware.py @@ -1,14 +1,8 @@ -from django.contrib import messages from django.core.exceptions import ObjectDoesNotExist from django.http import HttpResponseBadRequest -from django.urls import reverse -from utilities.api import is_api_request - -from .choices import BranchStatusChoices -from .constants import COOKIE_NAME, BRANCH_HEADER, QUERY_PARAM -from .models import Branch -from .utilities import activate_branch, is_api_request +from .constants import COOKIE_NAME, QUERY_PARAM +from .utilities import activate_branch, is_api_request, get_active_branch __all__ = ( 'BranchMiddleware', @@ -24,7 +18,7 @@ def __call__(self, request): # Set/clear the active Branch on the request try: - branch = self.get_active_branch(request) + branch = get_active_branch(request) except ObjectDoesNotExist: return HttpResponseBadRequest("Invalid branch identifier") @@ -39,34 +33,3 @@ def __call__(self, request): response.delete_cookie(COOKIE_NAME) return response - - @staticmethod - def get_active_branch(request): - """ - Return the active Branch (if any). - """ - # The active Branch may be specified by HTTP header for REST & GraphQL API requests. - if is_api_request(request) and BRANCH_HEADER in request.headers: - branch = Branch.objects.get(schema_id=request.headers.get(BRANCH_HEADER)) - if not branch.ready: - return HttpResponseBadRequest(f"Branch {branch} is not ready for use (status: {branch.status})") - return branch - - # Branch activated/deactivated by URL query parameter - elif QUERY_PARAM in request.GET: - if schema_id := request.GET.get(QUERY_PARAM): - branch = Branch.objects.get(schema_id=schema_id) - if branch.ready: - messages.success(request, f"Activated branch {branch}") - return branch - else: - messages.error(request, f"Branch {branch} is not ready for use (status: {branch.status})") - return None - else: - messages.success(request, f"Deactivated branch") - request.COOKIES.pop(COOKIE_NAME, None) # Delete cookie if set - return None - - # Branch set by cookie - elif schema_id := request.COOKIES.get(COOKIE_NAME): - return Branch.objects.filter(schema_id=schema_id, status=BranchStatusChoices.READY).first() diff --git a/netbox_branching/utilities.py b/netbox_branching/utilities.py index d7293d0..9e9e247 100644 --- a/netbox_branching/utilities.py +++ b/netbox_branching/utilities.py @@ -4,12 +4,17 @@ from contextlib import contextmanager from dataclasses import dataclass +from django.contrib import messages +from django.core.exceptions import ObjectDoesNotExist from django.db.models import ForeignKey, ManyToManyField +from django.http import HttpResponseBadRequest from django.urls import reverse from netbox.plugins import get_plugin_config from netbox.registry import registry +from .choices import BranchStatusChoices from .constants import EXEMPT_MODELS, INCLUDE_MODELS +from .constants import COOKIE_NAME, BRANCH_HEADER, QUERY_PARAM from .contextvars import active_branch __all__ = ( @@ -209,4 +214,39 @@ def is_api_request(request): """ Returns True if the given request is a REST or GraphQL API request. """ + if not hasattr(request, 'path_info'): + return False + return request.path_info.startswith(reverse('api-root')) or request.path_info.startswith(reverse('graphql')) + + +def get_active_branch(request): + """ + Return the active Branch (if any). + """ + # The active Branch may be specified by HTTP header for REST & GraphQL API requests. + from .models import Branch + if is_api_request(request) and BRANCH_HEADER in request.headers: + branch = Branch.objects.get(schema_id=request.headers.get(BRANCH_HEADER)) + if not branch.ready: + return HttpResponseBadRequest(f"Branch {branch} is not ready for use (status: {branch.status})") + return branch + + # Branch activated/deactivated by URL query parameter + elif QUERY_PARAM in request.GET: + if schema_id := request.GET.get(QUERY_PARAM): + branch = Branch.objects.get(schema_id=schema_id) + if branch.ready: + messages.success(request, f"Activated branch {branch}") + return branch + else: + messages.error(request, f"Branch {branch} is not ready for use (status: {branch.status})") + return None + else: + messages.success(request, f"Deactivated branch") + request.COOKIES.pop(COOKIE_NAME, None) # Delete cookie if set + return None + + # Branch set by cookie + elif schema_id := request.COOKIES.get(COOKIE_NAME): + return Branch.objects.filter(schema_id=schema_id, status=BranchStatusChoices.READY).first() From 66bed1ea0492be7ca7c047e1ca9f6f708fda7fdd Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Mon, 2 Dec 2024 13:23:37 -0800 Subject: [PATCH 03/12] 148 Add BranchingBackend to support running scripts --- netbox_branching/backends.py | 1 + 1 file changed, 1 insertion(+) diff --git a/netbox_branching/backends.py b/netbox_branching/backends.py index b42ee49..5f2bba5 100644 --- a/netbox_branching/backends.py +++ b/netbox_branching/backends.py @@ -1,5 +1,6 @@ from .utilities import activate_branch, get_active_branch + class BranchingBackend: def activate_branch(self, branch): return activate_branch(branch) From 0ffbd049af0f768d99472321c678db327250c0b0 Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Mon, 2 Dec 2024 13:26:02 -0800 Subject: [PATCH 04/12] 148 Add BranchingBackend to support running scripts --- README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5196850..5ade8d3 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,13 @@ PLUGINS = [ ] ``` -5. Create `local_settings.py` (in the same directory as `settings.py`) to override the `DATABASES` & `DATABASE_ROUTERS` settings. This enables dynamic schema support. +5. Add `BRANCHING_BACKEND` to `configuration.py`. + +```python +BRANCHING_BACKEND = 'netbox_branching.backends.BranchingBackend' +``` + +6. Create `local_settings.py` (in the same directory as `settings.py`) to override the `DATABASES` & `DATABASE_ROUTERS` settings. This enables dynamic schema support. ```python from netbox_branching.utilities import DynamicSchemaDict @@ -55,7 +61,7 @@ DATABASE_ROUTERS = [ ] ``` -6. Run NetBox migrations: +7. Run NetBox migrations: ``` $ ./manage.py migrate From a18f0634bade9068f26798afcc0a943d637599a2 Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Mon, 2 Dec 2024 13:32:10 -0800 Subject: [PATCH 05/12] 148 Fix instance check --- netbox_branching/template_content.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox_branching/template_content.py b/netbox_branching/template_content.py index 9be3b8d..5d5655b 100644 --- a/netbox_branching/template_content.py +++ b/netbox_branching/template_content.py @@ -36,7 +36,7 @@ def alerts(self): if not (instance := self.context['object']): return '' - if type(instance) == Script: + if isinstance(instance, Script): return self.render('netbox_branching/inc/script_branch.html', extra_context={ 'active_branch': active_branch.get(), }) From 596ed8d07effe08f1a30db385f417b186ab850c4 Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Mon, 9 Dec 2024 09:34:32 -0800 Subject: [PATCH 06/12] 148 change to use script context managers --- README.md | 4 ++-- netbox_branching/backends.py | 9 --------- netbox_branching/signal_receivers.py | 13 ++++++++++++- 3 files changed, 14 insertions(+), 12 deletions(-) delete mode 100644 netbox_branching/backends.py diff --git a/README.md b/README.md index 5ade8d3..830ed9f 100644 --- a/README.md +++ b/README.md @@ -38,10 +38,10 @@ PLUGINS = [ ] ``` -5. Add `BRANCHING_BACKEND` to `configuration.py`. +5. Add `SCRIPT_CONTEXT_MANAGERS` to `configuration.py`. ```python -BRANCHING_BACKEND = 'netbox_branching.backends.BranchingBackend' +SCRIPT_CONTEXT_MANAGERS = ['netbox_branching.script_context_manager.NetBoxScriptContextManager',] ``` 6. Create `local_settings.py` (in the same directory as `settings.py`) to override the `DATABASES` & `DATABASE_ROUTERS` settings. This enables dynamic schema support. diff --git a/netbox_branching/backends.py b/netbox_branching/backends.py deleted file mode 100644 index 5f2bba5..0000000 --- a/netbox_branching/backends.py +++ /dev/null @@ -1,9 +0,0 @@ -from .utilities import activate_branch, get_active_branch - - -class BranchingBackend: - def activate_branch(self, branch): - return activate_branch(branch) - - def get_active_branch(self, request): - return get_active_branch(request) diff --git a/netbox_branching/signal_receivers.py b/netbox_branching/signal_receivers.py index 026e6a6..71e5360 100644 --- a/netbox_branching/signal_receivers.py +++ b/netbox_branching/signal_receivers.py @@ -38,6 +38,18 @@ def record_change_diff(instance, **kwargs): object_type = instance.changed_object_type object_id = instance.changed_object_id + qs = ChangeDiff.objects.all() + from django.contrib.contenttypes.models import ContentType + from dcim.models import Site + + if qs: + for diff in qs: + print(f"{diff.id}: {diff.object_type} - {diff.object_id} - {diff.object} - {diff.object_repr}") + ct = ContentType.objects.get(app_label=diff.object_type.app_label, model=diff.object_type.model) + print(f"ct: {ct}") + site = Site.objects.get(id=diff.object_id) + print(f"site: {site}") + # If this type of object does not support branching, return immediately. if object_type.model not in registry['model_features']['branching'].get(object_type.app_label, []): return @@ -61,7 +73,6 @@ def record_change_diff(instance, **kwargs): # If this is a branch-aware change, create or update ChangeDiff for this object. else: - # Updating the existing ChangeDiff if diff := ChangeDiff.objects.filter(object_type=object_type, object_id=object_id, branch=branch).first(): logger.debug(f"Updating branch change diff for change to {instance.changed_object}") From 12f3be020ec95a45903e19aaddff97803eb9097c Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Mon, 9 Dec 2024 09:50:42 -0800 Subject: [PATCH 07/12] 148 remove incorrect checkin --- netbox_branching/signal_receivers.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/netbox_branching/signal_receivers.py b/netbox_branching/signal_receivers.py index 71e5360..026e6a6 100644 --- a/netbox_branching/signal_receivers.py +++ b/netbox_branching/signal_receivers.py @@ -38,18 +38,6 @@ def record_change_diff(instance, **kwargs): object_type = instance.changed_object_type object_id = instance.changed_object_id - qs = ChangeDiff.objects.all() - from django.contrib.contenttypes.models import ContentType - from dcim.models import Site - - if qs: - for diff in qs: - print(f"{diff.id}: {diff.object_type} - {diff.object_id} - {diff.object} - {diff.object_repr}") - ct = ContentType.objects.get(app_label=diff.object_type.app_label, model=diff.object_type.model) - print(f"ct: {ct}") - site = Site.objects.get(id=diff.object_id) - print(f"site: {site}") - # If this type of object does not support branching, return immediately. if object_type.model not in registry['model_features']['branching'].get(object_type.app_label, []): return @@ -73,6 +61,7 @@ def record_change_diff(instance, **kwargs): # If this is a branch-aware change, create or update ChangeDiff for this object. else: + # Updating the existing ChangeDiff if diff := ChangeDiff.objects.filter(object_type=object_type, object_id=object_id, branch=branch).first(): logger.debug(f"Updating branch change diff for change to {instance.changed_object}") From 84c56b38e6e7ea770e7a2653aefc389254895c88 Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Mon, 9 Dec 2024 09:56:54 -0800 Subject: [PATCH 08/12] 148 add missing file --- netbox_branching/script_context_manager.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 netbox_branching/script_context_manager.py diff --git a/netbox_branching/script_context_manager.py b/netbox_branching/script_context_manager.py new file mode 100644 index 0000000..5b938af --- /dev/null +++ b/netbox_branching/script_context_manager.py @@ -0,0 +1,10 @@ +from contextlib import nullcontext +from .utilities import activate_branch, get_active_branch + + +def NetBoxScriptContextManager(request): + branch = get_active_branch(request) + if branch: + return activate_branch(branch) + else: + return nullcontext() From b7697d911fade2c2c215d272655d3f801cae964d Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Mon, 9 Dec 2024 09:58:21 -0800 Subject: [PATCH 09/12] 148 cleanup --- netbox_branching/script_context_manager.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/netbox_branching/script_context_manager.py b/netbox_branching/script_context_manager.py index 5b938af..1833872 100644 --- a/netbox_branching/script_context_manager.py +++ b/netbox_branching/script_context_manager.py @@ -3,8 +3,7 @@ def NetBoxScriptContextManager(request): - branch = get_active_branch(request) - if branch: + if branch := get_active_branch(request) return activate_branch(branch) else: return nullcontext() From 2184b9e1bac169773c18596b9812e62fbaa0fe96 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 12 Dec 2024 14:59:20 -0500 Subject: [PATCH 10/12] Adapt for dynamic registration of request processors in NetBox --- README.md | 10 ++-------- netbox_branching/script_context_manager.py | 9 --------- .../netbox_branching/inc/script_branch.html | 2 +- netbox_branching/utilities.py | 16 ++++++++++++---- 4 files changed, 15 insertions(+), 22 deletions(-) delete mode 100644 netbox_branching/script_context_manager.py diff --git a/README.md b/README.md index 830ed9f..5196850 100644 --- a/README.md +++ b/README.md @@ -38,13 +38,7 @@ PLUGINS = [ ] ``` -5. Add `SCRIPT_CONTEXT_MANAGERS` to `configuration.py`. - -```python -SCRIPT_CONTEXT_MANAGERS = ['netbox_branching.script_context_manager.NetBoxScriptContextManager',] -``` - -6. Create `local_settings.py` (in the same directory as `settings.py`) to override the `DATABASES` & `DATABASE_ROUTERS` settings. This enables dynamic schema support. +5. Create `local_settings.py` (in the same directory as `settings.py`) to override the `DATABASES` & `DATABASE_ROUTERS` settings. This enables dynamic schema support. ```python from netbox_branching.utilities import DynamicSchemaDict @@ -61,7 +55,7 @@ DATABASE_ROUTERS = [ ] ``` -7. Run NetBox migrations: +6. Run NetBox migrations: ``` $ ./manage.py migrate diff --git a/netbox_branching/script_context_manager.py b/netbox_branching/script_context_manager.py deleted file mode 100644 index 1833872..0000000 --- a/netbox_branching/script_context_manager.py +++ /dev/null @@ -1,9 +0,0 @@ -from contextlib import nullcontext -from .utilities import activate_branch, get_active_branch - - -def NetBoxScriptContextManager(request): - if branch := get_active_branch(request) - return activate_branch(branch) - else: - return nullcontext() diff --git a/netbox_branching/templates/netbox_branching/inc/script_branch.html b/netbox_branching/templates/netbox_branching/inc/script_branch.html index c92ff05..002e3aa 100644 --- a/netbox_branching/templates/netbox_branching/inc/script_branch.html +++ b/netbox_branching/templates/netbox_branching/inc/script_branch.html @@ -2,7 +2,7 @@ {% if active_branch %} {% endif %} diff --git a/netbox_branching/utilities.py b/netbox_branching/utilities.py index 9e9e247..ada126c 100644 --- a/netbox_branching/utilities.py +++ b/netbox_branching/utilities.py @@ -1,28 +1,29 @@ import datetime import logging from collections import defaultdict -from contextlib import contextmanager +from contextlib import contextmanager, nullcontext from dataclasses import dataclass from django.contrib import messages -from django.core.exceptions import ObjectDoesNotExist from django.db.models import ForeignKey, ManyToManyField from django.http import HttpResponseBadRequest from django.urls import reverse from netbox.plugins import get_plugin_config from netbox.registry import registry +from netbox.utils import register_request_processor from .choices import BranchStatusChoices -from .constants import EXEMPT_MODELS, INCLUDE_MODELS -from .constants import COOKIE_NAME, BRANCH_HEADER, QUERY_PARAM +from .constants import BRANCH_HEADER, COOKIE_NAME, EXEMPT_MODELS, INCLUDE_MODELS, QUERY_PARAM from .contextvars import active_branch __all__ = ( 'ChangeSummary', 'DynamicSchemaDict', 'ListHandler', + 'ActiveBranchContextManager', 'activate_branch', 'deactivate_branch', + 'get_active_branch', 'get_branchable_object_types', 'get_tables_to_replicate', 'is_api_request', @@ -250,3 +251,10 @@ def get_active_branch(request): # Branch set by cookie elif schema_id := request.COOKIES.get(COOKIE_NAME): return Branch.objects.filter(schema_id=schema_id, status=BranchStatusChoices.READY).first() + + +@register_request_processor +def ActiveBranchContextManager(request): + if branch := get_active_branch(request): + return activate_branch(branch) + return nullcontext() From 4cab1d85ec28e4aa315b3880e3133ca5b37bd1a2 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 16 Dec 2024 13:41:47 -0500 Subject: [PATCH 11/12] Remove activate_branch() from middleware to avoid duplicate call --- netbox_branching/middleware.py | 3 +-- netbox_branching/utilities.py | 3 +++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/netbox_branching/middleware.py b/netbox_branching/middleware.py index a64954f..c339365 100644 --- a/netbox_branching/middleware.py +++ b/netbox_branching/middleware.py @@ -22,8 +22,7 @@ def __call__(self, request): except ObjectDoesNotExist: return HttpResponseBadRequest("Invalid branch identifier") - with activate_branch(branch): - response = self.get_response(request) + response = self.get_response(request) # Set/clear the branch cookie (for non-API requests) if not is_api_request(request): diff --git a/netbox_branching/utilities.py b/netbox_branching/utilities.py index ada126c..56fbc6e 100644 --- a/netbox_branching/utilities.py +++ b/netbox_branching/utilities.py @@ -255,6 +255,9 @@ def get_active_branch(request): @register_request_processor def ActiveBranchContextManager(request): + """ + Activate a branch if indicated by the request. + """ if branch := get_active_branch(request): return activate_branch(branch) return nullcontext() From c7d7fac9977e0527fcf13e032b1b494af2075db5 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 16 Dec 2024 13:54:16 -0500 Subject: [PATCH 12/12] Require NetBox >= v4.1.9 --- netbox_branching/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox_branching/__init__.py b/netbox_branching/__init__.py index 7ba11ca..32d1c2b 100644 --- a/netbox_branching/__init__.py +++ b/netbox_branching/__init__.py @@ -11,7 +11,7 @@ class AppConfig(PluginConfig): description = 'A git-like branching implementation for NetBox' version = '0.5.2' base_url = 'branching' - min_version = '4.1' + min_version = '4.1.9' middleware = [ 'netbox_branching.middleware.BranchMiddleware' ]