Compare commits

...

3 Commits

Author SHA1 Message Date
Jeremy Stretch
44e731a40a Release v4.5.0-beta1
Some checks failed
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Has been cancelled
2025-12-16 13:48:45 -05:00
Jason Novinger
a364ee832d Fixes #20929: Require render_config permission for UI config rendering (#20975)
* Closes #20929: Require render_config permission for UI config rendering

- Modified `ObjectRenderConfigView.has_permission()` to require both view and render_config permissions
- Added `remove_permissions()` test helper to remove permissions from existing ObjectPermission objects
- Added regression tests for Device and VirtualMachine render-config permission enforcement

The `render_config` permission action was introduced in #16681 for API endpoints. This extends PR_7604_description
to the UI render-config tabs, preventing users from viewing rendered configurations without explicit permission.

* Address PR feedback

* Address PR feedback
2025-12-16 08:09:25 -05:00
Jeremy Stretch
875e3e7979 Additional work for FR #20788 (#20973)
Some checks are pending
CI / build (20.x, 3.12) (push) Waiting to run
CI / build (20.x, 3.13) (push) Waiting to run
CI / build (20.x, 3.14) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
2025-12-15 14:41:07 -06:00
50 changed files with 16321 additions and 7999 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,14 @@ Minor releases are published in April, August, and December of each calendar yea
This page contains a history of all major and minor releases since NetBox v2.0. For more detail on a specific patch release, please see the release notes page for that specific minor release.
#### [Version 4.5](./version-4.5.md) (January 2026)
* Lookup Modifiers in Filter Forms ([#7604](https://github.com/netbox-community/netbox/issues/7604))
* Improved API Authentication Tokens ([#20210](https://github.com/netbox-community/netbox/issues/20210))
* Object Ownership ([#20304](https://github.com/netbox-community/netbox/issues/20304))
* Advanced Port Mappings ([#20564](https://github.com/netbox-community/netbox/issues/20564))
* Cable Profiles ([#20788](https://github.com/netbox-community/netbox/issues/20788))
#### [Version 4.4](./version-4.4.md) (September 2025)
* Background Jobs for Bulk Operations ([#19589](https://github.com/netbox-community/netbox/issues/19589), [#19891](https://github.com/netbox-community/netbox/issues/19891))

View File

@@ -0,0 +1,150 @@
## v4.5.0 (FUTURE)
### Breaking Changes
* Python 3.10 and 3.11 are no longer supported. NetBox now requires Python 3.12, 3.13, or 3.14.
* GraphQL API queries which filter by object IDs or enums must now specify a filter lookup similar to other fields. For example, `id: 123` becomes `id: {exact: 123 }`.
* Rendering a device or virtual machine configuration is now restricted to users with the `render_config` permission for the applicable object type.
* Retrieval of API token plaintexts is no longer supported. The `ALLOW_TOKEN_RETRIEVAL` config parameter has been removed.
* API tokens can no longer be reassigned from one user to another.
* A config context assigned to a platform will now also apply to any children of that platform. (Although this is typically desired behavior, it may introduce unanticipated changes for existing deployments.)
* The `/api/dcim/cable-terminations/` REST API endpoint is now read-only. Cable terminations must be set on cables directly via the `/api/dcim/cables/` endpoint.
* The UI view dedicated to swapping A/Z circuit terminations has been removed.
* The experimental HTMX navigation feature has been removed.
* The obsolete boolean field `is_staff` has been removed from the `User` model.
* Removal of deprecated behavior
* The `/api/extras/object-types/` REST API endpoint has been removed. (Use `/api/core/object-types/` instead.)
* Webhooks no longer specify a `model` in payload data. (Reference `object_type` instead, which includes the parent app label.)
* The obsolete module `core.models.contenttypes` has been removed (replaced in v4.4 by `core.models.object_types`).
* The `load_yaml()` and `load_json()` utility methods have been removed from the base class for custom scripts.
### New Features
#### Lookup Modifiers in Filter Forms ([#7604](https://github.com/netbox-community/netbox/issues/7604))
Most object list filters within the UI have been extended to include optional lookup modifiers to support more complex queries. For instance, filters for numeric values now include a dropdown where a user can select "less than," "greater than," or "not" in addition to the default equivalency match. The specific modifiers available depend on the type of each filter.
(Note that this feature does not introduce any new filters. Rather, it makes available in the UI filters which already exist.)
#### Improved API Authentication Tokens ([#20210](https://github.com/netbox-community/netbox/issues/20210))
This release introduces a new version of API token (v2) which implements several security improvements. HMAC hashing with a cryptographic pepper is used to authenticate these tokens, obviating the need to store plaintexts. The new tokens also employ a non-sensitive key which can be shared to identify tokens without divulging their plaintexts. We've also adopted the standard "bearer" HTTP header format, as shown below.
```
# v1 token header
Authorization: Token <TOKEN>
# v2 token header
Authorization: Bearer nbt_<KEY>.<TOKEN>
```
Note that v2 token keys are prefixed with the fixed string `nbt_`, which can be used to aid in secret detection.
Backward compatibility with legacy (v1) tokens is retained in this release. However, users are strongly encouraged to begin using only v2 tokens, as support for legacy tokens will be removed in NetBox v4.7.
#### Object Ownership ([#20304](https://github.com/netbox-community/netbox/issues/20304))
An optional `owner` foreign key field has been added to most models. This enables the assignment of objects to a new Owner model, which represents a set of users and/or groups. Through this relationship, we can now convey ownership of objects within NetBox natively, without needing to rely on the assignment of tags or custom fields.
(Note that ownership differs significantly in function from tenancy. Ownership determines the parties responsible for the maintenance of an object, whereas as tenancy conveys an operational dependency.)
#### Advanced Port Mappings ([#20564](https://github.com/netbox-community/netbox/issues/20564))
The previous many-to-one mapping of front to rear ports has been expanded to support bidirectional mappings. The `rear_port` and `rear_port_position` fields on the FrontPort model have been replaced with an intermediary PortMapping model, which supports any number of assignments between front port/position pair and a rear port/position pair. This change unlocks the ability to model complex inline devices that swap individual fiber pairs between cables.
#### Cable Profiles ([#20788](https://github.com/netbox-community/netbox/issues/20788))
Cables can now be assigned profiles which determine how they are treated for path tracing. A profile indicates the number of discrete parallel channels or lanes carried by the cable among its endpoints. For example, a 1-to-4 breakout cable has four lanes, shared at one end via a common termination and split out at the other end to four separate terminations. Profiles, when assigned, enable NetBox to more accurately trace a specific connection within a cable, rather than the cable as a whole.
The assignment of cable profiles is optional: Cable tracing will continue to operate as before for cables with no profile assigned.
### Enhancements
* [#16681](https://github.com/netbox-community/netbox/issues/16681) - Introduce a `render_config` permission, which is now required to render a device or virtual machine configuration
* [#18658](https://github.com/netbox-community/netbox/issues/18658) - Add a `start_on_boot` choice field for virtual machines
* [#19095](https://github.com/netbox-community/netbox/issues/19095) - Add support for Python 3.13 and 3.14
* [#19338](https://github.com/netbox-community/netbox/issues/19338) - Enable filter lookups for object IDs and enums in GraphQL API queries
* [#19523](https://github.com/netbox-community/netbox/issues/19523) - Cache the number of instances for device, module, and rack types, and enable filtering by these counts
* [#20417](https://github.com/netbox-community/netbox/issues/20417) - Add an optional `color` field for device type power outlets
* [#20476](https://github.com/netbox-community/netbox/issues/20476) - Once provisioned, the owner of an API token cannot be changed
* [#20492](https://github.com/netbox-community/netbox/issues/20492) - Completely disabled the means to retrieve legacy API token plaintexts (removed the `ALLOW_TOKEN_RETRIEVAL` config parameter)
* [#20639](https://github.com/netbox-community/netbox/issues/20639) - Apply config contexts to devices/VMs assigned any child platform of the parent platform
* [#20834](https://github.com/netbox-community/netbox/issues/20834) - Add an `enabled` boolean field to API tokens
* [#20917](https://github.com/netbox-community/netbox/issues/20917) - Include usage reference on API token views
* [#20925](https://github.com/netbox-community/netbox/issues/20925) - Add optional `comments` field to all subclasses of `OrganizationalModel`
* [#20929](https://github.com/netbox-community/netbox/issues/20929) - Require the `render_config` permission to view a rendered device/VM configuration in the UI
* [#20936](https://github.com/netbox-community/netbox/issues/20936) - Introduce the `/api/authentication-check/` REST API endpoint for validating authentication tokens
* [#20959](https://github.com/netbox-community/netbox/issues/20959) - Include a count of related module types for a manufacturer in the REST API
### Plugins
* [#13182](https://github.com/netbox-community/netbox/issues/13182) - Added `PrimaryModel`, `OrganizationalModel`, and `NestedGroupModel` to the plugins API, as well as their respective base classes for various resources
### Other Changes
* [#16137](https://github.com/netbox-community/netbox/issues/16137) - Remove the obsolete boolean field `is_staff` from the `User` model
* [#17571](https://github.com/netbox-community/netbox/issues/17571) - Remove the experimental HTMX navigation feature
* [#17936](https://github.com/netbox-community/netbox/issues/17936) - Introduce a dedicated `GFKSerializerField` for representing generic foreign keys in API serializers
* [#19889](https://github.com/netbox-community/netbox/issues/19889) - Drop support for Python 3.10 and 3.11
* [#19898](https://github.com/netbox-community/netbox/issues/19898) - Remove the obsolete REST API endpoint `/api/extras/object-types/`
* [#20088](https://github.com/netbox-community/netbox/issues/20088) - Remove the non-deterministic `model` key from webhook payload data
* [#20095](https://github.com/netbox-community/netbox/issues/20095) - Remove the obsolete module `core.models.contenttypes`
* [#20096](https://github.com/netbox-community/netbox/issues/20096) - Remove the `load_yaml()` and `load_json()` utility methods from the `BaseScript` class
* [#20204](https://github.com/netbox-community/netbox/issues/20204) - Started migrating object views from custom HTML templates to declarative layouts
* [#20295](https://github.com/netbox-community/netbox/issues/20295) - Cable terminations may be modified via the REST API only by modifying the cable itself
* [#20617](https://github.com/netbox-community/netbox/issues/20617) - Introduce `BaseModel` as the global base class for models
* [#20683](https://github.com/netbox-community/netbox/issues/20683) - Remove the UI view dedicated to swapping A/Z circuit terminations
* [#20926](https://github.com/netbox-community/netbox/issues/20926) - Standardize naming of GraphQL filters
### REST API Changes
* Most objects now include an optional `owner` foreign key field.
* The `/api/dcim/cable-terminations` endpoint is now read-only.
* Introduced the `/api/authentication-check/` endpoint to test REST API credentials
* `circuits.CircuitGroup`
* Add optional `comments` field
* `circuits.CircuitType`
* Add optional `comments` field
* `circuits.VirtualCircuitType`
* Add optional `comments` field
* `dcim.Cable`
* Add the optional `profile` choice field
* `dcim.FrontPort`
* Removed the `rear_port` and `rear_port_position` fields
* Add the `positions` integer field
* Add the `rear_ports` list for port mappings
* `dcim.InventoryItemRole`
* Add optional `comments` field
* `dcim.Manufacturer`
* Add optional `comments` field
* Add read-only `moduletype_count` integer field
* `dcim.ModuleType`
* Add read-only `module_count` integer field
* `dcim.PowerOutletTemplate`
* Add optional `color` field
* `dcim.RackRole`
* Add optional `comments` field
* `dcim.RackType`
* Add read-only `rack_count` integer field
* `dcim.RearPort`
* Add the `front_ports` list for port mappings
* `ipam.ASNRange`
* Add optional `comments` field
* `ipam.RIR`
* Add optional `comments` field
* `ipam.Role`
* Add optional `comments` field
* `ipam.VLANGroup`
* Add optional `comments` field
* `tenancy.ContactRole`
* Add optional `comments` field
* `users.Token`
* Add `enabled` boolean field
* `virtualization.ClusterGroup`
* Add optional `comments` field
* `virtualization.ClusterType`
* Add optional `comments` field
* `virtualization.VirtualMachine`
* Add optional `start_on_boot` choice field
* `vpn.TunnelGroup`
* Add optional `comments` field

View File

@@ -322,6 +322,7 @@ nav:
- git Cheat Sheet: 'development/git-cheat-sheet.md'
- Release Notes:
- Summary: 'release-notes/index.md'
- Version 4.5: 'release-notes/version-4.5.md'
- Version 4.4: 'release-notes/version-4.4.md'
- Version 4.3: 'release-notes/version-4.3.md'
- Version 4.2: 'release-notes/version-4.2.md'

View File

@@ -353,7 +353,7 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
model = CircuitTermination
fields = (
'id', 'termination_id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id', 'description',
'mark_connected', 'pp_info', 'cable_end', 'cable_position',
'mark_connected', 'pp_info', 'cable_end', 'cable_connector',
)
def search(self, queryset, name, value):

View File

@@ -0,0 +1,39 @@
import django.contrib.postgres.fields
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('circuits', '0053_owner'),
]
operations = [
migrations.AddField(
model_name='circuittermination',
name='cable_connector',
field=models.PositiveSmallIntegerField(
blank=True,
null=True,
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(256)
],
),
),
migrations.AddField(
model_name='circuittermination',
name='cable_positions',
field=django.contrib.postgres.fields.ArrayField(
base_field=models.PositiveSmallIntegerField(
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(1024),
]
),
blank=True,
null=True,
size=None,
),
),
]

View File

@@ -1,23 +0,0 @@
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('circuits', '0053_owner'),
]
operations = [
migrations.AddField(
model_name='circuittermination',
name='cable_position',
field=models.PositiveIntegerField(
blank=True,
null=True,
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(1024),
],
),
),
]

View File

@@ -6,7 +6,7 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('circuits', '0054_cable_position'),
('circuits', '0054_cable_connector_positions'),
]
operations = [

View File

@@ -433,7 +433,7 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = CircuitTermination.objects.all()
filterset = CircuitTerminationFilterSet
ignore_fields = ('cable',)
ignore_fields = ('cable', 'cable_positions')
@classmethod
def setUpTestData(cls):

View File

@@ -61,11 +61,12 @@ class CableTerminationSerializer(NetBoxModelSerializer):
model = CableTermination
fields = [
'id', 'url', 'display', 'cable', 'cable_end', 'termination_type', 'termination_id',
'termination', 'position', 'created', 'last_updated',
'termination', 'connector', 'positions', 'created', 'last_updated',
]
read_only_fields = fields
brief_fields = (
'id', 'url', 'display', 'cable', 'cable_end', 'position', 'termination_type', 'termination_id',
'id', 'url', 'display', 'cable', 'cable_end', 'connector', 'positions', 'termination_type',
'termination_id',
)

View File

@@ -1,108 +1,390 @@
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from dcim.choices import CableEndChoices
from dcim.models import CableTermination
class BaseCableProfile:
# Maximum number of terminations allowed per side
a_max_connections = None
b_max_connections = None
"""Base class for representing a cable profile."""
# Mappings of connectors to the number of positions presented by each, at either end of the cable. For example, a
# 12-strand MPO fiber cable would have one connector at either end with six positions (six bidirectional fiber
# pairs).
a_connectors = {}
b_connectors = {}
# Defined a mapping of A/B connector & position pairings. If not defined, all positions are presumed to be
# symmetrical (i.e. 1:1 on side A maps to 1:1 on side B). If defined, it must be constructed as a dictionary of
# two-item tuples, e.g. {(1, 1): (1, 1)}.
_mapping = None
def clean(self, cable):
# Enforce maximum connection limits
if self.a_max_connections and len(cable.a_terminations) > self.a_max_connections:
# Enforce maximum terminations limits
a_terminations_count = len(cable.a_terminations)
b_terminations_count = len(cable.b_terminations)
max_a_terminations = len(self.a_connectors)
max_b_terminations = len(self.b_connectors)
if a_terminations_count > max_a_terminations:
raise ValidationError({
'a_terminations': _(
'Maximum A side connections for profile {profile}: {max}'
'A side of cable has {count} terminations but only {max} are permitted for profile {profile}'
).format(
count=a_terminations_count,
profile=cable.get_profile_display(),
max=self.a_max_connections,
max=max_a_terminations,
)
})
if self.b_max_connections and len(cable.b_terminations) > self.b_max_connections:
if b_terminations_count > max_b_terminations:
raise ValidationError({
'b_terminations': _(
'Maximum B side connections for profile {profile}: {max}'
'B side of cable has {count} terminations but only {max} are permitted for profile {profile}'
).format(
count=b_terminations_count,
profile=cable.get_profile_display(),
max=self.b_max_connections,
max=max_b_terminations,
)
})
def get_mapped_position(self, side, position):
def get_mapped_position(self, side, connector, position):
"""
Return the mapped position for a given cable end and position.
By default, assume all positions are symmetrical.
Return the mapped far-end connector & position for a given cable end the local connector & position.
"""
return position
# By default, assume all positions are symmetrical.
if self._mapping:
return self._mapping.get((connector, position))
return connector, position
def get_peer_terminations(self, terminations, position_stack):
local_end = terminations[0].cable_end
qs = CableTermination.objects.filter(
cable=terminations[0].cable,
cable_end=terminations[0].opposite_cable_end
)
def get_peer_termination(self, termination, position):
"""
Given a terminating object, return the peer terminating object (if any) on the opposite end of the cable.
"""
try:
connector, position = self.get_mapped_position(
termination.cable_end,
termination.cable_connector,
position
)
except TypeError:
raise ValueError(
f"Could not map connector {termination.cable_connector} position {position} on side "
f"{termination.cable_end}"
)
try:
ct = CableTermination.objects.get(
cable=termination.cable,
cable_end=termination.opposite_cable_end,
connector=connector,
positions__contains=[position],
)
return ct.termination, position
except CableTermination.DoesNotExist:
return None, None
# TODO: Optimize this to use a single query under any condition
if position_stack:
# Attempt to find a peer termination at the same position currently in the stack. Pop the stack only if
# we find one. Otherwise, return any peer terminations with a null position.
position = self.get_mapped_position(local_end, position_stack[-1][0])
if peers := qs.filter(position=position):
position_stack.pop()
return peers
return qs.filter(position=None)
@staticmethod
def get_position_list(n):
"""Return a list of integers from 1 to n, inclusive."""
return list(range(1, n + 1))
class StraightSingleCableProfile(BaseCableProfile):
a_max_connections = 1
b_max_connections = 1
# Profile naming:
# - Single: One connector per side, with one or more positions
# - Trunk: Two or more connectors per side, with one or more positions per connector
# - Breakout: One or more connectors on the A side which map to a greater number of B side connectors
# - Shuffle: A cable with nonlinear position mappings between sides
class StraightMultiCableProfile(BaseCableProfile):
a_max_connections = None
b_max_connections = None
class Shuffle2x2MPO8CableProfile(BaseCableProfile):
a_max_connections = 8
b_max_connections = 8
_mapping = {
class Single1C1PCableProfile(BaseCableProfile):
a_connectors = {
1: 1,
}
b_connectors = a_connectors
class Single1C2PCableProfile(BaseCableProfile):
a_connectors = {
1: 2,
}
b_connectors = a_connectors
class Single1C4PCableProfile(BaseCableProfile):
a_connectors = {
1: 4,
}
b_connectors = a_connectors
class Single1C6PCableProfile(BaseCableProfile):
a_connectors = {
1: 6,
}
b_connectors = a_connectors
class Single1C8PCableProfile(BaseCableProfile):
a_connectors = {
1: 8,
}
b_connectors = a_connectors
class Single1C12PCableProfile(BaseCableProfile):
a_connectors = {
1: 12,
}
b_connectors = a_connectors
class Single1C16PCableProfile(BaseCableProfile):
a_connectors = {
1: 16,
}
b_connectors = a_connectors
class Trunk2C1PCableProfile(BaseCableProfile):
a_connectors = {
1: 1,
2: 1,
}
b_connectors = a_connectors
class Trunk2C2PCableProfile(BaseCableProfile):
a_connectors = {
1: 2,
2: 2,
3: 5,
4: 6,
5: 3,
6: 4,
7: 7,
8: 8,
}
def get_mapped_position(self, side, position):
return self._mapping.get(position)
b_connectors = a_connectors
class Shuffle4x4MPO8CableProfile(BaseCableProfile):
a_max_connections = 8
b_max_connections = 8
# A side to B side position mapping
_a_mapping = {
class Trunk2C4PCableProfile(BaseCableProfile):
a_connectors = {
1: 4,
2: 4,
}
b_connectors = a_connectors
class Trunk2C6PCableProfile(BaseCableProfile):
a_connectors = {
1: 6,
2: 6,
}
b_connectors = a_connectors
class Trunk2C8PCableProfile(BaseCableProfile):
a_connectors = {
1: 8,
2: 8,
}
b_connectors = a_connectors
class Trunk2C12PCableProfile(BaseCableProfile):
a_connectors = {
1: 12,
2: 12,
}
b_connectors = a_connectors
class Trunk4C1PCableProfile(BaseCableProfile):
a_connectors = {
1: 1,
2: 3,
3: 5,
4: 7,
5: 2,
6: 4,
7: 6,
8: 8,
2: 1,
3: 1,
4: 1,
}
# B side to A side position mapping (reverse of _a_mapping)
_b_mapping = {v: k for k, v in _a_mapping.items()}
b_connectors = a_connectors
def get_mapped_position(self, side, position):
if side.lower() == 'b':
return self._b_mapping.get(position)
return self._a_mapping.get(position)
class Trunk4C2PCableProfile(BaseCableProfile):
a_connectors = {
1: 2,
2: 2,
3: 2,
4: 2,
}
b_connectors = a_connectors
class Trunk4C4PCableProfile(BaseCableProfile):
a_connectors = {
1: 4,
2: 4,
3: 4,
4: 4,
}
b_connectors = a_connectors
class Trunk4C6PCableProfile(BaseCableProfile):
a_connectors = {
1: 6,
2: 6,
3: 6,
4: 6,
}
b_connectors = a_connectors
class Trunk4C8PCableProfile(BaseCableProfile):
a_connectors = {
1: 8,
2: 8,
3: 8,
4: 8,
}
b_connectors = a_connectors
class Trunk8C4PCableProfile(BaseCableProfile):
a_connectors = {
1: 4,
2: 4,
3: 4,
4: 4,
5: 4,
6: 4,
7: 4,
8: 4,
}
b_connectors = a_connectors
class Breakout1C4Px4C1PCableProfile(BaseCableProfile):
a_connectors = {
1: 4,
}
b_connectors = {
1: 1,
2: 1,
3: 1,
4: 1,
}
_mapping = {
(1, 1): (1, 1),
(1, 2): (2, 1),
(1, 3): (3, 1),
(1, 4): (4, 1),
(2, 1): (1, 2),
(3, 1): (1, 3),
(4, 1): (1, 4),
}
class Breakout1C6Px6C1PCableProfile(BaseCableProfile):
a_connectors = {
1: 6,
}
b_connectors = {
1: 1,
2: 1,
3: 1,
4: 1,
5: 1,
6: 1,
}
_mapping = {
(1, 1): (1, 1),
(1, 2): (2, 1),
(1, 3): (3, 1),
(1, 4): (4, 1),
(1, 5): (5, 1),
(1, 6): (6, 1),
(2, 1): (1, 2),
(3, 1): (1, 3),
(4, 1): (1, 4),
(5, 1): (1, 5),
(6, 1): (1, 6),
}
class Trunk2C4PShuffleCableProfile(BaseCableProfile):
a_connectors = {
1: 4,
2: 4,
}
b_connectors = a_connectors
_mapping = {
(1, 1): (1, 1),
(1, 2): (1, 2),
(1, 3): (2, 1),
(1, 4): (2, 2),
(2, 1): (1, 3),
(2, 2): (1, 4),
(2, 3): (2, 3),
(2, 4): (2, 4),
}
class Trunk4C4PShuffleCableProfile(BaseCableProfile):
a_connectors = {
1: 4,
2: 4,
3: 4,
4: 4,
}
b_connectors = a_connectors
_mapping = {
(1, 1): (1, 1),
(1, 2): (2, 1),
(1, 3): (3, 1),
(1, 4): (4, 1),
(2, 1): (1, 2),
(2, 2): (2, 2),
(2, 3): (3, 2),
(2, 4): (4, 2),
(3, 1): (1, 3),
(3, 2): (2, 3),
(3, 3): (3, 3),
(3, 4): (4, 3),
(4, 1): (1, 4),
(4, 2): (2, 4),
(4, 3): (3, 4),
(4, 4): (4, 4),
}
class Breakout2C4Px8C1PShuffleCableProfile(BaseCableProfile):
a_connectors = {
1: 4,
2: 4,
}
b_connectors = {
1: 1,
2: 1,
3: 1,
4: 1,
5: 1,
6: 1,
7: 1,
8: 1,
}
_a_mapping = {
(1, 1): (1, 1),
(1, 2): (2, 1),
(1, 3): (5, 1),
(1, 4): (6, 1),
(2, 1): (3, 1),
(2, 2): (4, 1),
(2, 3): (7, 1),
(2, 4): (8, 1),
}
_b_mapping = {
(1, 1): (1, 1),
(2, 1): (1, 2),
(3, 1): (2, 1),
(4, 1): (2, 2),
(5, 1): (1, 3),
(6, 1): (1, 4),
(7, 1): (2, 3),
(8, 1): (2, 4),
}
def get_mapped_position(self, side, connector, position):
if side.upper() == CableEndChoices.SIDE_A:
return self._a_mapping.get((connector, position))
return self._b_mapping.get((connector, position))

View File

@@ -1722,16 +1722,74 @@ class PortTypeChoices(ChoiceSet):
#
class CableProfileChoices(ChoiceSet):
STRAIGHT_SINGLE = 'straight-single'
STRAIGHT_MULTI = 'straight-multi'
SHUFFLE_2X2_MPO8 = 'shuffle-2x2-mpo8'
SHUFFLE_4X4_MPO8 = 'shuffle-4x4-mpo8'
# Singles
SINGLE_1C1P = 'single-1c1p'
SINGLE_1C2P = 'single-1c2p'
SINGLE_1C4P = 'single-1c4p'
SINGLE_1C6P = 'single-1c6p'
SINGLE_1C8P = 'single-1c8p'
SINGLE_1C12P = 'single-1c12p'
SINGLE_1C16P = 'single-1c16p'
# Trunks
TRUNK_2C1P = 'trunk-2c1p'
TRUNK_2C2P = 'trunk-2c2p'
TRUNK_2C4P = 'trunk-2c4p'
TRUNK_2C4P_SHUFFLE = 'trunk-2c4p-shuffle'
TRUNK_2C6P = 'trunk-2c6p'
TRUNK_2C8P = 'trunk-2c8p'
TRUNK_2C12P = 'trunk-2c12p'
TRUNK_4C1P = 'trunk-4c1p'
TRUNK_4C2P = 'trunk-4c2p'
TRUNK_4C4P = 'trunk-4c4p'
TRUNK_4C4P_SHUFFLE = 'trunk-4c4p-shuffle'
TRUNK_4C6P = 'trunk-4c6p'
TRUNK_4C8P = 'trunk-4c8p'
TRUNK_8C4P = 'trunk-8c4p'
# Breakouts
BREAKOUT_1C4P_4C1P = 'breakout-1c4p-4c1p'
BREAKOUT_1C6P_6C1P = 'breakout-1c6p-6c1p'
BREAKOUT_2C4P_8C1P_SHUFFLE = 'breakout-2c4p-8c1p-shuffle'
CHOICES = (
(STRAIGHT_SINGLE, _('Straight (single position)')),
(STRAIGHT_MULTI, _('Straight (multi-position)')),
(SHUFFLE_2X2_MPO8, _('Shuffle (2x2 MPO8)')),
(SHUFFLE_4X4_MPO8, _('Shuffle (4x4 MPO8)')),
(
_('Single'),
(
(SINGLE_1C1P, _('1C1P')),
(SINGLE_1C2P, _('1C2P')),
(SINGLE_1C4P, _('1C4P')),
(SINGLE_1C6P, _('1C6P')),
(SINGLE_1C8P, _('1C8P')),
(SINGLE_1C12P, _('1C12P')),
(SINGLE_1C16P, _('1C16P')),
),
),
(
_('Trunk'),
(
(TRUNK_2C1P, _('2C1P trunk')),
(TRUNK_2C2P, _('2C2P trunk')),
(TRUNK_2C4P, _('2C4P trunk')),
(TRUNK_2C4P_SHUFFLE, _('2C4P trunk (shuffle)')),
(TRUNK_2C6P, _('2C6P trunk')),
(TRUNK_2C8P, _('2C8P trunk')),
(TRUNK_2C12P, _('2C12P trunk')),
(TRUNK_4C1P, _('4C1P trunk')),
(TRUNK_4C2P, _('4C2P trunk')),
(TRUNK_4C4P, _('4C4P trunk')),
(TRUNK_4C4P_SHUFFLE, _('4C4P trunk (shuffle)')),
(TRUNK_4C6P, _('4C6P trunk')),
(TRUNK_4C8P, _('4C8P trunk')),
(TRUNK_8C4P, _('8C4P trunk')),
),
),
(
_('Breakout'),
(
(BREAKOUT_1C4P_4C1P, _('1C4P:4C1P breakout')),
(BREAKOUT_1C6P_6C1P, _('1C6P:6C1P breakout')),
(BREAKOUT_2C4P_8C1P_SHUFFLE, _('2C4P:8C1P breakout (shuffle)')),
),
),
)

View File

@@ -24,6 +24,9 @@ RACK_STARTING_UNIT_DEFAULT = 1
# Cables
#
CABLE_CONNECTOR_MIN = 1
CABLE_CONNECTOR_MAX = 256
CABLE_POSITION_MIN = 1
CABLE_POSITION_MAX = 1024

View File

@@ -1748,7 +1748,9 @@ class ConsolePortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSe
class Meta:
model = ConsolePort
fields = ('id', 'name', 'label', 'speed', 'description', 'mark_connected', 'cable_end', 'cable_position')
fields = (
'id', 'name', 'label', 'speed', 'description', 'mark_connected', 'cable_end', 'cable_connector',
)
@register_filterset
@@ -1760,7 +1762,9 @@ class ConsoleServerPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFi
class Meta:
model = ConsoleServerPort
fields = ('id', 'name', 'label', 'speed', 'description', 'mark_connected', 'cable_end', 'cable_position')
fields = (
'id', 'name', 'label', 'speed', 'description', 'mark_connected', 'cable_end', 'cable_connector',
)
@register_filterset
@@ -1774,7 +1778,7 @@ class PowerPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet,
model = PowerPort
fields = (
'id', 'name', 'label', 'maximum_draw', 'allocated_draw', 'description', 'mark_connected', 'cable_end',
'cable_position',
'cable_connector',
)
@@ -1801,7 +1805,7 @@ class PowerOutletFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSe
model = PowerOutlet
fields = (
'id', 'name', 'status', 'label', 'feed_leg', 'description', 'color', 'mark_connected', 'cable_end',
'cable_position',
'cable_connector',
)
@@ -2111,7 +2115,7 @@ class InterfaceFilterSet(
fields = (
'id', 'name', 'label', 'type', 'enabled', 'mtu', 'mgmt_only', 'poe_mode', 'poe_type', 'mode', 'rf_role',
'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'mark_connected',
'cable_id', 'cable_end', 'cable_position',
'cable_id', 'cable_end', 'cable_connector',
)
def filter_virtual_chassis_member_or_master(self, queryset, name, value):
@@ -2167,7 +2171,7 @@ class FrontPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet)
model = FrontPort
fields = (
'id', 'name', 'label', 'type', 'color', 'positions', 'description', 'mark_connected', 'cable_end',
'cable_position',
'cable_connector',
)
@@ -2188,7 +2192,7 @@ class RearPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet):
model = RearPort
fields = (
'id', 'name', 'label', 'type', 'color', 'positions', 'description', 'mark_connected', 'cable_end',
'cable_position',
'cable_connector',
)
@@ -2544,7 +2548,7 @@ class CableTerminationFilterSet(ChangeLoggedModelFilterSet):
class Meta:
model = CableTermination
fields = ('id', 'cable', 'cable_end', 'position', 'termination_type', 'termination_id')
fields = ('id', 'cable', 'cable_end', 'termination_type', 'termination_id')
@register_filterset
@@ -2663,7 +2667,7 @@ class PowerFeedFilterSet(PrimaryModelFilterSet, CabledObjectFilterSet, PathEndpo
model = PowerFeed
fields = (
'id', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization',
'available_power', 'mark_connected', 'cable_end', 'cable_position', 'description',
'available_power', 'mark_connected', 'cable_end', 'cable_connector', 'description',
)
def search(self, queryset, name, value):

View File

@@ -1,3 +1,4 @@
import django.contrib.postgres.fields
import django.core.validators
from django.db import migrations, models
@@ -16,25 +17,40 @@ class Migration(migrations.Migration):
),
migrations.AddField(
model_name='cabletermination',
name='position',
field=models.PositiveIntegerField(
name='connector',
field=models.PositiveSmallIntegerField(
blank=True,
null=True,
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(1024),
],
django.core.validators.MaxValueValidator(256)
]
),
),
migrations.AddField(
model_name='cabletermination',
name='positions',
field=django.contrib.postgres.fields.ArrayField(
base_field=models.PositiveSmallIntegerField(
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(1024)
]
),
blank=True,
null=True,
size=None
),
),
migrations.AlterModelOptions(
name='cabletermination',
options={'ordering': ('cable', 'cable_end', 'position', 'pk')},
options={'ordering': ('cable', 'cable_end', 'connector', 'pk')}, # connector may be null
),
migrations.AddConstraint(
model_name='cabletermination',
constraint=models.UniqueConstraint(
fields=('cable', 'cable_end', 'position'),
name='dcim_cabletermination_unique_position'
fields=('cable', 'cable_end', 'connector'),
name='dcim_cabletermination_unique_connector'
),
),
]

View File

@@ -0,0 +1,228 @@
import django.contrib.postgres.fields
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0220_cable_profile'),
]
operations = [
migrations.AddField(
model_name='consoleport',
name='cable_connector',
field=models.PositiveSmallIntegerField(
blank=True,
null=True,
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(256)
],
),
),
migrations.AddField(
model_name='consoleport',
name='cable_positions',
field=django.contrib.postgres.fields.ArrayField(
base_field=models.PositiveSmallIntegerField(
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(1024),
]
),
blank=True,
null=True,
size=None,
),
),
migrations.AddField(
model_name='consoleserverport',
name='cable_connector',
field=models.PositiveSmallIntegerField(
blank=True,
null=True,
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(256)
],
),
),
migrations.AddField(
model_name='consoleserverport',
name='cable_positions',
field=django.contrib.postgres.fields.ArrayField(
base_field=models.PositiveSmallIntegerField(
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(1024),
]
),
blank=True,
null=True,
size=None,
),
),
migrations.AddField(
model_name='frontport',
name='cable_connector',
field=models.PositiveSmallIntegerField(
blank=True,
null=True,
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(256)
],
),
),
migrations.AddField(
model_name='frontport',
name='cable_positions',
field=django.contrib.postgres.fields.ArrayField(
base_field=models.PositiveSmallIntegerField(
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(1024),
]
),
blank=True,
null=True,
size=None,
),
),
migrations.AddField(
model_name='interface',
name='cable_connector',
field=models.PositiveSmallIntegerField(
blank=True,
null=True,
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(256)
],
),
),
migrations.AddField(
model_name='interface',
name='cable_positions',
field=django.contrib.postgres.fields.ArrayField(
base_field=models.PositiveSmallIntegerField(
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(1024),
]
),
blank=True,
null=True,
size=None,
),
),
migrations.AddField(
model_name='powerfeed',
name='cable_connector',
field=models.PositiveSmallIntegerField(
blank=True,
null=True,
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(256)
],
),
),
migrations.AddField(
model_name='powerfeed',
name='cable_positions',
field=django.contrib.postgres.fields.ArrayField(
base_field=models.PositiveSmallIntegerField(
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(1024),
]
),
blank=True,
null=True,
size=None,
),
),
migrations.AddField(
model_name='poweroutlet',
name='cable_connector',
field=models.PositiveSmallIntegerField(
blank=True,
null=True,
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(256)
],
),
),
migrations.AddField(
model_name='poweroutlet',
name='cable_positions',
field=django.contrib.postgres.fields.ArrayField(
base_field=models.PositiveSmallIntegerField(
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(1024),
]
),
blank=True,
null=True,
size=None,
),
),
migrations.AddField(
model_name='powerport',
name='cable_connector',
field=models.PositiveSmallIntegerField(
blank=True,
null=True,
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(256)
],
),
),
migrations.AddField(
model_name='powerport',
name='cable_positions',
field=django.contrib.postgres.fields.ArrayField(
base_field=models.PositiveSmallIntegerField(
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(1024),
]
),
blank=True,
null=True,
size=None,
),
),
migrations.AddField(
model_name='rearport',
name='cable_connector',
field=models.PositiveSmallIntegerField(
blank=True,
null=True,
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(256)
],
),
),
migrations.AddField(
model_name='rearport',
name='cable_positions',
field=django.contrib.postgres.fields.ArrayField(
base_field=models.PositiveSmallIntegerField(
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(1024),
]
),
blank=True,
null=True,
size=None,
),
),
]

View File

@@ -1,107 +0,0 @@
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0220_cable_profile'),
]
operations = [
migrations.AddField(
model_name='consoleport',
name='cable_position',
field=models.PositiveIntegerField(
blank=True,
null=True,
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(1024),
],
),
),
migrations.AddField(
model_name='consoleserverport',
name='cable_position',
field=models.PositiveIntegerField(
blank=True,
null=True,
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(1024),
],
),
),
migrations.AddField(
model_name='frontport',
name='cable_position',
field=models.PositiveIntegerField(
blank=True,
null=True,
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(1024),
],
),
),
migrations.AddField(
model_name='interface',
name='cable_position',
field=models.PositiveIntegerField(
blank=True,
null=True,
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(1024),
],
),
),
migrations.AddField(
model_name='powerfeed',
name='cable_position',
field=models.PositiveIntegerField(
blank=True,
null=True,
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(1024),
],
),
),
migrations.AddField(
model_name='poweroutlet',
name='cable_position',
field=models.PositiveIntegerField(
blank=True,
null=True,
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(1024),
],
),
),
migrations.AddField(
model_name='powerport',
name='cable_position',
field=models.PositiveIntegerField(
blank=True,
null=True,
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(1024),
],
),
),
migrations.AddField(
model_name='rearport',
name='cable_position',
field=models.PositiveIntegerField(
blank=True,
null=True,
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(1024),
],
),
),
]

View File

@@ -59,7 +59,7 @@ def populate_port_mappings(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [
('dcim', '0221_cable_position'),
('dcim', '0221_cable_connector_positions'),
]
operations = [

View File

@@ -3,6 +3,7 @@ import logging
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.fields import ArrayField
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
@@ -136,10 +137,30 @@ class Cable(PrimaryModel):
def profile_class(self):
from dcim import cable_profiles
return {
CableProfileChoices.STRAIGHT_SINGLE: cable_profiles.StraightSingleCableProfile,
CableProfileChoices.STRAIGHT_MULTI: cable_profiles.StraightMultiCableProfile,
CableProfileChoices.SHUFFLE_2X2_MPO8: cable_profiles.Shuffle2x2MPO8CableProfile,
CableProfileChoices.SHUFFLE_4X4_MPO8: cable_profiles.Shuffle4x4MPO8CableProfile,
CableProfileChoices.SINGLE_1C1P: cable_profiles.Single1C1PCableProfile,
CableProfileChoices.SINGLE_1C2P: cable_profiles.Single1C2PCableProfile,
CableProfileChoices.SINGLE_1C4P: cable_profiles.Single1C4PCableProfile,
CableProfileChoices.SINGLE_1C6P: cable_profiles.Single1C6PCableProfile,
CableProfileChoices.SINGLE_1C8P: cable_profiles.Single1C8PCableProfile,
CableProfileChoices.SINGLE_1C12P: cable_profiles.Single1C12PCableProfile,
CableProfileChoices.SINGLE_1C16P: cable_profiles.Single1C16PCableProfile,
CableProfileChoices.TRUNK_2C1P: cable_profiles.Trunk2C1PCableProfile,
CableProfileChoices.TRUNK_2C2P: cable_profiles.Trunk2C2PCableProfile,
CableProfileChoices.TRUNK_2C4P: cable_profiles.Trunk2C4PCableProfile,
CableProfileChoices.TRUNK_2C4P_SHUFFLE: cable_profiles.Trunk2C4PShuffleCableProfile,
CableProfileChoices.TRUNK_2C6P: cable_profiles.Trunk2C6PCableProfile,
CableProfileChoices.TRUNK_2C8P: cable_profiles.Trunk2C8PCableProfile,
CableProfileChoices.TRUNK_2C12P: cable_profiles.Trunk2C12PCableProfile,
CableProfileChoices.TRUNK_4C1P: cable_profiles.Trunk4C1PCableProfile,
CableProfileChoices.TRUNK_4C2P: cable_profiles.Trunk4C2PCableProfile,
CableProfileChoices.TRUNK_4C4P: cable_profiles.Trunk4C4PCableProfile,
CableProfileChoices.TRUNK_4C4P_SHUFFLE: cable_profiles.Trunk4C4PShuffleCableProfile,
CableProfileChoices.TRUNK_4C6P: cable_profiles.Trunk4C6PCableProfile,
CableProfileChoices.TRUNK_4C8P: cable_profiles.Trunk4C8PCableProfile,
CableProfileChoices.TRUNK_8C4P: cable_profiles.Trunk8C4PCableProfile,
CableProfileChoices.BREAKOUT_1C4P_4C1P: cable_profiles.Breakout1C4Px4C1PCableProfile,
CableProfileChoices.BREAKOUT_1C6P_6C1P: cable_profiles.Breakout1C6Px6C1PCableProfile,
CableProfileChoices.BREAKOUT_2C4P_8C1P_SHUFFLE: cable_profiles.Breakout2C4Px8C1PShuffleCableProfile,
}.get(self.profile)
def _get_x_terminations(self, side):
@@ -338,14 +359,33 @@ class Cable(PrimaryModel):
ct.delete()
# Save any new CableTerminations
profile = self.profile_class() if self.profile else None
for i, termination in enumerate(self.a_terminations, start=1):
if not termination.pk or termination not in a_terminations:
position = i if self.profile and isinstance(termination, PathEndpoint) else None
CableTermination(cable=self, cable_end='A', position=position, termination=termination).save()
connector = positions = None
if profile:
connector = i
positions = profile.get_position_list(profile.a_connectors[i])
CableTermination(
cable=self,
cable_end=CableEndChoices.SIDE_A,
connector=connector,
positions=positions,
termination=termination
).save()
for i, termination in enumerate(self.b_terminations, start=1):
if not termination.pk or termination not in b_terminations:
position = i if self.profile and isinstance(termination, PathEndpoint) else None
CableTermination(cable=self, cable_end='B', position=position, termination=termination).save()
connector = positions = None
if profile:
connector = i
positions = profile.get_position_list(profile.b_connectors[i])
CableTermination(
cable=self,
cable_end=CableEndChoices.SIDE_B,
connector=connector,
positions=positions,
termination=termination
).save()
class CableTermination(ChangeLoggedModel):
@@ -372,13 +412,23 @@ class CableTermination(ChangeLoggedModel):
ct_field='termination_type',
fk_field='termination_id'
)
position = models.PositiveIntegerField(
connector = models.PositiveSmallIntegerField(
blank=True,
null=True,
validators=(
MinValueValidator(CABLE_POSITION_MIN),
MaxValueValidator(CABLE_POSITION_MAX)
)
MinValueValidator(CABLE_CONNECTOR_MIN),
MaxValueValidator(CABLE_CONNECTOR_MAX)
),
)
positions = ArrayField(
base_field=models.PositiveSmallIntegerField(
validators=(
MinValueValidator(CABLE_POSITION_MIN),
MaxValueValidator(CABLE_POSITION_MAX)
)
),
blank=True,
null=True,
)
# Cached associations to enable efficient filtering
@@ -410,15 +460,15 @@ class CableTermination(ChangeLoggedModel):
objects = RestrictedQuerySet.as_manager()
class Meta:
ordering = ('cable', 'cable_end', 'position', 'pk')
ordering = ('cable', 'cable_end', 'connector', 'pk')
constraints = (
models.UniqueConstraint(
fields=('termination_type', 'termination_id'),
name='%(app_label)s_%(class)s_unique_termination'
),
models.UniqueConstraint(
fields=('cable', 'cable_end', 'position'),
name='%(app_label)s_%(class)s_unique_position'
fields=('cable', 'cable_end', 'connector'),
name='%(app_label)s_%(class)s_unique_connector'
),
)
verbose_name = _('cable termination')
@@ -481,9 +531,7 @@ class CableTermination(ChangeLoggedModel):
# Set the cable on the terminating object
termination = self.termination._meta.model.objects.get(pk=self.termination_id)
termination.snapshot()
termination.cable = self.cable
termination.cable_end = self.cable_end
termination.cable_position = self.position
termination.set_cable_termination(self)
termination.save()
def delete(self, *args, **kwargs):
@@ -491,9 +539,7 @@ class CableTermination(ChangeLoggedModel):
# Delete the cable association on the terminating object
termination = self.termination._meta.model.objects.get(pk=self.termination_id)
termination.snapshot()
termination.cable = None
termination.cable_end = None
termination.cable_position = None
termination.clear_cable_termination(self)
termination.save()
super().delete(*args, **kwargs)
@@ -701,9 +747,9 @@ class CablePath(models.Model):
path.append([
object_to_path_node(t) for t in terminations
])
# If not null, push cable_position onto the stack
if terminations[0].cable_position is not None:
position_stack.append([terminations[0].cable_position])
# If not null, push cable position onto the stack
if isinstance(terminations[0], PathEndpoint) and terminations[0].cable_positions:
position_stack.append([terminations[0].cable_positions[0]])
# Step 2: Determine the attached links (Cable or WirelessLink), if any
links = list(dict.fromkeys(
@@ -744,8 +790,9 @@ class CablePath(models.Model):
# Profile-based tracing
if links[0].profile:
cable_profile = links[0].profile_class()
peer_cable_terminations = cable_profile.get_peer_terminations(terminations, position_stack)
remote_terminations = [ct.termination for ct in peer_cable_terminations]
term, position = cable_profile.get_peer_termination(terminations[0], position_stack.pop()[0])
remote_terminations = [term]
position_stack.append([position])
# Legacy (positionless) behavior
else:

View File

@@ -1,6 +1,7 @@
from functools import cached_property
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.contrib.postgres.fields import ArrayField
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
@@ -177,15 +178,24 @@ class CabledObjectModel(models.Model):
blank=True,
null=True
)
cable_position = models.PositiveIntegerField(
verbose_name=_('cable position'),
cable_connector = models.PositiveSmallIntegerField(
blank=True,
null=True,
validators=(
MinValueValidator(CABLE_POSITION_MIN),
MaxValueValidator(CABLE_POSITION_MAX)
MinValueValidator(CABLE_CONNECTOR_MIN),
MaxValueValidator(CABLE_CONNECTOR_MAX)
),
)
cable_positions = ArrayField(
base_field=models.PositiveSmallIntegerField(
validators=(
MinValueValidator(CABLE_POSITION_MIN),
MaxValueValidator(CABLE_POSITION_MAX)
)
),
blank=True,
null=True,
)
mark_connected = models.BooleanField(
verbose_name=_('mark connected'),
default=False,
@@ -210,18 +220,31 @@ class CabledObjectModel(models.Model):
raise ValidationError({
"cable_end": _("Must specify cable end (A or B) when attaching a cable.")
})
if self.cable_end and not self.cable:
raise ValidationError({
"cable_end": _("Cable end must not be set without a cable.")
})
if self.cable_position and not self.cable:
raise ValidationError({
"cable_position": _("Cable termination position must not be set without a cable.")
})
if self.mark_connected and self.cable:
raise ValidationError({
"mark_connected": _("Cannot mark as connected with a cable attached.")
})
if self.cable_connector and not self.cable_positions:
raise ValidationError({
"cable_positions": _("Must specify position(s) when specifying a cable connector.")
})
if self.cable_positions and not self.cable_connector:
raise ValidationError({
"cable_positions": _("Cable positions cannot be set without a cable connector.")
})
if self.mark_connected:
raise ValidationError({
"mark_connected": _("Cannot mark as connected with a cable attached.")
})
else:
if self.cable_end:
raise ValidationError({
"cable_end": _("Cable end must not be set without a cable.")
})
if self.cable_connector:
raise ValidationError({
"cable_connector": _("Cable connector must not be set without a cable.")
})
if self.cable_positions:
raise ValidationError({
"cable_positions": _("Cable termination positions must not be set without a cable.")
})
@property
def link(self):
@@ -256,6 +279,22 @@ class CabledObjectModel(models.Model):
return None
return CableEndChoices.SIDE_A if self.cable_end == CableEndChoices.SIDE_B else CableEndChoices.SIDE_B
def set_cable_termination(self, termination):
"""Save attributes from the given CableTermination on the terminating object."""
self.cable = termination.cable
self.cable_end = termination.cable_end
self.cable_connector = termination.connector
self.cable_positions = termination.positions
set_cable_termination.alters_data = True
def clear_cable_termination(self, termination):
"""Clear all cable termination attributes from the terminating object."""
self.cable = None
self.cable_end = None
self.cable_connector = None
self.cable_positions = None
clear_cable_termination.alters_data = True
class PathEndpoint(models.Model):
"""

View File

@@ -2586,7 +2586,7 @@ class CableTest(APIViewTestCases.APIViewTestCase):
'object_id': interfaces[14].pk,
}],
'label': 'Cable 4',
'profile': CableProfileChoices.STRAIGHT_SINGLE,
'profile': CableProfileChoices.SINGLE_1C1P,
},
{
'a_terminations': [{
@@ -2598,7 +2598,7 @@ class CableTest(APIViewTestCases.APIViewTestCase):
'object_id': interfaces[15].pk,
}],
'label': 'Cable 5',
'profile': CableProfileChoices.STRAIGHT_SINGLE,
'profile': CableProfileChoices.SINGLE_1C1P,
},
{
'a_terminations': [{
@@ -2620,7 +2620,9 @@ class CableTerminationTest(
APIViewTestCases.ListObjectsViewTestCase,
):
model = CableTermination
brief_fields = ['cable', 'cable_end', 'display', 'id', 'position', 'termination_id', 'termination_type', 'url']
brief_fields = [
'cable', 'cable_end', 'connector', 'display', 'id', 'positions', 'termination_id', 'termination_type', 'url',
]
@classmethod
def setUpTestData(cls):

File diff suppressed because it is too large Load Diff

View File

@@ -3332,6 +3332,7 @@ class ModuleTestCase(TestCase, ChangeLoggedFilterSetTests):
class ConsolePortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests):
queryset = ConsolePort.objects.all()
filterset = ConsolePortFilterSet
ignore_fields = ('cable_positions',)
@classmethod
def setUpTestData(cls):
@@ -3582,6 +3583,7 @@ class ConsolePortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
class ConsoleServerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests):
queryset = ConsoleServerPort.objects.all()
filterset = ConsoleServerPortFilterSet
ignore_fields = ('cable_positions',)
@classmethod
def setUpTestData(cls):
@@ -3832,6 +3834,7 @@ class ConsoleServerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeL
class PowerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests):
queryset = PowerPort.objects.all()
filterset = PowerPortFilterSet
ignore_fields = ('cable_positions',)
@classmethod
def setUpTestData(cls):
@@ -4096,6 +4099,7 @@ class PowerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests):
queryset = PowerOutlet.objects.all()
filterset = PowerOutletFilterSet
ignore_fields = ('cable_positions',)
@classmethod
def setUpTestData(cls):
@@ -4380,7 +4384,7 @@ class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests):
queryset = Interface.objects.all()
filterset = InterfaceFilterSet
ignore_fields = ('tagged_vlans', 'untagged_vlan', 'qinq_svlan', 'vdcs')
ignore_fields = ('tagged_vlans', 'untagged_vlan', 'qinq_svlan', 'vdcs', 'cable_positions')
@classmethod
def setUpTestData(cls):
@@ -5017,6 +5021,7 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests):
queryset = FrontPort.objects.all()
filterset = FrontPortFilterSet
ignore_fields = ('cable_positions',)
@classmethod
def setUpTestData(cls):
@@ -5321,6 +5326,7 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
class RearPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests):
queryset = RearPort.objects.all()
filterset = RearPortFilterSet
ignore_fields = ('cable_positions',)
@classmethod
def setUpTestData(cls):
@@ -6859,6 +6865,7 @@ class PowerPanelTestCase(TestCase, ChangeLoggedFilterSetTests):
class PowerFeedTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = PowerFeed.objects.all()
filterset = PowerFeedFilterSet
ignore_fields = ('cable_positions',)
@classmethod
def setUpTestData(cls):

View File

@@ -11,6 +11,7 @@ from core.models import ObjectType
from dcim.choices import *
from dcim.constants import *
from dcim.models import *
from extras.models import ConfigTemplate
from ipam.models import ASN, RIR, VLAN, VRF
from netbox.choices import CSVDelimiterChoices, ImportFormatChoices, WeightUnitChoices
from tenancy.models import Tenant
@@ -2339,6 +2340,28 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
url = reverse('dcim:device_inventory', kwargs={'pk': device.pk})
self.assertHttpStatus(self.client.get(url), 200)
def test_device_renderconfig(self):
configtemplate = ConfigTemplate.objects.create(
name='Test Config Template',
template_code='Config for device {{ device.name }}'
)
device = Device.objects.first()
device.config_template = configtemplate
device.save()
url = reverse('dcim:device_render-config', kwargs={'pk': device.pk})
# User with only view permission should NOT be able to render config
self.add_permissions('dcim.view_device')
self.assertHttpStatus(self.client.get(url), 403)
# With render_config permission added should be able to render config
self.add_permissions('dcim.render_config_device')
self.assertHttpStatus(self.client.get(url), 200)
# With view permission removed should NOT be able to render config
self.remove_permissions('dcim.view_device')
self.assertHttpStatus(self.client.get(url), 403)
class ModuleTestCase(
# Module does not support bulk renaming (no name field) or

View File

@@ -41,12 +41,12 @@ def create_cablepaths(objects):
"""
from dcim.models import CablePath
# Arrange objects by cable position. All objects with a null position are grouped together.
# Arrange objects by cable connector. All objects with a null connector are grouped together.
origins = defaultdict(list)
for obj in objects:
origins[obj.cable_position].append(obj)
origins[obj.cable_connector].append(obj)
for position, objects in origins.items():
for connector, objects in origins.items():
if cp := CablePath.from_origin(objects):
cp.save()

View File

@@ -2682,6 +2682,7 @@ class DeviceConfigContextView(ObjectConfigContextView):
class DeviceRenderConfigView(ObjectRenderConfigView):
queryset = Device.objects.all()
base_template = 'dcim/device/base.html'
additional_permissions = ['dcim.render_config_device']
tab = ViewTab(
label=_('Render Config'),
weight=2100,

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -28,10 +28,10 @@
"bootstrap": "5.3.8",
"clipboard": "2.0.11",
"flatpickr": "4.6.13",
"gridstack": "12.3.3",
"gridstack": "12.4.1",
"htmx.org": "2.0.8",
"query-string": "9.3.1",
"sass": "1.95.0",
"sass": "1.97.0",
"tom-select": "2.4.3",
"typeface-inter": "3.18.1",
"typeface-roboto-mono": "1.1.13"
@@ -39,21 +39,21 @@
"devDependencies": {
"@eslint/compat": "^2.0.0",
"@eslint/eslintrc": "^3.3.3",
"@eslint/js": "^9.39.1",
"@eslint/js": "^9.39.2",
"@types/bootstrap": "5.2.10",
"@types/cookie": "^1.0.0",
"@types/node": "^24.10.1",
"@typescript-eslint/eslint-plugin": "^8.48.1",
"@typescript-eslint/parser": "^8.48.1",
"esbuild": "^0.27.0",
"esbuild": "^0.27.1",
"esbuild-sass-plugin": "^3.3.1",
"eslint": "^9.39.1",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-prettier": "^5.5.1",
"globals": "^16.5.0",
"prettier": "^3.7.3",
"prettier": "^3.7.4",
"typescript": "^5.9.3"
},
"resolutions": {

View File

@@ -24,135 +24,135 @@
dependencies:
tslib "^2.4.0"
"@esbuild/aix-ppc64@0.27.0":
version "0.27.0"
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz#1d8be43489a961615d49e037f1bfa0f52a773737"
integrity sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==
"@esbuild/aix-ppc64@0.27.1":
version "0.27.1"
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.1.tgz#116edcd62c639ed8ab551e57b38251bb28384de4"
integrity sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==
"@esbuild/android-arm64@0.27.0":
version "0.27.0"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz#bd1763194aad60753fa3338b1ba9bda974b58724"
integrity sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==
"@esbuild/android-arm64@0.27.1":
version "0.27.1"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.27.1.tgz#31c00d864c80f6de1900a11de8a506dbfbb27349"
integrity sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==
"@esbuild/android-arm@0.27.0":
version "0.27.0"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.27.0.tgz#69c7b57f02d3b3618a5ba4f82d127b57665dc397"
integrity sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==
"@esbuild/android-arm@0.27.1":
version "0.27.1"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.27.1.tgz#d2b73ab0ba894923a1d1378fd4b15cc20985f436"
integrity sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==
"@esbuild/android-x64@0.27.0":
version "0.27.0"
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.27.0.tgz#6ea22b5843acb23243d0126c052d7d3b6a11ca90"
integrity sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==
"@esbuild/android-x64@0.27.1":
version "0.27.1"
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.27.1.tgz#d9f74d8278191317250cfe0c15a13f410540b122"
integrity sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==
"@esbuild/darwin-arm64@0.27.0":
version "0.27.0"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz#5ad7c02bc1b1a937a420f919afe40665ba14ad1e"
integrity sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==
"@esbuild/darwin-arm64@0.27.1":
version "0.27.1"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.1.tgz#baf6914b8c57ed9d41f9de54023aa3ff9b084680"
integrity sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==
"@esbuild/darwin-x64@0.27.0":
version "0.27.0"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz#48470c83c5fd6d1fc7c823c2c603aeee96e101c9"
integrity sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==
"@esbuild/darwin-x64@0.27.1":
version "0.27.1"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.27.1.tgz#64e37400795f780a76c858a118ff19681a64b4e0"
integrity sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==
"@esbuild/freebsd-arm64@0.27.0":
version "0.27.0"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz#d5a8effd8b0be7be613cd1009da34d629d4c2457"
integrity sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==
"@esbuild/freebsd-arm64@0.27.1":
version "0.27.1"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.1.tgz#6572f2f235933eee906e070dfaae54488ee60acd"
integrity sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==
"@esbuild/freebsd-x64@0.27.0":
version "0.27.0"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz#9bde638bda31aa244d6d64dbafafb41e6e799bcc"
integrity sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==
"@esbuild/freebsd-x64@0.27.1":
version "0.27.1"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.1.tgz#83105dba9cf6ac4f44336799446d7f75c8c3a1e1"
integrity sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==
"@esbuild/linux-arm64@0.27.0":
version "0.27.0"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz#96008c3a207d8ca495708db714c475ea5bf7e2af"
integrity sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==
"@esbuild/linux-arm64@0.27.1":
version "0.27.1"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.27.1.tgz#035ff647d4498bdf16eb2d82801f73b366477dfa"
integrity sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==
"@esbuild/linux-arm@0.27.0":
version "0.27.0"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz#9b47cb0f222e567af316e978c7f35307db97bc0e"
integrity sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==
"@esbuild/linux-arm@0.27.1":
version "0.27.1"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.27.1.tgz#3516c74d2afbe305582dbb546d60f7978a8ece7f"
integrity sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==
"@esbuild/linux-ia32@0.27.0":
version "0.27.0"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz#d1e1e38d406cbdfb8a49f4eca0c25bbc344e18cc"
integrity sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==
"@esbuild/linux-ia32@0.27.1":
version "0.27.1"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.27.1.tgz#788db5db8ecd3d75dd41c42de0fe8f1fd967a4a7"
integrity sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==
"@esbuild/linux-loong64@0.27.0":
version "0.27.0"
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz#c13bc6a53e3b69b76f248065bebee8415b44dfce"
integrity sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==
"@esbuild/linux-loong64@0.27.1":
version "0.27.1"
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.27.1.tgz#8211f08b146916a6302ec2b8f87ec0cc4b62c49e"
integrity sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==
"@esbuild/linux-mips64el@0.27.0":
version "0.27.0"
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz#05f8322eb0a96ce1bfbc59691abe788f71e2d217"
integrity sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==
"@esbuild/linux-mips64el@0.27.1":
version "0.27.1"
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.1.tgz#cc58586ea83b3f171e727a624e7883a1c3eb4c04"
integrity sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==
"@esbuild/linux-ppc64@0.27.0":
version "0.27.0"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz#6fc5e7af98b4fb0c6a7f0b73ba837ce44dc54980"
integrity sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==
"@esbuild/linux-ppc64@0.27.1":
version "0.27.1"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.1.tgz#632477bbd98175cf8e53a7c9952d17fb2d6d4115"
integrity sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==
"@esbuild/linux-riscv64@0.27.0":
version "0.27.0"
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz#508afa9f69a3f97368c0bf07dd894a04af39d86e"
integrity sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==
"@esbuild/linux-riscv64@0.27.1":
version "0.27.1"
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.1.tgz#35435a82435a8a750edf433b83ac0d10239ac3fe"
integrity sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==
"@esbuild/linux-s390x@0.27.0":
version "0.27.0"
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz#21fda656110ee242fc64f87a9e0b0276d4e4ec5b"
integrity sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==
"@esbuild/linux-s390x@0.27.1":
version "0.27.1"
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.27.1.tgz#172edd7086438edacd86c0e2ea25ac9dbb62aac5"
integrity sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==
"@esbuild/linux-x64@0.27.0":
version "0.27.0"
resolved "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz"
integrity sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==
"@esbuild/linux-x64@0.27.1":
version "0.27.1"
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.27.1.tgz#09c771de9e2d8169d5969adf298ae21581f08c7f"
integrity sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==
"@esbuild/netbsd-arm64@0.27.0":
version "0.27.0"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz#a0131159f4db6e490da35cc4bb51ef0d03b7848a"
integrity sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==
"@esbuild/netbsd-arm64@0.27.1":
version "0.27.1"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.1.tgz#475ac0ce7edf109a358b1669f67759de4bcbb7c4"
integrity sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==
"@esbuild/netbsd-x64@0.27.0":
version "0.27.0"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz#6f4877d7c2ba425a2b80e4330594e0b43caa2d7d"
integrity sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==
"@esbuild/netbsd-x64@0.27.1":
version "0.27.1"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.1.tgz#3c31603d592477dc43b63df1ae100000f7fb59d7"
integrity sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==
"@esbuild/openbsd-arm64@0.27.0":
version "0.27.0"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz#cbefbd4c2f375cebeb4f965945be6cf81331bd01"
integrity sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==
"@esbuild/openbsd-arm64@0.27.1":
version "0.27.1"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.1.tgz#482067c847665b10d66431e936d4bc5fa8025abf"
integrity sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==
"@esbuild/openbsd-x64@0.27.0":
version "0.27.0"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz#31fa9e8649fc750d7c2302c8b9d0e1547f57bc84"
integrity sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==
"@esbuild/openbsd-x64@0.27.1":
version "0.27.1"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.1.tgz#687a188c2b184e5b671c5f74a6cd6247c0718c52"
integrity sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==
"@esbuild/openharmony-arm64@0.27.0":
version "0.27.0"
resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz#03727780f1fdf606e7b56193693a715d9f1ee001"
integrity sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==
"@esbuild/openharmony-arm64@0.27.1":
version "0.27.1"
resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.1.tgz#9929ee7fa8c1db2f33ef4d86198018dac9c1744f"
integrity sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==
"@esbuild/sunos-x64@0.27.0":
version "0.27.0"
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz#866a35f387234a867ced35af8906dfffb073b9ff"
integrity sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==
"@esbuild/sunos-x64@0.27.1":
version "0.27.1"
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.27.1.tgz#94071a146f313e7394c6424af07b2b564f1f994d"
integrity sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==
"@esbuild/win32-arm64@0.27.0":
version "0.27.0"
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz#53de43a9629b8a34678f28cd56cc104db1b67abb"
integrity sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==
"@esbuild/win32-arm64@0.27.1":
version "0.27.1"
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.27.1.tgz#869fde72a3576fdf48824085d05493fceebe395d"
integrity sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==
"@esbuild/win32-ia32@0.27.0":
version "0.27.0"
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz#924d2aed8692fea5d27bfb6500f9b8b9c1a34af4"
integrity sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==
"@esbuild/win32-ia32@0.27.1":
version "0.27.1"
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.27.1.tgz#31d7585893ed7b54483d0b8d87a4bfeba0ecfff5"
integrity sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==
"@esbuild/win32-x64@0.27.0":
version "0.27.0"
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz#64995295227e001f2940258617c6674efb3ac48d"
integrity sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==
"@esbuild/win32-x64@0.27.1":
version "0.27.1"
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.27.1.tgz#5efe5a112938b1180e98c76685ff9185cfa4f16e"
integrity sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==
"@eslint-community/eslint-utils@^4.7.0":
version "4.7.0"
@@ -230,10 +230,10 @@
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
"@eslint/js@9.39.1", "@eslint/js@^9.39.1":
version "9.39.1"
resolved "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz"
integrity sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==
"@eslint/js@9.39.2", "@eslint/js@^9.39.2":
version "9.39.2"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.39.2.tgz#2d4b8ec4c3ea13c1b3748e0c97ecd766bdd80599"
integrity sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==
"@eslint/object-schema@^2.1.7":
version "2.1.7"
@@ -1814,37 +1814,37 @@ esbuild-sass-plugin@^3.3.1:
safe-identifier "^0.4.2"
sass "^1.71.1"
esbuild@^0.27.0:
version "0.27.0"
resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz"
integrity sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==
esbuild@^0.27.1:
version "0.27.1"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.27.1.tgz#56bf43e6a4b4d2004642ec7c091b78de02b0831a"
integrity sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==
optionalDependencies:
"@esbuild/aix-ppc64" "0.27.0"
"@esbuild/android-arm" "0.27.0"
"@esbuild/android-arm64" "0.27.0"
"@esbuild/android-x64" "0.27.0"
"@esbuild/darwin-arm64" "0.27.0"
"@esbuild/darwin-x64" "0.27.0"
"@esbuild/freebsd-arm64" "0.27.0"
"@esbuild/freebsd-x64" "0.27.0"
"@esbuild/linux-arm" "0.27.0"
"@esbuild/linux-arm64" "0.27.0"
"@esbuild/linux-ia32" "0.27.0"
"@esbuild/linux-loong64" "0.27.0"
"@esbuild/linux-mips64el" "0.27.0"
"@esbuild/linux-ppc64" "0.27.0"
"@esbuild/linux-riscv64" "0.27.0"
"@esbuild/linux-s390x" "0.27.0"
"@esbuild/linux-x64" "0.27.0"
"@esbuild/netbsd-arm64" "0.27.0"
"@esbuild/netbsd-x64" "0.27.0"
"@esbuild/openbsd-arm64" "0.27.0"
"@esbuild/openbsd-x64" "0.27.0"
"@esbuild/openharmony-arm64" "0.27.0"
"@esbuild/sunos-x64" "0.27.0"
"@esbuild/win32-arm64" "0.27.0"
"@esbuild/win32-ia32" "0.27.0"
"@esbuild/win32-x64" "0.27.0"
"@esbuild/aix-ppc64" "0.27.1"
"@esbuild/android-arm" "0.27.1"
"@esbuild/android-arm64" "0.27.1"
"@esbuild/android-x64" "0.27.1"
"@esbuild/darwin-arm64" "0.27.1"
"@esbuild/darwin-x64" "0.27.1"
"@esbuild/freebsd-arm64" "0.27.1"
"@esbuild/freebsd-x64" "0.27.1"
"@esbuild/linux-arm" "0.27.1"
"@esbuild/linux-arm64" "0.27.1"
"@esbuild/linux-ia32" "0.27.1"
"@esbuild/linux-loong64" "0.27.1"
"@esbuild/linux-mips64el" "0.27.1"
"@esbuild/linux-ppc64" "0.27.1"
"@esbuild/linux-riscv64" "0.27.1"
"@esbuild/linux-s390x" "0.27.1"
"@esbuild/linux-x64" "0.27.1"
"@esbuild/netbsd-arm64" "0.27.1"
"@esbuild/netbsd-x64" "0.27.1"
"@esbuild/openbsd-arm64" "0.27.1"
"@esbuild/openbsd-x64" "0.27.1"
"@esbuild/openharmony-arm64" "0.27.1"
"@esbuild/sunos-x64" "0.27.1"
"@esbuild/win32-arm64" "0.27.1"
"@esbuild/win32-ia32" "0.27.1"
"@esbuild/win32-x64" "0.27.1"
escape-string-regexp@^4.0.0:
version "4.0.0"
@@ -1944,10 +1944,10 @@ eslint-visitor-keys@^4.2.1:
resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz"
integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==
eslint@^9.39.1:
version "9.39.1"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.39.1.tgz#be8bf7c6de77dcc4252b5a8dcb31c2efff74a6e5"
integrity sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==
eslint@^9.39.2:
version "9.39.2"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.39.2.tgz#cb60e6d16ab234c0f8369a3fe7cc87967faf4b6c"
integrity sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==
dependencies:
"@eslint-community/eslint-utils" "^4.8.0"
"@eslint-community/regexpp" "^4.12.1"
@@ -1955,7 +1955,7 @@ eslint@^9.39.1:
"@eslint/config-helpers" "^0.4.2"
"@eslint/core" "^0.17.0"
"@eslint/eslintrc" "^3.3.1"
"@eslint/js" "9.39.1"
"@eslint/js" "9.39.2"
"@eslint/plugin-kit" "^0.4.1"
"@humanfs/node" "^0.16.6"
"@humanwhocodes/module-importer" "^1.0.1"
@@ -2304,10 +2304,10 @@ graphql@16.12.0:
resolved "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz"
integrity sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==
gridstack@12.3.3:
version "12.3.3"
resolved "https://registry.npmjs.org/gridstack/-/gridstack-12.3.3.tgz"
integrity sha512-Bboi4gj7HXGnx1VFXQNde4Nwi5srdUSuCCnOSszKhFjBs8EtMEWhsKX02BjIKkErq/FjQUkNUbXUYeQaVMQ0jQ==
gridstack@12.4.1:
version "12.4.1"
resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-12.4.1.tgz#4a44511e5da33016e731f00bee279bed550d4ab9"
integrity sha512-dYBNVEDw2zwnz0bCDouHk8rMclrMoMn4r6rtNyyWSeYsV3RF8QV2KFRTj4c86T2FsZPr3iQv+/LD/ae29FcpHQ==
has-bigints@^1.0.1, has-bigints@^1.0.2:
version "1.0.2"
@@ -3061,10 +3061,10 @@ prettier-linter-helpers@^1.0.0:
dependencies:
fast-diff "^1.1.2"
prettier@^3.7.3:
version "3.7.3"
resolved "https://registry.npmjs.org/prettier/-/prettier-3.7.3.tgz"
integrity sha512-QgODejq9K3OzoBbuyobZlUhznP5SKwPqp+6Q6xw6o8gnhr4O85L2U915iM2IDcfF2NPXVaM9zlo9tdwipnYwzg==
prettier@^3.7.4:
version "3.7.4"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.7.4.tgz#d2f8335d4b1cec47e1c8098645411b0c9dff9c0f"
integrity sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==
punycode.js@^2.3.1:
version "2.3.1"
@@ -3251,10 +3251,10 @@ safe-regex-test@^1.1.0:
es-errors "^1.3.0"
is-regex "^1.2.1"
sass@1.95.0:
version "1.95.0"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.95.0.tgz#3a3a4d4d954313ab50eaf16f6e2548a2f6ec0811"
integrity sha512-9QMjhLq+UkOg/4bb8Lt8A+hJZvY3t+9xeZMKSBtBEgxrXA3ed5Ts4NDreUkYgJP1BTmrscQE/xYhf7iShow6lw==
sass@1.97.0:
version "1.97.0"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.97.0.tgz#8ed65df5e2f73012d5ef0e98837ff63550657ab2"
integrity sha512-KR0igP1z4avUJetEuIeOdDlwaUDvkH8wSx7FdSjyYBS3dpyX3TzHfAMO0G1Q4/3cdjcmi3r7idh+KCmKqS+KeQ==
dependencies:
chokidar "^4.0.0"
immutable "^5.0.2"

View File

@@ -1,3 +1,4 @@
version: "4.4.8"
version: "4.5.0"
edition: "Community"
published: "2025-12-09"
published: "2025-12-16"
designation: "beta1"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -67,6 +67,16 @@ class TestCase(_TestCase):
obj_perm.users.add(self.user)
obj_perm.object_types.add(object_type)
def remove_permissions(self, *names):
"""
Remove a set of permissions from the test user. Accepts permission names in the form <app>.<action>_<model>.
"""
for name in names:
object_type, action = resolve_permission_type(name)
ObjectPermission.objects.filter(
actions__contains=[action], object_types=object_type, users=self.user
).delete()
#
# Custom assertions
#

View File

@@ -4,6 +4,7 @@ from django.urls import reverse
from dcim.choices import InterfaceModeChoices
from dcim.models import DeviceRole, Platform, Site
from extras.models import ConfigTemplate
from ipam.models import VLAN, VRF
from utilities.testing import ViewTestCases, create_tags, create_test_device, create_test_virtualmachine
from virtualization.choices import *
@@ -326,6 +327,28 @@ class VirtualMachineTestCase(ViewTestCases.PrimaryObjectViewTestCase):
url = reverse('virtualization:virtualmachine_interfaces', kwargs={'pk': virtualmachine.pk})
self.assertHttpStatus(self.client.get(url), 200)
def test_virtualmachine_renderconfig(self):
configtemplate = ConfigTemplate.objects.create(
name='Test Config Template',
template_code='Config for VM {{ virtualmachine.name }}'
)
vm = VirtualMachine.objects.first()
vm.config_template = configtemplate
vm.save()
url = reverse('virtualization:virtualmachine_render-config', kwargs={'pk': vm.pk})
# User with only view permission should NOT be able to render config
self.add_permissions('virtualization.view_virtualmachine')
self.assertHttpStatus(self.client.get(url), 403)
# With render_config permission added should be able to render config
self.add_permissions('virtualization.render_config_virtualmachine')
self.assertHttpStatus(self.client.get(url), 200)
# With view permission removed should NOT be able to render config
self.remove_permissions('virtualization.view_virtualmachine')
self.assertHttpStatus(self.client.get(url), 403)
class VMInterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
model = VMInterface

View File

@@ -405,6 +405,7 @@ class VirtualMachineConfigContextView(ObjectConfigContextView):
class VirtualMachineRenderConfigView(ObjectRenderConfigView):
queryset = VirtualMachine.objects.all()
base_template = 'virtualization/virtualmachine/base.html'
additional_permissions = ['virtualization.render_config_virtualmachine']
tab = ViewTab(
label=_('Render Config'),
weight=2100,

View File

@@ -3,8 +3,8 @@
[project]
name = "netbox"
version = "4.4.7"
requires-python = ">=3.10"
version = "4.5.0-beta1"
requires-python = ">=3.12"
description = "The premier source of truth powering network automation."
readme = "README.md"
license = "Apache-2.0"

View File

@@ -36,8 +36,8 @@ rq==2.6.1
social-auth-app-django==5.6.0
social-auth-core==4.8.1
sorl-thumbnail==12.11.0
strawberry-graphql==0.287.2
strawberry-graphql==0.287.3
strawberry-graphql-django==0.70.1
svgwrite==1.4.3
tablib==3.9.0
tzdata==2025.2
tzdata==2025.3