mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-26 18:38:38 -06:00
Allow assigning Service to FHRP Group, in addition to Device and VirtualMachine
This commit is contained in:
parent
fc0acb020f
commit
60e8268882
@ -3,7 +3,7 @@ import yaml
|
||||
|
||||
from functools import cached_property
|
||||
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.files.storage import default_storage
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
@ -609,6 +609,12 @@ class Device(
|
||||
null=True,
|
||||
help_text=_("GPS coordinate in decimal format (xx.yyyyyy)")
|
||||
)
|
||||
services = GenericRelation(
|
||||
to='ipam.Service',
|
||||
content_type_field='parent_object_type',
|
||||
object_id_field='parent_object_id',
|
||||
related_query_name='devices',
|
||||
)
|
||||
|
||||
# Counter fields
|
||||
console_port_count = CounterCacheField(
|
||||
|
@ -1,9 +1,13 @@
|
||||
from dcim.api.serializers_.devices import DeviceSerializer
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from rest_framework import serializers
|
||||
|
||||
from dcim.models import Device
|
||||
from ipam.choices import *
|
||||
from ipam.models import IPAddress, Service, ServiceTemplate
|
||||
from ipam.models import IPAddress, FHRPGroup, Service, ServiceTemplate
|
||||
from netbox.api.fields import ChoiceField, SerializedPKRelatedField
|
||||
from netbox.api.serializers import NetBoxModelSerializer
|
||||
from virtualization.api.serializers_.virtualmachines import VirtualMachineSerializer
|
||||
from utilities.api import get_serializer_for_model
|
||||
from virtualization.models import VirtualMachine
|
||||
from .ip import IPAddressSerializer
|
||||
|
||||
__all__ = (
|
||||
@ -25,8 +29,9 @@ class ServiceTemplateSerializer(NetBoxModelSerializer):
|
||||
|
||||
|
||||
class ServiceSerializer(NetBoxModelSerializer):
|
||||
device = DeviceSerializer(nested=True, required=False, allow_null=True)
|
||||
virtual_machine = VirtualMachineSerializer(nested=True, required=False, allow_null=True)
|
||||
device = serializers.SerializerMethodField(read_only=True)
|
||||
virtual_machine = serializers.SerializerMethodField(read_only=True)
|
||||
fhrp_group = serializers.SerializerMethodField(read_only=True)
|
||||
protocol = ChoiceField(choices=ServiceProtocolChoices, required=False)
|
||||
ipaddresses = SerializedPKRelatedField(
|
||||
queryset=IPAddress.objects.all(),
|
||||
@ -39,7 +44,34 @@ class ServiceSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = Service
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'device', 'virtual_machine', 'name', 'protocol', 'ports',
|
||||
'ipaddresses', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'id', 'url', 'display_url', 'display', 'device', 'virtual_machine', 'fhrp_group', 'name',
|
||||
'protocol', 'ports', 'ipaddresses', 'description', 'comments', 'tags', 'custom_fields',
|
||||
'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'protocol', 'ports', 'description')
|
||||
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||
def get_parent(self, obj):
|
||||
if obj.parent is None:
|
||||
return None
|
||||
serializer = get_serializer_for_model(obj.parent)
|
||||
context = {'request': self.context['request']}
|
||||
return serializer(obj.parent, nested=True, context=context).data
|
||||
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||
def get_device(self, obj):
|
||||
if isinstance(obj.parent, Device):
|
||||
return self.get_parent(obj)
|
||||
return None
|
||||
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||
def get_virtual_machine(self, obj):
|
||||
if isinstance(obj.parent, VirtualMachine):
|
||||
return self.get_parent(obj)
|
||||
return None
|
||||
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||
def get_fhrp_group(self, obj):
|
||||
if isinstance(obj.parent, FHRPGroup):
|
||||
return self.get_parent(obj)
|
||||
return None
|
||||
|
@ -83,6 +83,12 @@ VLANGROUP_SCOPE_TYPES = (
|
||||
# Services
|
||||
#
|
||||
|
||||
SERVICE_ASSIGNMENT_MODELS = Q(
|
||||
Q(app_label='dcim', model='device') |
|
||||
Q(app_label='ipam', model='fhrpgroup') |
|
||||
Q(app_label='virtualization', model='virtualmachine')
|
||||
)
|
||||
|
||||
# 16-bit port number
|
||||
SERVICE_PORT_MIN = 1
|
||||
SERVICE_PORT_MAX = 65535
|
||||
|
@ -1150,26 +1150,36 @@ class ServiceTemplateFilterSet(NetBoxModelFilterSet):
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class ServiceFilterSet(ContactModelFilterSet, NetBoxModelFilterSet):
|
||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Device.objects.all(),
|
||||
label=_('Device (ID)'),
|
||||
)
|
||||
device = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device__name',
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
class ServiceFilterSet(NetBoxModelFilterSet):
|
||||
device = MultiValueCharFilter(
|
||||
method='filter_device',
|
||||
field_name='name',
|
||||
label=_('Device (name)'),
|
||||
)
|
||||
virtual_machine_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=VirtualMachine.objects.all(),
|
||||
device_id = MultiValueNumberFilter(
|
||||
method='filter_device',
|
||||
field_name='pk',
|
||||
label=_('Device (ID)'),
|
||||
)
|
||||
virtual_machine = MultiValueCharFilter(
|
||||
method='filter_virtual_machine',
|
||||
field_name='name',
|
||||
label=_('Virtual machine (name)'),
|
||||
)
|
||||
virtual_machine_id = MultiValueNumberFilter(
|
||||
method='filter_virtual_machine',
|
||||
field_name='pk',
|
||||
label=_('Virtual machine (ID)'),
|
||||
)
|
||||
virtual_machine = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='virtual_machine__name',
|
||||
queryset=VirtualMachine.objects.all(),
|
||||
to_field_name='name',
|
||||
label=_('Virtual machine (name)'),
|
||||
fhrp_group = MultiValueCharFilter(
|
||||
method='filter_fhrp_group',
|
||||
field_name='name',
|
||||
label=_('FHRP Group (name)'),
|
||||
)
|
||||
fhrp_group_id = MultiValueNumberFilter(
|
||||
method='filter_fhrp_group',
|
||||
field_name='pk',
|
||||
label=_('FHRP Group (ID)'),
|
||||
)
|
||||
ip_address_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='ipaddresses',
|
||||
@ -1197,6 +1207,33 @@ class ServiceFilterSet(ContactModelFilterSet, NetBoxModelFilterSet):
|
||||
qs_filter = Q(name__icontains=value) | Q(description__icontains=value)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
def filter_device(self, queryset, name, value):
|
||||
devices = Device.objects.filter(**{'{}__in'.format(name): value})
|
||||
if not devices.exists():
|
||||
return queryset.none()
|
||||
service_ids = []
|
||||
for device in devices:
|
||||
service_ids.extend(device.services.values_list('id', flat=True))
|
||||
return queryset.filter(id__in=service_ids)
|
||||
|
||||
def filter_fhrp_group(self, queryset, name, value):
|
||||
groups = FHRPGroup.objects.filter(**{'{}__in'.format(name): value})
|
||||
if not groups.exists():
|
||||
return queryset.none()
|
||||
service_ids = []
|
||||
for group in groups:
|
||||
service_ids.extend(group.services.values_list('id', flat=True))
|
||||
return queryset.filter(id__in=service_ids)
|
||||
|
||||
def filter_virtual_machine(self, queryset, name, value):
|
||||
virtual_machines = VirtualMachine.objects.filter(**{'{}__in'.format(name): value})
|
||||
if not virtual_machines.exists():
|
||||
return queryset.none()
|
||||
service_ids = []
|
||||
for vm in virtual_machines:
|
||||
service_ids.extend(vm.services.values_list('id', flat=True))
|
||||
return queryset.filter(id__in=service_ids)
|
||||
|
||||
|
||||
class PrimaryIPFilterSet(django_filters.FilterSet):
|
||||
"""
|
||||
|
@ -763,13 +763,19 @@ class ServiceForm(NetBoxModelForm):
|
||||
label=_('Device'),
|
||||
queryset=Device.objects.all(),
|
||||
required=False,
|
||||
selector=True
|
||||
selector=True,
|
||||
)
|
||||
virtual_machine = DynamicModelChoiceField(
|
||||
label=_('Virtual machine'),
|
||||
queryset=VirtualMachine.objects.all(),
|
||||
required=False,
|
||||
selector=True
|
||||
selector=True,
|
||||
)
|
||||
fhrp_group = DynamicModelChoiceField(
|
||||
label=_('FHRP Group'),
|
||||
queryset=FHRPGroup.objects.all(),
|
||||
required=False,
|
||||
selector=True,
|
||||
)
|
||||
ports = NumericArrayField(
|
||||
label=_('Ports'),
|
||||
@ -795,6 +801,7 @@ class ServiceForm(NetBoxModelForm):
|
||||
TabbedGroups(
|
||||
FieldSet('device', name=_('Device')),
|
||||
FieldSet('virtual_machine', name=_('Virtual Machine')),
|
||||
FieldSet('fhrp_group', name=_('FHRP Group')),
|
||||
),
|
||||
'name',
|
||||
InlineFields('protocol', 'ports', label=_('Port(s)')),
|
||||
@ -805,9 +812,42 @@ class ServiceForm(NetBoxModelForm):
|
||||
class Meta:
|
||||
model = Service
|
||||
fields = [
|
||||
'device', 'virtual_machine', 'name', 'protocol', 'ports', 'ipaddresses', 'description', 'comments', 'tags',
|
||||
'name', 'protocol', 'ports', 'ipaddresses', 'description', 'comments', 'tags',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
# Initialize helper selectors
|
||||
instance = kwargs.get('instance')
|
||||
initial = kwargs.get('initial', {}).copy()
|
||||
if instance:
|
||||
parent_type = type(instance.parent)
|
||||
if parent_type is Device:
|
||||
initial['device'] = instance.parent
|
||||
elif parent_type is VirtualMachine:
|
||||
initial['virtual_machine'] = instance.parent
|
||||
elif parent_type is FHRPGroup:
|
||||
initial['fhrp_group'] = instance.parent
|
||||
kwargs['initial'] = initial
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
selected_objects = [f for f in ('device', 'virtual_machine', 'fhrp_group') if self.cleaned_data[f]]
|
||||
if len(selected_objects) > 1:
|
||||
raise forms.ValidationError({
|
||||
field: _("A Service must be associated with exactly one device, virtual machine, or FHRP group.")
|
||||
for field in selected_objects
|
||||
})
|
||||
elif selected_objects:
|
||||
self.instance.parent = self.cleaned_data[selected_objects[0]]
|
||||
else:
|
||||
raise forms.ValidationError({
|
||||
'device': _("A service must be associated with a device, a virtual machine, or an FHRP group.")
|
||||
})
|
||||
|
||||
|
||||
class ServiceCreateForm(ServiceForm):
|
||||
service_template = DynamicModelChoiceField(
|
||||
@ -821,6 +861,7 @@ class ServiceCreateForm(ServiceForm):
|
||||
TabbedGroups(
|
||||
FieldSet('device', name=_('Device')),
|
||||
FieldSet('virtual_machine', name=_('Virtual Machine')),
|
||||
FieldSet('fhrp_group', name=_('FHRP Group')),
|
||||
),
|
||||
TabbedGroups(
|
||||
FieldSet('service_template', name=_('From Template')),
|
||||
@ -832,7 +873,7 @@ class ServiceCreateForm(ServiceForm):
|
||||
|
||||
class Meta(ServiceForm.Meta):
|
||||
fields = [
|
||||
'device', 'virtual_machine', 'service_template', 'name', 'protocol', 'ports', 'ipaddresses', 'description',
|
||||
'service_template', 'name', 'protocol', 'ports', 'ipaddresses', 'description',
|
||||
'comments', 'tags',
|
||||
]
|
||||
|
||||
|
@ -5,10 +5,12 @@ import strawberry_django
|
||||
|
||||
from circuits.graphql.types import ProviderType
|
||||
from dcim.graphql.types import SiteType
|
||||
from dcim.models import Device
|
||||
from extras.graphql.mixins import ContactsMixin
|
||||
from ipam import models
|
||||
from netbox.graphql.scalars import BigInt
|
||||
from netbox.graphql.types import BaseObjectType, NetBoxObjectType, OrganizationalObjectType
|
||||
from virtualization.models import VirtualMachine
|
||||
from .filters import *
|
||||
from .mixins import IPAddressesMixin
|
||||
|
||||
@ -247,11 +249,43 @@ class RouteTargetType(NetBoxObjectType):
|
||||
)
|
||||
class ServiceType(NetBoxObjectType, ContactsMixin):
|
||||
ports: List[int]
|
||||
device: Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
virtual_machine: Annotated["VirtualMachineType", strawberry.lazy('virtualization.graphql.types')] | None
|
||||
|
||||
# device: Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
# virtual_machine: Annotated["VirtualMachineType", strawberry.lazy('virtualization.graphql.types')] | None
|
||||
# fhrp_group: Annotated["FHRPGroupType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
ipaddresses: List[Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')]]
|
||||
|
||||
@strawberry_django.field
|
||||
def device(self) -> Annotated[Union[
|
||||
Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')],
|
||||
], strawberry.union("ServiceAssignmentType")] | None:
|
||||
if isinstance(self.parent, Device):
|
||||
return self.parent
|
||||
return None
|
||||
|
||||
@strawberry_django.field
|
||||
def virtual_machine(self) -> Annotated[Union[
|
||||
Annotated["VirtualMachineType", strawberry.lazy('virtualization.graphql.types')],
|
||||
], strawberry.union("ServiceAssignmentType")] | None:
|
||||
if isinstance(self.parent, VirtualMachine):
|
||||
return self.parent
|
||||
return None
|
||||
|
||||
@strawberry_django.field
|
||||
def fhrp_group(self) -> Annotated[Union[
|
||||
Annotated["FHRPGroupType", strawberry.lazy('ipam.graphql.types')],
|
||||
], strawberry.union("ServiceAssignmentType")] | None:
|
||||
if isinstance(self.parent, models.FHRPGroup):
|
||||
return self.parent
|
||||
return None
|
||||
|
||||
@strawberry_django.field
|
||||
def parent(self) -> Annotated[Union[
|
||||
Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["VirtualMachineType", strawberry.lazy('virtualization.graphql.types')],
|
||||
Annotated["FHRPGroupType", strawberry.lazy('ipam.graphql.types')],
|
||||
], strawberry.union("ServiceParentType")] | None:
|
||||
return self.parent
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.ServiceTemplate,
|
||||
|
@ -0,0 +1,37 @@
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('ipam', '0077_vlangroup_tenant'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='service',
|
||||
name='parent_object_id',
|
||||
field=models.PositiveBigIntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='service',
|
||||
name='parent_object_type',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
limit_choices_to=models.Q(
|
||||
models.Q(
|
||||
models.Q(('app_label', 'dcim'), ('model', 'device')),
|
||||
models.Q(('app_label', 'ipam'), ('model', 'fhrpgroup')),
|
||||
models.Q(('app_label', 'virtualization'), ('model', 'virtualmachine')),
|
||||
_connector='OR'
|
||||
)
|
||||
),
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='+',
|
||||
to='contenttypes.contenttype'
|
||||
),
|
||||
),
|
||||
]
|
54
netbox/ipam/migrations/0079_populate_service_parent.py
Normal file
54
netbox/ipam/migrations/0079_populate_service_parent.py
Normal file
@ -0,0 +1,54 @@
|
||||
from django.db import migrations
|
||||
from django.db.models import F
|
||||
|
||||
|
||||
def populate_service_parent_gfk(apps, schema_config):
|
||||
Service = apps.get_model('ipam', 'Service')
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
Device = apps.get_model('dcim', 'device')
|
||||
VirtualMachine = apps.get_model('virtualization', 'virtualmachine')
|
||||
|
||||
Service.objects.filter(device_id__isnull=False).update(
|
||||
parent_object_type=ContentType.objects.get_for_model(Device),
|
||||
parent_object_id=F('device_id'),
|
||||
)
|
||||
|
||||
Service.objects.filter(virtual_machine_id__isnull=False).update(
|
||||
parent_object_type=ContentType.objects.get_for_model(VirtualMachine),
|
||||
parent_object_id=F('virtual_machine_id'),
|
||||
)
|
||||
|
||||
|
||||
def repopulate_device_and_virtualmachine_relations(apps, schemaconfig):
|
||||
Service = apps.get_model('ipam', 'Service')
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
Device = apps.get_model('dcim', 'device')
|
||||
VirtualMachine = apps.get_model('virtualization', 'virtualmachine')
|
||||
|
||||
Service.objects.filter(
|
||||
parent_object_type=ContentType.objects.get_for_model(Device),
|
||||
).update(
|
||||
device_id=F('parent_object_id')
|
||||
)
|
||||
|
||||
Service.objects.filter(
|
||||
parent_object_type=ContentType.objects.get_for_model(VirtualMachine),
|
||||
).update(
|
||||
virtual_machine_id=F('parent_object_id')
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0202_location_comments_region_comments_sitegroup_comments'),
|
||||
('ipam', '0078_service_parent_object_id_service_parent_object_type'),
|
||||
('virtualization', '0048_populate_mac_addresses'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
populate_service_parent_gfk,
|
||||
reverse_code=repopulate_device_and_virtualmachine_relations,
|
||||
)
|
||||
]
|
@ -0,0 +1,19 @@
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ipam', '0079_populate_service_parent'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='service',
|
||||
name='device',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='service',
|
||||
name='virtual_machine',
|
||||
),
|
||||
]
|
@ -0,0 +1,19 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('extras', '0125_exporttemplate_file_name'),
|
||||
('ipam', '0080_remove_service_device_remove_service_virtual_machine'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name='service',
|
||||
index=models.Index(
|
||||
fields=['parent_object_type', 'parent_object_id'], name='ipam_servic_parent__563d2b_idx'
|
||||
),
|
||||
),
|
||||
]
|
@ -48,6 +48,12 @@ class FHRPGroup(PrimaryModel):
|
||||
object_id_field='assigned_object_id',
|
||||
related_query_name='fhrpgroup'
|
||||
)
|
||||
services = GenericRelation(
|
||||
to='ipam.Service',
|
||||
content_type_field='parent_object_type',
|
||||
object_id_field='parent_object_id',
|
||||
related_query_name='fhrpgroup',
|
||||
)
|
||||
|
||||
clone_fields = ('protocol', 'auth_type', 'auth_key', 'description')
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
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
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@ -64,21 +64,23 @@ class Service(ContactsMixin, ServiceBase, PrimaryModel):
|
||||
A Service represents a layer-four service (e.g. HTTP or SSH) running on a Device or VirtualMachine. A Service may
|
||||
optionally be tied to one or more specific IPAddresses belonging to its parent.
|
||||
"""
|
||||
device = models.ForeignKey(
|
||||
to='dcim.Device',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='services',
|
||||
verbose_name=_('device'),
|
||||
null=True,
|
||||
blank=True
|
||||
parent_object_type = models.ForeignKey(
|
||||
to='contenttypes.ContentType',
|
||||
limit_choices_to=SERVICE_ASSIGNMENT_MODELS,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='+',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
virtual_machine = models.ForeignKey(
|
||||
to='virtualization.VirtualMachine',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='services',
|
||||
null=True,
|
||||
blank=True
|
||||
parent_object_id = models.PositiveBigIntegerField(
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
parent = GenericForeignKey(
|
||||
ct_field='parent_object_type',
|
||||
fk_field='parent_object_id'
|
||||
)
|
||||
|
||||
name = models.CharField(
|
||||
max_length=100,
|
||||
verbose_name=_('name')
|
||||
@ -91,22 +93,12 @@ class Service(ContactsMixin, ServiceBase, PrimaryModel):
|
||||
help_text=_("The specific IP addresses (if any) to which this service is bound")
|
||||
)
|
||||
|
||||
clone_fields = ['protocol', 'ports', 'description', 'device', 'virtual_machine', 'ipaddresses', ]
|
||||
clone_fields = ['protocol', 'ports', 'description', 'parent', 'ipaddresses', ]
|
||||
|
||||
class Meta:
|
||||
indexes = (
|
||||
models.Index(fields=('parent_object_type', 'parent_object_id')),
|
||||
)
|
||||
ordering = ('protocol', 'ports', 'pk') # (protocol, port) may be non-unique
|
||||
verbose_name = _('service')
|
||||
verbose_name_plural = _('services')
|
||||
|
||||
@property
|
||||
def parent(self):
|
||||
return self.device or self.virtual_machine
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# A Service must belong to a Device *or* to a VirtualMachine
|
||||
if self.device and self.virtual_machine:
|
||||
raise ValidationError(_("A service cannot be associated with both a device and a virtual machine."))
|
||||
if not self.device and not self.virtual_machine:
|
||||
raise ValidationError(_("A service must be associated with either a device or a virtual machine."))
|
||||
|
@ -1065,9 +1065,9 @@ class ServiceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
interface = Interface.objects.create(device=device, name='Interface 1', type=InterfaceTypeChoices.TYPE_VIRTUAL)
|
||||
|
||||
services = (
|
||||
Service(device=device, name='Service 1', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[101]),
|
||||
Service(device=device, name='Service 2', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[102]),
|
||||
Service(device=device, name='Service 3', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[103]),
|
||||
Service(parent=device, name='Service 1', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[101]),
|
||||
Service(parent=device, name='Service 2', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[102]),
|
||||
Service(parent=device, name='Service 3', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[103]),
|
||||
)
|
||||
Service.objects.bulk_create(services)
|
||||
|
||||
@ -1125,13 +1125,13 @@ class ServiceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
request = {
|
||||
'path': self._get_url('add'),
|
||||
'data': {
|
||||
'device': device.pk,
|
||||
'parent': device.pk,
|
||||
'service_template': service_template.pk,
|
||||
},
|
||||
}
|
||||
self.assertHttpStatus(self.client.post(**request), 302)
|
||||
instance = self._get_queryset().order_by('pk').last()
|
||||
self.assertEqual(instance.device, device)
|
||||
self.assertEqual(instance.parent, device)
|
||||
self.assertEqual(instance.name, service_template.name)
|
||||
self.assertEqual(instance.protocol, service_template.protocol)
|
||||
self.assertEqual(instance.ports, service_template.ports)
|
||||
|
@ -1409,7 +1409,7 @@ class ServiceTemplateBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
@register_model_view(Service, 'list', path='', detail=False)
|
||||
class ServiceListView(generic.ObjectListView):
|
||||
queryset = Service.objects.prefetch_related('device', 'virtual_machine')
|
||||
queryset = Service.objects.prefetch_related('parent')
|
||||
filterset = filtersets.ServiceFilterSet
|
||||
filterset_form = forms.ServiceFilterForm
|
||||
table = tables.ServiceTable
|
||||
|
@ -126,6 +126,12 @@ class VirtualMachine(ContactsMixin, ImageAttachmentsMixin, RenderConfigMixin, Co
|
||||
blank=True,
|
||||
max_length=50
|
||||
)
|
||||
services = GenericRelation(
|
||||
to='ipam.Service',
|
||||
content_type_field='parent_object_type',
|
||||
object_id_field='parent_object_id',
|
||||
related_query_name='virtualmachines',
|
||||
)
|
||||
|
||||
# Counter fields
|
||||
interface_count = CounterCacheField(
|
||||
|
Loading…
Reference in New Issue
Block a user