mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-22 12:06:53 -06:00
Merge branch 'develop-2.4' into 1898-activity-logging
This commit is contained in:
commit
9d419de9dc
@ -1,7 +1,5 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from collections import OrderedDict
|
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.validators import UniqueTogetherValidator
|
from rest_framework.validators import UniqueTogetherValidator
|
||||||
from taggit.models import Tag
|
from taggit.models import Tag
|
||||||
@ -63,9 +61,9 @@ class SiteSerializer(CustomFieldModelSerializer):
|
|||||||
model = Site
|
model = Site
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description',
|
'id', 'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description',
|
||||||
'physical_address', 'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'comments',
|
'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone',
|
||||||
'tags', 'custom_fields', 'created', 'last_updated', 'count_prefixes', 'count_vlans', 'count_racks',
|
'contact_email', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'count_prefixes',
|
||||||
'count_devices', 'count_circuits',
|
'count_vlans', 'count_racks', 'count_devices', 'count_circuits',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -115,8 +115,8 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
model = Site
|
model = Site
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'slug', 'status', 'region', 'tenant_group', 'tenant', 'facility', 'asn', 'time_zone', 'description',
|
'name', 'slug', 'status', 'region', 'tenant_group', 'tenant', 'facility', 'asn', 'time_zone', 'description',
|
||||||
'physical_address', 'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'comments',
|
'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone',
|
||||||
'tags',
|
'contact_email', 'comments', 'tags',
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'physical_address': SmallTextarea(attrs={'rows': 3}),
|
'physical_address': SmallTextarea(attrs={'rows': 3}),
|
||||||
@ -129,7 +129,9 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
'time_zone': "Local time zone",
|
'time_zone': "Local time zone",
|
||||||
'description': "Short description (will appear in sites list)",
|
'description': "Short description (will appear in sites list)",
|
||||||
'physical_address': "Physical location of the building (e.g. for GPS)",
|
'physical_address': "Physical location of the building (e.g. for GPS)",
|
||||||
'shipping_address': "If different from the physical address"
|
'shipping_address': "If different from the physical address",
|
||||||
|
'latitude': "Latitude in decimal format (xx.yyyyyy)",
|
||||||
|
'longitude': "Longitude in decimal format (xx.yyyyyy)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
25
netbox/dcim/migrations/0059_site_latitude_longitude.py
Normal file
25
netbox/dcim/migrations/0059_site_latitude_longitude.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.12 on 2018-06-21 18:45
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0058_relax_rack_naming_constraints'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='site',
|
||||||
|
name='latitude',
|
||||||
|
field=models.DecimalField(blank=True, decimal_places=6, max_digits=8, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='site',
|
||||||
|
name='longitude',
|
||||||
|
field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True),
|
||||||
|
),
|
||||||
|
]
|
@ -8,7 +8,7 @@ from django.db import migrations, models
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('dcim', '0058_relax_rack_naming_constraints'),
|
('dcim', '0059_site_latitude_longitude'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
@ -136,6 +136,18 @@ class Site(ChangeLoggedModel, CustomFieldModel):
|
|||||||
max_length=200,
|
max_length=200,
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
|
latitude = models.DecimalField(
|
||||||
|
max_digits=8,
|
||||||
|
decimal_places=6,
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
longitude = models.DecimalField(
|
||||||
|
max_digits=9,
|
||||||
|
decimal_places=6,
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
contact_name = models.CharField(
|
contact_name = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
blank=True
|
blank=True
|
||||||
@ -166,7 +178,7 @@ class Site(ChangeLoggedModel, CustomFieldModel):
|
|||||||
serializer = 'dcim.api.serializers.SiteSerializer'
|
serializer = 'dcim.api.serializers.SiteSerializer'
|
||||||
csv_headers = [
|
csv_headers = [
|
||||||
'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'physical_address',
|
'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'physical_address',
|
||||||
'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'comments',
|
'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone', 'contact_email', 'comments',
|
||||||
]
|
]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -191,6 +203,8 @@ class Site(ChangeLoggedModel, CustomFieldModel):
|
|||||||
self.description,
|
self.description,
|
||||||
self.physical_address,
|
self.physical_address,
|
||||||
self.shipping_address,
|
self.shipping_address,
|
||||||
|
self.latitude,
|
||||||
|
self.longitude,
|
||||||
self.contact_name,
|
self.contact_name,
|
||||||
self.contact_phone,
|
self.contact_phone,
|
||||||
self.contact_email,
|
self.contact_email,
|
||||||
|
@ -3,11 +3,11 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
# Models which support custom fields
|
# Models which support custom fields
|
||||||
CUSTOMFIELD_MODELS = (
|
CUSTOMFIELD_MODELS = (
|
||||||
'provider', 'circuit', # Circuits
|
'provider', 'circuit', # Circuits
|
||||||
'site', 'rack', 'devicetype', 'device', # DCIM
|
'site', 'rack', 'devicetype', 'device', # DCIM
|
||||||
'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', # IPAM
|
'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', 'service', # IPAM
|
||||||
'tenant', # Tenancy
|
'tenant', # Tenancy
|
||||||
'cluster', 'virtualmachine', # Virtualization
|
'cluster', 'virtualmachine', # Virtualization
|
||||||
)
|
)
|
||||||
|
|
||||||
# Custom field types
|
# Custom field types
|
||||||
|
@ -301,7 +301,7 @@ class AvailableIPSerializer(serializers.Serializer):
|
|||||||
# Services
|
# Services
|
||||||
#
|
#
|
||||||
|
|
||||||
class ServiceSerializer(ValidatedModelSerializer):
|
class ServiceSerializer(CustomFieldModelSerializer):
|
||||||
device = NestedDeviceSerializer(required=False, allow_null=True)
|
device = NestedDeviceSerializer(required=False, allow_null=True)
|
||||||
virtual_machine = NestedVirtualMachineSerializer(required=False, allow_null=True)
|
virtual_machine = NestedVirtualMachineSerializer(required=False, allow_null=True)
|
||||||
protocol = ChoiceFieldSerializer(choices=IP_PROTOCOL_CHOICES)
|
protocol = ChoiceFieldSerializer(choices=IP_PROTOCOL_CHOICES)
|
||||||
@ -315,6 +315,6 @@ class ServiceSerializer(ValidatedModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Service
|
model = Service
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'device', 'virtual_machine', 'name', 'port', 'protocol', 'ipaddresses', 'description', 'created',
|
'id', 'device', 'virtual_machine', 'name', 'port', 'protocol', 'ipaddresses', 'description',
|
||||||
'last_updated',
|
'custom_fields', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
@ -426,6 +426,10 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
|
|
||||||
|
|
||||||
class ServiceFilter(django_filters.FilterSet):
|
class ServiceFilter(django_filters.FilterSet):
|
||||||
|
q = django_filters.CharFilter(
|
||||||
|
method='search',
|
||||||
|
label='Search',
|
||||||
|
)
|
||||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
label='Device (ID)',
|
label='Device (ID)',
|
||||||
@ -450,3 +454,9 @@ class ServiceFilter(django_filters.FilterSet):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Service
|
model = Service
|
||||||
fields = ['name', 'protocol', 'port']
|
fields = ['name', 'protocol', 'port']
|
||||||
|
|
||||||
|
def search(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
|
qs_filter = Q(name__icontains=value) | Q(description__icontains=value)
|
||||||
|
return queryset.filter(qs_filter)
|
||||||
|
@ -2,6 +2,7 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.exceptions import MultipleObjectsReturned
|
from django.core.exceptions import MultipleObjectsReturned
|
||||||
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from taggit.forms import TagField
|
from taggit.forms import TagField
|
||||||
|
|
||||||
@ -15,7 +16,9 @@ from utilities.forms import (
|
|||||||
SlugField, add_blank_choice,
|
SlugField, add_blank_choice,
|
||||||
)
|
)
|
||||||
from virtualization.models import VirtualMachine
|
from virtualization.models import VirtualMachine
|
||||||
from .constants import IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES
|
from .constants import (
|
||||||
|
IP_PROTOCOL_CHOICES, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES,
|
||||||
|
)
|
||||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||||
|
|
||||||
IP_FAMILY_CHOICES = [
|
IP_FAMILY_CHOICES = [
|
||||||
@ -914,7 +917,7 @@ class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
# Services
|
# Services
|
||||||
#
|
#
|
||||||
|
|
||||||
class ServiceForm(BootstrapMixin, forms.ModelForm):
|
class ServiceForm(BootstrapMixin, CustomFieldForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Service
|
model = Service
|
||||||
@ -940,3 +943,28 @@ class ServiceForm(BootstrapMixin, forms.ModelForm):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.fields['ipaddresses'].choices = []
|
self.fields['ipaddresses'].choices = []
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
|
model = Service
|
||||||
|
q = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
label='Search'
|
||||||
|
)
|
||||||
|
protocol = forms.ChoiceField(
|
||||||
|
choices=add_blank_choice(IP_PROTOCOL_CHOICES),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
port = forms.IntegerField(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(queryset=Service.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
|
protocol = forms.ChoiceField(choices=add_blank_choice(IP_PROTOCOL_CHOICES), required=False)
|
||||||
|
port = forms.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(65535)], required=False)
|
||||||
|
description = forms.CharField(max_length=100, required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ['site', 'group', 'tenant', 'role', 'description']
|
||||||
|
@ -831,7 +831,7 @@ class VLAN(ChangeLoggedModel, CustomFieldModel):
|
|||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class Service(ChangeLoggedModel):
|
class Service(ChangeLoggedModel, CustomFieldModel):
|
||||||
"""
|
"""
|
||||||
A Service represents a layer-four service (e.g. HTTP or SSH) running on a Device or VirtualMachine. A Service may
|
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.
|
optionally be tied to one or more specific IPAddresses belonging to its parent.
|
||||||
@ -871,6 +871,11 @@ class Service(ChangeLoggedModel):
|
|||||||
max_length=100,
|
max_length=100,
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
|
custom_field_values = GenericRelation(
|
||||||
|
to='extras.CustomFieldValue',
|
||||||
|
content_type_field='obj_type',
|
||||||
|
object_id_field='obj_id'
|
||||||
|
)
|
||||||
|
|
||||||
serializer = 'ipam.api.serializers.ServiceSerializer'
|
serializer = 'ipam.api.serializers.ServiceSerializer'
|
||||||
|
|
||||||
@ -880,6 +885,9 @@ class Service(ChangeLoggedModel):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '{} ({}/{})'.format(self.name, self.port, self.get_protocol_display())
|
return '{} ({}/{})'.format(self.name, self.port, self.get_protocol_display())
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('ipam:service', args=[self.pk])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def parent(self):
|
def parent(self):
|
||||||
return self.device or self.virtual_machine
|
return self.device or self.virtual_machine
|
||||||
|
@ -6,7 +6,7 @@ from django_tables2.utils import Accessor
|
|||||||
from dcim.models import Interface
|
from dcim.models import Interface
|
||||||
from tenancy.tables import COL_TENANT
|
from tenancy.tables import COL_TENANT
|
||||||
from utilities.tables import BaseTable, ToggleColumn
|
from utilities.tables import BaseTable, ToggleColumn
|
||||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF
|
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||||
|
|
||||||
RIR_UTILIZATION = """
|
RIR_UTILIZATION = """
|
||||||
<div class="progress">
|
<div class="progress">
|
||||||
@ -401,3 +401,19 @@ class VLANMemberTable(BaseTable):
|
|||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = ('parent', 'name', 'untagged', 'actions')
|
fields = ('parent', 'name', 'untagged', 'actions')
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Services
|
||||||
|
#
|
||||||
|
|
||||||
|
class ServiceTable(BaseTable):
|
||||||
|
pk = ToggleColumn()
|
||||||
|
name = tables.LinkColumn(
|
||||||
|
viewname='ipam:service',
|
||||||
|
args=[Accessor('pk')]
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta(BaseTable.Meta):
|
||||||
|
model = Service
|
||||||
|
fields = ('pk', 'name', 'parent', 'protocol', 'port', 'description')
|
||||||
|
@ -95,6 +95,10 @@ urlpatterns = [
|
|||||||
url(r'^vlans/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='vlan_changelog', kwargs={'model': VLAN}),
|
url(r'^vlans/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='vlan_changelog', kwargs={'model': VLAN}),
|
||||||
|
|
||||||
# Services
|
# Services
|
||||||
|
url(r'^services/$', views.ServiceListView.as_view(), name='service_list'),
|
||||||
|
url(r'^services/edit/$', views.ServiceBulkEditView.as_view(), name='service_bulk_edit'),
|
||||||
|
url(r'^services/delete/$', views.ServiceBulkDeleteView.as_view(), name='service_bulk_delete'),
|
||||||
|
url(r'^services/(?P<pk>\d+)/$', views.ServiceView.as_view(), name='service'),
|
||||||
url(r'^services/(?P<pk>\d+)/edit/$', views.ServiceEditView.as_view(), name='service_edit'),
|
url(r'^services/(?P<pk>\d+)/edit/$', views.ServiceEditView.as_view(), name='service_edit'),
|
||||||
url(r'^services/(?P<pk>\d+)/delete/$', views.ServiceDeleteView.as_view(), name='service_delete'),
|
url(r'^services/(?P<pk>\d+)/delete/$', views.ServiceDeleteView.as_view(), name='service_delete'),
|
||||||
url(r'^services/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='service_changelog', kwargs={'model': Service}),
|
url(r'^services/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='service_changelog', kwargs={'model': Service}),
|
||||||
|
@ -923,6 +923,25 @@ class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
# Services
|
# Services
|
||||||
#
|
#
|
||||||
|
|
||||||
|
class ServiceListView(ObjectListView):
|
||||||
|
queryset = Service.objects.select_related('device', 'virtual_machine')
|
||||||
|
filter = filters.ServiceFilter
|
||||||
|
filter_form = forms.ServiceFilterForm
|
||||||
|
table = tables.ServiceTable
|
||||||
|
template_name = 'ipam/service_list.html'
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceView(View):
|
||||||
|
|
||||||
|
def get(self, request, pk):
|
||||||
|
|
||||||
|
service = get_object_or_404(Service, pk=pk)
|
||||||
|
|
||||||
|
return render(request, 'ipam/service.html', {
|
||||||
|
'service': service,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
class ServiceCreateView(PermissionRequiredMixin, ObjectEditView):
|
class ServiceCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||||
permission_required = 'ipam.add_service'
|
permission_required = 'ipam.add_service'
|
||||||
model = Service
|
model = Service
|
||||||
@ -936,9 +955,6 @@ class ServiceCreateView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
obj.virtual_machine = get_object_or_404(VirtualMachine, pk=url_kwargs['virtualmachine'])
|
obj.virtual_machine = get_object_or_404(VirtualMachine, pk=url_kwargs['virtualmachine'])
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
def get_return_url(self, request, obj):
|
|
||||||
return obj.parent.get_absolute_url()
|
|
||||||
|
|
||||||
|
|
||||||
class ServiceEditView(ServiceCreateView):
|
class ServiceEditView(ServiceCreateView):
|
||||||
permission_required = 'ipam.change_service'
|
permission_required = 'ipam.change_service'
|
||||||
@ -947,3 +963,22 @@ class ServiceEditView(ServiceCreateView):
|
|||||||
class ServiceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
class ServiceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
permission_required = 'ipam.delete_service'
|
permission_required = 'ipam.delete_service'
|
||||||
model = Service
|
model = Service
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
|
permission_required = 'ipam.change_service'
|
||||||
|
cls = Service
|
||||||
|
queryset = Service.objects.all()
|
||||||
|
filter = filters.ServiceFilter
|
||||||
|
table = tables.ServiceTable
|
||||||
|
form = forms.ServiceBulkEditForm
|
||||||
|
default_return_url = 'ipam:service_list'
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
|
permission_required = 'ipam.delete_service'
|
||||||
|
cls = Service
|
||||||
|
queryset = Service.objects.all()
|
||||||
|
filter = filters.ServiceFilter
|
||||||
|
table = tables.ServiceTable
|
||||||
|
default_return_url = 'ipam:service_list'
|
||||||
|
@ -187,6 +187,16 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>GPS Coordinates</td>
|
||||||
|
<td>
|
||||||
|
{% if site.latitude and site.longitude %}
|
||||||
|
{{ site.latitude }}, {{ site.longitude }}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">N/A</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Contact Name</td>
|
<td>Contact Name</td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -27,6 +27,8 @@
|
|||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
{% render_field form.physical_address %}
|
{% render_field form.physical_address %}
|
||||||
{% render_field form.shipping_address %}
|
{% render_field form.shipping_address %}
|
||||||
|
{% render_field form.latitude %}
|
||||||
|
{% render_field form.longitude %}
|
||||||
{% render_field form.contact_name %}
|
{% render_field form.contact_name %}
|
||||||
{% render_field form.contact_phone %}
|
{% render_field form.contact_phone %}
|
||||||
{% render_field form.contact_email %}
|
{% render_field form.contact_email %}
|
||||||
|
@ -286,6 +286,11 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{% url 'ipam:vlangroup_list' %}">VLAN Groups</a>
|
<a href="{% url 'ipam:vlangroup_list' %}">VLAN Groups</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="divider"></li>
|
||||||
|
<li class="dropdown-header">Services</li>
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'ipam:service_list' %}">Services</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li class="dropdown{% if request.path|contains:'/virtualization/' %} active{% endif %}">
|
<li class="dropdown{% if request.path|contains:'/virtualization/' %} active{% endif %}">
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>{{ service.name }}</td>
|
<td>
|
||||||
|
<a href="{{ service.get_absolute_url }}">{{ service.name }}</a>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ service.get_protocol_display }}/{{ service.port }}
|
{{ service.get_protocol_display }}/{{ service.port }}
|
||||||
</td>
|
</td>
|
||||||
|
88
netbox/templates/ipam/service.html
Normal file
88
netbox/templates/ipam/service.html
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
{% extends '_base.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-8 col-md-9">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li><a href="{% url 'ipam:service_list' %}">Services</a></li>
|
||||||
|
<li><a href="{{ service.parent.get_absolute_url }}">{{ service.parent }}</a></li>
|
||||||
|
<li>{{ service }}</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-4 col-md-3">
|
||||||
|
<form action="{% url 'ipam:service_list' %}" method="get">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" name="q" class="form-control" placeholder="Search Services" />
|
||||||
|
<span class="input-group-btn">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<span class="fa fa-search" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if perms.dcim.change_service %}
|
||||||
|
<div class="pull-right">
|
||||||
|
<a href="{% url 'ipam:service_edit' pk=service.pk %}" class="btn btn-warning">
|
||||||
|
<span class="fa fa-pencil" aria-hidden="true"></span>
|
||||||
|
Edit this service
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<h1>{% block title %}{{ service }}{% endblock %}</h1>
|
||||||
|
{% include 'inc/created_updated.html' with obj=service %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<strong>Service</strong>
|
||||||
|
</div>
|
||||||
|
<table class="table table-hover panel-body attr-table">
|
||||||
|
<tr>
|
||||||
|
<td>Name</td>
|
||||||
|
<td>{{ service.name }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Parent</td>
|
||||||
|
<td>
|
||||||
|
<a href="{{ service.parent.get_absolute_url }}">{{ service.parent }}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Protocol</td>
|
||||||
|
<td>{{ service.get_protocol_display }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Port</td>
|
||||||
|
<td>{{ service.port }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>IP Addresses</td>
|
||||||
|
<td>
|
||||||
|
{% for ipaddress in service.ipaddresses.all %}
|
||||||
|
<a href="{{ ipaddress.get_absolute_url }}">{{ ipaddress }}</a><br />
|
||||||
|
{% empty %}
|
||||||
|
<span class="text-muted">None</span>
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Description</td>
|
||||||
|
<td>
|
||||||
|
{% if service.description %}
|
||||||
|
{{ service.description }}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">N/A</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% with service.get_custom_fields as custom_fields %}
|
||||||
|
{% include 'inc/custom_fields_panel.html' %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -32,4 +32,12 @@
|
|||||||
{% render_field form.description %}
|
{% render_field form.description %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% if form.custom_fields %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Custom Fields</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% render_custom_fields form %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
13
netbox/templates/ipam/service_list.html
Normal file
13
netbox/templates/ipam/service_list.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{% extends '_base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>{% block title %}Services{% endblock %}</h1>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-9">
|
||||||
|
{% include 'utilities/obj_table.html' with bulk_edit_url='ipam:service_bulk_edit' bulk_delete_url='ipam:service_bulk_delete' %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
{% include 'inc/search_panel.html' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
Loading…
Reference in New Issue
Block a user