Skip to content

Release v0.5.3 #212

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

Merged
merged 8 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions .github/ISSUE_TEMPLATE/01-feature_request.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: ✨ Feature Request
description: Propose a new NetBox feature or enhancement
description: Propose a new feature or enhancement
labels: ["type: feature"]
body:
- type: input
Expand All @@ -13,21 +13,21 @@ body:
attributes:
label: Proposed functionality
description: >
Describe in detail the new feature or behavior you are proposing. Include any specific changes
to work flows, data models, and/or the user interface. The more detail you provide here, the
greater chance your proposal has of being discussed.
Describe in detail the new feature or behavior you are proposing. Include any specific changes to work flows,
data models, and/or the user interface. The more detail you provide here, the greater chance your proposal has
of being discussed.
validations:
required: true
- type: textarea
attributes:
label: Use case
description: >
Explain how adding this functionality would benefit users. What need does it address?
Explain how adding this functionality would benefit users. What specific need(s) does it address?
validations:
required: true
- type: textarea
attributes:
label: External dependencies
description: >
List any new dependencies on external libraries or services that this new feature would
introduce. For example, does the proposal require the installation of a new Python package?
List any new dependencies on external libraries or services that this new feature would introduce. For example,
does the proposal require the installation of a new Python package?
7 changes: 3 additions & 4 deletions .github/ISSUE_TEMPLATE/02-bug_report.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,8 @@ body:
attributes:
label: Steps to Reproduce
description: >
Describe in detail the exact steps that someone else can take to reproduce
this bug. A numbered list of discrete steps is strongly preferred. Remember
to capture the creation of any objects which must exist to reproduce the
Describe in detail the exact steps that someone else can take to reproduce this bug. A numbered list of discrete
steps is strongly preferred. Remember to capture the creation of any objects which must exist to reproduce the
behavior.
placeholder: |
1. Click on "create widget"
Expand All @@ -45,7 +44,7 @@ body:
- type: textarea
attributes:
label: Observed Behavior
description: What happened instead?
description: What happened instead? Be sure to include any error messages.
placeholder: A TypeError exception was raised
validations:
required: true
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/03-documentation_change.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ body:
- type: textarea
attributes:
label: Proposed Changes
description: Describe the proposed changes and why they are necessary.
description: Describe the proposed changes and explain why they are necessary.
validations:
required: true
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/04-housekeeping.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: 🏡 Housekeeping
description: A change pertaining to the codebase itself (developers only)
description: An internal change pertaining to the codebase itself
labels: ["type: housekeeping"]
body:
- type: textarea
Expand Down
14 changes: 14 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Change Log

## v0.5.3

### Enhancements

* [#209](https://github.com/netboxlabs/netbox-branching/issues/209) - Prevent merging branches whose `last_sync` time exceeds the configured changelog retention window

### Bug Fixes

* [#87](https://github.com/netboxlabs/netbox-branching/issues/87) - Deactivate the active branch (if any) when creating a new branch
* [#148](https://github.com/netboxlabs/netbox-branching/issues/148) - Fix `IntegrityError` exception raised when executing custom scripts within a branch
* [#178](https://github.com/netboxlabs/netbox-branching/issues/178) - Fix display of assigned tags in the branches list

---

## v0.5.2

### Bug Fixes
Expand Down
3 changes: 3 additions & 0 deletions docs/using-branches/syncing-merging.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ Synchronizing a branch replicates all recent changes from main into the branch.

To synchronize a branch, click the "Sync" button. (If this button is not visible, verify that the branch status shows "ready" and that you have permission to synchronize the branch.)

!!! warning
A branch must be synchronized frequently enough to avoid exceeding NetBox's configured [changelog retention period](https://netboxlabs.com/docs/netbox/en/stable/configuration/miscellaneous/#changelog_retention) (which defaults to 90 days). This is to protect against data loss when replicating changes from main. A branch whose `last_sync` time exceeds the configured retention window can no longer be synced.

While a branch is being synchronized, its status will show "synchronizing."

!!! tip
Expand Down
4 changes: 2 additions & 2 deletions netbox_branching/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ class AppConfig(PluginConfig):
name = 'netbox_branching'
verbose_name = 'NetBox Branching'
description = 'A git-like branching implementation for NetBox'
version = '0.5.2'
version = '0.5.3'
base_url = 'branching'
min_version = '4.1'
min_version = '4.1.9'
middleware = [
'netbox_branching.middleware.BranchMiddleware'
]
Expand Down
46 changes: 4 additions & 42 deletions netbox_branching/middleware.py
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -24,12 +18,11 @@ 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")

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):
Expand All @@ -39,34 +32,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()
23 changes: 17 additions & 6 deletions netbox_branching/models/branches.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging
import random
import string
from datetime import timedelta
from functools import cached_property, partial

from django.conf import settings
Expand All @@ -15,6 +16,7 @@
from django.utils.translation import gettext_lazy as _

from core.models import ObjectChange as ObjectChange_
from netbox.config import get_config
from netbox.context import current_request
from netbox.context_managers import event_tracking
from netbox.models import PrimaryModel
Expand Down Expand Up @@ -121,7 +123,7 @@ def schema_name(self):
def connection_name(self):
return f'schema_{self.schema_name}'

@cached_property
@property
def synced_time(self):
return self.last_sync or self.created

Expand Down Expand Up @@ -159,9 +161,6 @@ def save(self, provision=True, *args, **kwargs):

_provision = provision and self.pk is None

if active_branch.get():
raise AbortRequest(_("Cannot create or modify a branch while a branch is active."))

super().save(*args, **kwargs)

if _provision:
Expand All @@ -173,8 +172,8 @@ def save(self, provision=True, *args, **kwargs):
)

def delete(self, *args, **kwargs):
if active_branch.get():
raise AbortRequest(_("Cannot delete a branch while a branch is active."))
if active_branch.get() == self:
raise AbortRequest(_("The active branch cannot be deleted."))

# Deprovision the schema
self.deprovision()
Expand Down Expand Up @@ -243,6 +242,16 @@ def get_event_history(self):
last_time = event.time
return history

@property
def is_stale(self):
"""
Indicates whether the branch is too far out of date to be synced.
"""
if not (changelog_retention := get_config().CHANGELOG_RETENTION):
# Changelog retention is disabled
return False
return self.synced_time < timezone.now() - timedelta(days=changelog_retention)

def sync(self, user, commit=True):
"""
Apply changes from the main schema onto the Branch's schema.
Expand All @@ -252,6 +261,8 @@ def sync(self, user, commit=True):

if not self.ready:
raise Exception(f"Branch {self} is not ready to sync")
if self.is_stale:
raise Exception(f"Branch {self} is stale and can no longer be synced")

# Emit pre-sync signal
pre_sync.send(sender=self.__class__, branch=self, user=user)
Expand Down
2 changes: 1 addition & 1 deletion netbox_branching/navigation.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
link_text=_('Branches'),
buttons=(
PluginMenuButton('plugins:netbox_branching:branch_add', _('Add'), 'mdi mdi-plus-thick'),
PluginMenuButton('plugins:netbox_branching:branch_import', _('Import'), 'mdi mdi-upload'),
PluginMenuButton('plugins:netbox_branching:branch_bulk_import', _('Import'), 'mdi mdi-upload'),
)
),
PluginMenuItem(
Expand Down
19 changes: 15 additions & 4 deletions netbox_branching/tables/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,24 +56,35 @@ class BranchTable(NetBoxTable):
verbose_name=_('Name'),
linkify=True
)
is_active = columns.BooleanColumn(
verbose_name=_('Active')
)
status = columns.ChoiceFieldColumn(
verbose_name=_('Status'),
verbose_name=_('Status')
)
is_stale = columns.BooleanColumn(
true_mark=mark_safe('<span class="text-danger"><i class="mdi mdi-alert-circle"></i></span>'),
false_mark=None,
verbose_name=_('Stale')
)
conflicts = ConflictsColumn(
verbose_name=_('Conflicts')
)
schema_id = tables.TemplateColumn(
template_code='<span class="font-monospace">{{ value }}</code>'
)
tags = columns.TagColumn(
url_name='plugins:netbox_branching:branch_list'
)

class Meta(NetBoxTable.Meta):
model = Branch
fields = (
'pk', 'id', 'name', 'is_active', 'status', 'conflicts', 'schema_id', 'description', 'owner', 'tags',
'created', 'last_updated',
'pk', 'id', 'name', 'is_active', 'status', 'is_stale', 'conflicts', 'schema_id', 'description', 'owner',
'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'is_active', 'status', 'owner', 'conflicts', 'schema_id', 'description',
'pk', 'name', 'is_active', 'status', 'is_stale', 'owner', 'conflicts', 'schema_id', 'description',
)

def render_is_active(self, value):
Expand Down
21 changes: 19 additions & 2 deletions netbox_branching/template_content.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
from django.contrib.contenttypes.models import ContentType
from netbox.plugins import PluginTemplateExtension

from netbox.plugins import PluginTemplateExtension
from .choices import BranchStatusChoices
from .contextvars import active_branch
from .models import Branch, ChangeDiff

__all__ = (
'BranchNotification',
'BranchSelector',
'ScriptNotification',
'ShareButton',
'template_extensions',
)

Expand All @@ -34,6 +36,7 @@ class BranchNotification(PluginTemplateExtension):
def alerts(self):
if not (instance := self.context['object']):
return ''

ct = ContentType.objects.get_for_model(instance)
relevant_changes = ChangeDiff.objects.filter(
object_type=ct,
Expand All @@ -51,4 +54,18 @@ def alerts(self):
})


template_extensions = [BranchSelector, ShareButton, BranchNotification]
class ScriptNotification(PluginTemplateExtension):
models = ['extras.script']

def alerts(self):
return self.render('netbox_branching/inc/script_alert.html', extra_context={
'active_branch': active_branch.get(),
})


template_extensions = (
BranchSelector,
BranchNotification,
ScriptNotification,
ShareButton,
)
17 changes: 15 additions & 2 deletions netbox_branching/templates/netbox_branching/branch.html
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,24 @@ <h5 class="card-header">{% trans "Branch" %}</h5>
</tr>
<tr>
<th scope="row">{% trans "Last synced" %}</th>
<td>{{ object.synced_time|isodatetime }}</td>
<td>
{{ object.synced_time|isodatetime }}
{% if object.is_stale %}
<span class="text-danger" title="{% trans "Branch is stale and can no longer be synced" %}">
<i class="mdi mdi-alert-circle"></i>
</span>
{% endif %}
<div class="small text-muted">{{ object.synced_time|timesince }} {% trans "ago" %}</div>
</td>
</tr>
<tr>
<th scope="row">{% trans "Last activity" %}</th>
<td>{{ latest_change.time|isodatetime|placeholder }}</td>
<td>
{{ latest_change.time|isodatetime|placeholder }}
{% if latest_change %}
<div class="small text-muted">{{ latest_change.time|timesince }} {% trans "ago" %}</div>
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans "Conflicts" %}</th>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{% load i18n %}
{% if perms.netbox_branching.sync_branch %}
{% if perms.netbox_branching.sync_branch and not branch.is_stale %}
<a href="{% url 'plugins:netbox_branching:branch_sync' pk=branch.pk %}" class="btn btn-primary">
<i class="mdi mdi-sync"></i> {% trans "Sync" %}
</a>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{% load i18n %}
{% if active_branch %}
<div class="alert alert-warning" role="alert">
<i class="mdi mdi-alert"></i>
{% trans "This script will be run in branch" %}
<a href="{{ active_branch.get_absolute_url }}">{{ active_branch.name }}</a>
</div>
{% endif %}
Loading