mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-10 05:42:16 -06:00
Compare commits
25 Commits
v4.5-beta1
...
cf16a29ad3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf16a29ad3 | ||
|
|
544c97d923 | ||
|
|
77ee6baa23 | ||
|
|
09d1049267 | ||
|
|
da1e0f4b53 | ||
|
|
7f39f75d3d | ||
|
|
922b08c0ff | ||
|
|
84864fa5e1 | ||
|
|
767dfccd8f | ||
|
|
93e5f919ba | ||
|
|
dc4bab7477 | ||
|
|
929d024003 | ||
|
|
e4b614038e | ||
|
|
3016b1d90b | ||
|
|
57b47dc1ea | ||
|
|
60aa952eb1 | ||
|
|
da4c669312 | ||
|
|
71f707b7ac | ||
|
|
e11508dd6c | ||
|
|
5b5b5c8909 | ||
|
|
a49869af42 | ||
|
|
2e0ff04f84 | ||
|
|
bfeba36514 | ||
|
|
111aca115b | ||
|
|
b4160ad59b |
@@ -12,7 +12,7 @@ Depending on its classification, each NetBox model may support various features
|
|||||||
|
|
||||||
| Feature | Feature Mixin | Registry Key | Description |
|
| Feature | Feature Mixin | Registry Key | Description |
|
||||||
|------------------------------------------------------------|-------------------------|---------------------|-----------------------------------------------------------------------------------------|
|
|------------------------------------------------------------|-------------------------|---------------------|-----------------------------------------------------------------------------------------|
|
||||||
| [Bookmarks](../features/customization.md#bookmarks) | `BookmarksMixin` | `bookmarks` | These models can be bookmarked natively in the user interface |
|
| [Bookmarks](../features/user-preferences.md#bookmarks) | `BookmarksMixin` | `bookmarks` | These models can be bookmarked natively in the user interface |
|
||||||
| [Change logging](../features/change-logging.md) | `ChangeLoggingMixin` | `change_logging` | Changes to these objects are automatically recorded in the change log |
|
| [Change logging](../features/change-logging.md) | `ChangeLoggingMixin` | `change_logging` | Changes to these objects are automatically recorded in the change log |
|
||||||
| Cloning | `CloningMixin` | `cloning` | Provides the `clone()` method to prepare a copy |
|
| Cloning | `CloningMixin` | `cloning` | Provides the `clone()` method to prepare a copy |
|
||||||
| [Contacts](../features/contacts.md) | `ContactsMixin` | `contacts` | Contacts can be associated with these models |
|
| [Contacts](../features/contacts.md) | `ContactsMixin` | `contacts` | Contacts can be associated with these models |
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ class DataSourceStatusChoices(ChoiceSet):
|
|||||||
SYNCING = 'syncing'
|
SYNCING = 'syncing'
|
||||||
COMPLETED = 'completed'
|
COMPLETED = 'completed'
|
||||||
FAILED = 'failed'
|
FAILED = 'failed'
|
||||||
|
READY = 'ready'
|
||||||
|
|
||||||
CHOICES = (
|
CHOICES = (
|
||||||
(NEW, _('New'), 'blue'),
|
(NEW, _('New'), 'blue'),
|
||||||
@@ -20,6 +21,7 @@ class DataSourceStatusChoices(ChoiceSet):
|
|||||||
(SYNCING, _('Syncing'), 'cyan'),
|
(SYNCING, _('Syncing'), 'cyan'),
|
||||||
(COMPLETED, _('Completed'), 'green'),
|
(COMPLETED, _('Completed'), 'green'),
|
||||||
(FAILED, _('Failed'), 'red'),
|
(FAILED, _('Failed'), 'red'),
|
||||||
|
(READY, _('Ready'), 'green'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from utilities.forms import get_field_value
|
|||||||
from utilities.forms.fields import CommentField, JSONField
|
from utilities.forms.fields import CommentField, JSONField
|
||||||
from utilities.forms.rendering import FieldSet
|
from utilities.forms.rendering import FieldSet
|
||||||
from utilities.forms.widgets import HTMXSelect
|
from utilities.forms.widgets import HTMXSelect
|
||||||
|
from core.choices import DataSourceStatusChoices
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ConfigRevisionForm',
|
'ConfigRevisionForm',
|
||||||
@@ -79,14 +80,28 @@ class DataSourceForm(NetBoxModelForm):
|
|||||||
if self.instance and self.instance.parameters:
|
if self.instance and self.instance.parameters:
|
||||||
self.fields[field_name].initial = self.instance.parameters.get(name)
|
self.fields[field_name].initial = self.instance.parameters.get(name)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
parameters = {}
|
parameters = {}
|
||||||
for name in self.fields:
|
for name in self.fields:
|
||||||
if name.startswith('backend_'):
|
if name.startswith('backend_'):
|
||||||
parameters[name[8:]] = self.cleaned_data[name]
|
parameters[name[8:]] = self.cleaned_data[name]
|
||||||
self.instance.parameters = parameters
|
self.instance.parameters = parameters
|
||||||
|
|
||||||
|
# Determine initial status based on new/existing instance
|
||||||
|
if not self.instance.pk:
|
||||||
|
# New instance
|
||||||
|
object_status = DataSourceStatusChoices.NEW
|
||||||
|
else:
|
||||||
|
# Existing instance
|
||||||
|
if not self.cleaned_data.get("sync_interval"):
|
||||||
|
object_status = DataSourceStatusChoices.READY
|
||||||
|
else:
|
||||||
|
object_status = self.instance.status
|
||||||
|
|
||||||
|
# # Final override only if the user explicitly provided a status
|
||||||
|
self.instance.status = object_status
|
||||||
|
|
||||||
return super().save(*args, **kwargs)
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -111,10 +111,7 @@ class DataSource(JobsMixin, PrimaryModel):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def ready_for_sync(self):
|
def ready_for_sync(self):
|
||||||
return self.enabled and self.status not in (
|
return self.enabled and self.status != DataSourceStatusChoices.SYNCING
|
||||||
DataSourceStatusChoices.QUEUED,
|
|
||||||
DataSourceStatusChoices.SYNCING
|
|
||||||
)
|
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import decimal
|
||||||
|
|
||||||
import django.core.validators
|
import django.core.validators
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
@@ -17,8 +19,8 @@ class Migration(migrations.Migration):
|
|||||||
max_digits=8,
|
max_digits=8,
|
||||||
null=True,
|
null=True,
|
||||||
validators=[
|
validators=[
|
||||||
django.core.validators.MinValueValidator(-90.0),
|
django.core.validators.MinValueValidator(decimal.Decimal('-90.0')),
|
||||||
django.core.validators.MaxValueValidator(90.0),
|
django.core.validators.MaxValueValidator(decimal.Decimal('90.0'))
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -31,8 +33,8 @@ class Migration(migrations.Migration):
|
|||||||
max_digits=9,
|
max_digits=9,
|
||||||
null=True,
|
null=True,
|
||||||
validators=[
|
validators=[
|
||||||
django.core.validators.MinValueValidator(-180.0),
|
django.core.validators.MinValueValidator(decimal.Decimal('-180.0')),
|
||||||
django.core.validators.MaxValueValidator(180.0),
|
django.core.validators.MaxValueValidator(decimal.Decimal('180.0'))
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -45,8 +47,8 @@ class Migration(migrations.Migration):
|
|||||||
max_digits=8,
|
max_digits=8,
|
||||||
null=True,
|
null=True,
|
||||||
validators=[
|
validators=[
|
||||||
django.core.validators.MinValueValidator(-90.0),
|
django.core.validators.MinValueValidator(decimal.Decimal('-90.0')),
|
||||||
django.core.validators.MaxValueValidator(90.0),
|
django.core.validators.MaxValueValidator(decimal.Decimal('90.0'))
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -59,8 +61,8 @@ class Migration(migrations.Migration):
|
|||||||
max_digits=9,
|
max_digits=9,
|
||||||
null=True,
|
null=True,
|
||||||
validators=[
|
validators=[
|
||||||
django.core.validators.MinValueValidator(-180.0),
|
django.core.validators.MinValueValidator(decimal.Decimal('-180.0')),
|
||||||
django.core.validators.MaxValueValidator(180.0),
|
django.core.validators.MaxValueValidator(decimal.Decimal('180.0'))
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -646,7 +646,10 @@ class Device(
|
|||||||
decimal_places=6,
|
decimal_places=6,
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
validators=[MinValueValidator(-90.0), MaxValueValidator(90.0)],
|
validators=[
|
||||||
|
MinValueValidator(decimal.Decimal('-90.0')),
|
||||||
|
MaxValueValidator(decimal.Decimal('90.0'))
|
||||||
|
],
|
||||||
help_text=_("GPS coordinate in decimal format (xx.yyyyyy)")
|
help_text=_("GPS coordinate in decimal format (xx.yyyyyy)")
|
||||||
)
|
)
|
||||||
longitude = models.DecimalField(
|
longitude = models.DecimalField(
|
||||||
@@ -655,7 +658,10 @@ class Device(
|
|||||||
decimal_places=6,
|
decimal_places=6,
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
validators=[MinValueValidator(-180.0), MaxValueValidator(180.0)],
|
validators=[
|
||||||
|
MinValueValidator(decimal.Decimal('-180.0')),
|
||||||
|
MaxValueValidator(decimal.Decimal('180.0'))
|
||||||
|
],
|
||||||
help_text=_("GPS coordinate in decimal format (xx.yyyyyy)")
|
help_text=_("GPS coordinate in decimal format (xx.yyyyyy)")
|
||||||
)
|
)
|
||||||
services = GenericRelation(
|
services = GenericRelation(
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import decimal
|
||||||
|
|
||||||
from django.contrib.contenttypes.fields import GenericRelation
|
from django.contrib.contenttypes.fields import GenericRelation
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
@@ -211,7 +213,10 @@ class Site(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
|
|||||||
decimal_places=6,
|
decimal_places=6,
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
validators=[MinValueValidator(-90.0), MaxValueValidator(90.0)],
|
validators=[
|
||||||
|
MinValueValidator(decimal.Decimal('-90.0')),
|
||||||
|
MaxValueValidator(decimal.Decimal('90.0'))
|
||||||
|
],
|
||||||
help_text=_('GPS coordinate in decimal format (xx.yyyyyy)')
|
help_text=_('GPS coordinate in decimal format (xx.yyyyyy)')
|
||||||
)
|
)
|
||||||
longitude = models.DecimalField(
|
longitude = models.DecimalField(
|
||||||
@@ -220,7 +225,10 @@ class Site(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
|
|||||||
decimal_places=6,
|
decimal_places=6,
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
validators=[MinValueValidator(-180.0), MaxValueValidator(180.0)],
|
validators=[
|
||||||
|
MinValueValidator(decimal.Decimal('-180.0')),
|
||||||
|
MaxValueValidator(decimal.Decimal('180.0'))
|
||||||
|
],
|
||||||
help_text=_('GPS coordinate in decimal format (xx.yyyyyy)')
|
help_text=_('GPS coordinate in decimal format (xx.yyyyyy)')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,14 @@ import logging
|
|||||||
import traceback
|
import traceback
|
||||||
from contextlib import ExitStack
|
from contextlib import ExitStack
|
||||||
|
|
||||||
from django.db import transaction
|
from django.db import router, transaction
|
||||||
|
from django.db import DEFAULT_DB_ALIAS
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from core.signals import clear_events
|
from core.signals import clear_events
|
||||||
|
from dcim.models import Device
|
||||||
from extras.models import Script as ScriptModel
|
from extras.models import Script as ScriptModel
|
||||||
|
from netbox.context_managers import event_tracking
|
||||||
from netbox.jobs import JobRunner
|
from netbox.jobs import JobRunner
|
||||||
from netbox.registry import registry
|
from netbox.registry import registry
|
||||||
from utilities.exceptions import AbortScript, AbortTransaction
|
from utilities.exceptions import AbortScript, AbortTransaction
|
||||||
@@ -42,7 +45,18 @@ class ScriptJob(JobRunner):
|
|||||||
# A script can modify multiple models so need to do an atomic lock on
|
# A script can modify multiple models so need to do an atomic lock on
|
||||||
# both the default database (for non ChangeLogged models) and potentially
|
# both the default database (for non ChangeLogged models) and potentially
|
||||||
# any other database (for ChangeLogged models)
|
# any other database (for ChangeLogged models)
|
||||||
with transaction.atomic():
|
changeloged_db = router.db_for_write(Device)
|
||||||
|
with transaction.atomic(using=DEFAULT_DB_ALIAS):
|
||||||
|
# If branch database is different from default, wrap in a second atomic transaction
|
||||||
|
# Note: Don't add any extra code between the two atomic transactions,
|
||||||
|
# otherwise the changes might get committed to the default database
|
||||||
|
# if there are any raised exceptions.
|
||||||
|
if changeloged_db != DEFAULT_DB_ALIAS:
|
||||||
|
with transaction.atomic(using=changeloged_db):
|
||||||
|
script.output = script.run(data, commit)
|
||||||
|
if not commit:
|
||||||
|
raise AbortTransaction()
|
||||||
|
else:
|
||||||
script.output = script.run(data, commit)
|
script.output = script.run(data, commit)
|
||||||
if not commit:
|
if not commit:
|
||||||
raise AbortTransaction()
|
raise AbortTransaction()
|
||||||
@@ -108,14 +122,14 @@ class ScriptJob(JobRunner):
|
|||||||
script.request = request
|
script.request = request
|
||||||
self.logger.debug(f"Request ID: {request.id if request else None}")
|
self.logger.debug(f"Request ID: {request.id if request else None}")
|
||||||
|
|
||||||
# Execute the script. If commit is True, wrap it with the event_tracking context manager to ensure we process
|
|
||||||
# change logging, event rules, etc.
|
|
||||||
if commit:
|
if commit:
|
||||||
self.logger.info("Executing script (commit enabled)")
|
self.logger.info("Executing script (commit enabled)")
|
||||||
with ExitStack() as stack:
|
|
||||||
for request_processor in registry['request_processors']:
|
|
||||||
stack.enter_context(request_processor(request))
|
|
||||||
self.run_script(script, request, data, commit)
|
|
||||||
else:
|
else:
|
||||||
self.logger.warning("Executing script (commit disabled)")
|
self.logger.warning("Executing script (commit disabled)")
|
||||||
|
|
||||||
|
with ExitStack() as stack:
|
||||||
|
for request_processor in registry['request_processors']:
|
||||||
|
if not commit and request_processor is event_tracking:
|
||||||
|
continue
|
||||||
|
stack.enter_context(request_processor(request))
|
||||||
self.run_script(script, request, data, commit)
|
self.run_script(script, request, data, commit)
|
||||||
|
|||||||
@@ -559,6 +559,7 @@ class ComponentCreateView(GetReturnURLMixin, BaseObjectView):
|
|||||||
form.instance._replicated_base = hasattr(self.form, "replication_fields")
|
form.instance._replicated_base = hasattr(self.form, "replication_fields")
|
||||||
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
|
changelog_message = form.cleaned_data.pop('changelog_message', '')
|
||||||
new_components = []
|
new_components = []
|
||||||
data = deepcopy(request.POST)
|
data = deepcopy(request.POST)
|
||||||
pattern_count = len(form.cleaned_data[self.form.replication_fields[0]])
|
pattern_count = len(form.cleaned_data[self.form.replication_fields[0]])
|
||||||
@@ -585,6 +586,9 @@ class ComponentCreateView(GetReturnURLMixin, BaseObjectView):
|
|||||||
# Create the new components
|
# Create the new components
|
||||||
new_objs = []
|
new_objs = []
|
||||||
for component_form in new_components:
|
for component_form in new_components:
|
||||||
|
# Record changelog message (if any)
|
||||||
|
if changelog_message:
|
||||||
|
component_form.instance._changelog_message = changelog_message
|
||||||
obj = component_form.save()
|
obj = component_form.save()
|
||||||
new_objs.append(obj)
|
new_objs.append(obj)
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user