mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-22 12:06:53 -06:00
Enabled services on virtual machines
This commit is contained in:
parent
85cb333a5d
commit
3bb0d523d3
@ -126,7 +126,7 @@ urlpatterns = [
|
|||||||
url(r'^devices/(?P<pk>\d+)/lldp-neighbors/$', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'),
|
url(r'^devices/(?P<pk>\d+)/lldp-neighbors/$', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'),
|
||||||
url(r'^devices/(?P<pk>\d+)/config/$', views.DeviceConfigView.as_view(), name='device_config'),
|
url(r'^devices/(?P<pk>\d+)/config/$', views.DeviceConfigView.as_view(), name='device_config'),
|
||||||
url(r'^devices/(?P<pk>\d+)/add-secret/$', secret_add, name='device_addsecret'),
|
url(r'^devices/(?P<pk>\d+)/add-secret/$', secret_add, name='device_addsecret'),
|
||||||
url(r'^devices/(?P<device>\d+)/services/assign/$', ServiceCreateView.as_view(), name='service_assign'),
|
url(r'^devices/(?P<device>\d+)/services/assign/$', ServiceCreateView.as_view(), name='device_service_assign'),
|
||||||
url(r'^devices/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}),
|
url(r'^devices/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}),
|
||||||
|
|
||||||
# Console ports
|
# Console ports
|
||||||
|
@ -12,6 +12,7 @@ from ipam.models import (
|
|||||||
)
|
)
|
||||||
from tenancy.api.serializers import NestedTenantSerializer
|
from tenancy.api.serializers import NestedTenantSerializer
|
||||||
from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer
|
from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer
|
||||||
|
from virtualization.api.serializers import NestedVirtualMachineSerializer
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -295,12 +296,13 @@ class AvailableIPSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
class ServiceSerializer(serializers.ModelSerializer):
|
class ServiceSerializer(serializers.ModelSerializer):
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
|
virtual_machine = NestedVirtualMachineSerializer()
|
||||||
protocol = ChoiceFieldSerializer(choices=IP_PROTOCOL_CHOICES)
|
protocol = ChoiceFieldSerializer(choices=IP_PROTOCOL_CHOICES)
|
||||||
ipaddresses = NestedIPAddressSerializer(many=True)
|
ipaddresses = NestedIPAddressSerializer(many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Service
|
model = Service
|
||||||
fields = ['id', 'device', 'name', 'port', 'protocol', 'ipaddresses', 'description']
|
fields = ['id', 'device', 'virtual_machine', 'name', 'port', 'protocol', 'ipaddresses', 'description']
|
||||||
|
|
||||||
|
|
||||||
# TODO: Figure out how to use model validation with ManyToManyFields. Calling clean() yields a ValueError.
|
# TODO: Figure out how to use model validation with ManyToManyFields. Calling clean() yields a ValueError.
|
||||||
@ -308,4 +310,4 @@ class WritableServiceSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Service
|
model = Service
|
||||||
fields = ['id', 'device', 'name', 'port', 'protocol', 'ipaddresses', 'description']
|
fields = ['id', 'device', 'virtual_machine', 'name', 'port', 'protocol', 'ipaddresses', 'description']
|
||||||
|
@ -861,5 +861,14 @@ class ServiceForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
super(ServiceForm, self).__init__(*args, **kwargs)
|
super(ServiceForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Limit IP address choices to those assigned to interfaces of the parent device
|
# Limit IP address choices to those assigned to interfaces of the parent device/VM
|
||||||
self.fields['ipaddresses'].queryset = IPAddress.objects.filter(interface__device=self.instance.device)
|
if self.instance.device:
|
||||||
|
self.fields['ipaddresses'].queryset = IPAddress.objects.filter(
|
||||||
|
interface__device=self.instance.device
|
||||||
|
)
|
||||||
|
elif self.instance.virtual_machine:
|
||||||
|
self.fields['ipaddresses'].queryset = IPAddress.objects.filter(
|
||||||
|
interface__virtual_machine=self.instance.virtual_machine
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.fields['ipaddresses'].choices = []
|
||||||
|
31
netbox/ipam/migrations/0019_virtualization.py
Normal file
31
netbox/ipam/migrations/0019_virtualization.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.4 on 2017-08-31 15:44
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('virtualization', '0001_virtualization'),
|
||||||
|
('ipam', '0018_remove_service_uniqueness_constraint'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='service',
|
||||||
|
options={'ordering': ['protocol', 'port']},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='service',
|
||||||
|
name='virtual_machine',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='services', to='virtualization.VirtualMachine'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='service',
|
||||||
|
name='device',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='services', to='dcim.Device', verbose_name='device'),
|
||||||
|
),
|
||||||
|
]
|
@ -588,20 +588,59 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class Service(CreatedUpdatedModel):
|
class Service(CreatedUpdatedModel):
|
||||||
"""
|
"""
|
||||||
A Service represents a layer-four service (e.g. HTTP or SSH) running on a Device. A Service may optionally be tied
|
A Service represents a layer-four service (e.g. HTTP or SSH) running on a Device or VirtualMachine. A Service may
|
||||||
to one or more specific IPAddresses belonging to the Device.
|
optionally be tied to one or more specific IPAddresses belonging to its parent.
|
||||||
"""
|
"""
|
||||||
device = models.ForeignKey('dcim.Device', related_name='services', on_delete=models.CASCADE, verbose_name='device')
|
device = models.ForeignKey(
|
||||||
name = models.CharField(max_length=30)
|
to='dcim.Device',
|
||||||
protocol = models.PositiveSmallIntegerField(choices=IP_PROTOCOL_CHOICES)
|
on_delete=models.CASCADE,
|
||||||
port = models.PositiveIntegerField(validators=[MinValueValidator(1), MaxValueValidator(65535)],
|
related_name='services',
|
||||||
verbose_name='Port number')
|
verbose_name='device',
|
||||||
ipaddresses = models.ManyToManyField('ipam.IPAddress', related_name='services', blank=True,
|
null=True,
|
||||||
verbose_name='IP addresses')
|
blank=True
|
||||||
description = models.CharField(max_length=100, blank=True)
|
)
|
||||||
|
virtual_machine = models.ForeignKey(
|
||||||
|
to='virtualization.VirtualMachine',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='services',
|
||||||
|
null=True,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=30
|
||||||
|
)
|
||||||
|
protocol = models.PositiveSmallIntegerField(
|
||||||
|
choices=IP_PROTOCOL_CHOICES
|
||||||
|
)
|
||||||
|
port = models.PositiveIntegerField(
|
||||||
|
validators=[MinValueValidator(1), MaxValueValidator(65535)],
|
||||||
|
verbose_name='Port number'
|
||||||
|
)
|
||||||
|
ipaddresses = models.ManyToManyField(
|
||||||
|
to='ipam.IPAddress',
|
||||||
|
related_name='services',
|
||||||
|
blank=True,
|
||||||
|
verbose_name='IP addresses'
|
||||||
|
)
|
||||||
|
description = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['device', 'protocol', 'port']
|
ordering = ['protocol', 'port']
|
||||||
|
|
||||||
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())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def parent(self):
|
||||||
|
return self.device or self.virtual_machine
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
|
||||||
|
# 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.")
|
||||||
|
@ -15,6 +15,7 @@ from utilities.paginator import EnhancedPaginator
|
|||||||
from utilities.views import (
|
from utilities.views import (
|
||||||
BulkCreateView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
BulkCreateView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||||
)
|
)
|
||||||
|
from virtualization.models import VirtualMachine
|
||||||
from . import filters, forms, tables
|
from . import filters, forms, tables
|
||||||
from .constants import IPADDRESS_ROLE_ANYCAST
|
from .constants import IPADDRESS_ROLE_ANYCAST
|
||||||
from .models import (
|
from .models import (
|
||||||
@ -838,10 +839,12 @@ class ServiceCreateView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
def alter_obj(self, obj, request, url_args, url_kwargs):
|
def alter_obj(self, obj, request, url_args, url_kwargs):
|
||||||
if 'device' in url_kwargs:
|
if 'device' in url_kwargs:
|
||||||
obj.device = get_object_or_404(Device, pk=url_kwargs['device'])
|
obj.device = get_object_or_404(Device, pk=url_kwargs['device'])
|
||||||
|
elif 'virtualmachine' in url_kwargs:
|
||||||
|
obj.virtual_machine = get_object_or_404(VirtualMachine, pk=url_kwargs['virtualmachine'])
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
def get_return_url(self, request, obj):
|
def get_return_url(self, request, obj):
|
||||||
return obj.device.get_absolute_url()
|
return obj.parent.get_absolute_url()
|
||||||
|
|
||||||
|
|
||||||
class ServiceEditView(ServiceCreateView):
|
class ServiceEditView(ServiceCreateView):
|
||||||
|
@ -196,7 +196,7 @@
|
|||||||
{% if services %}
|
{% if services %}
|
||||||
<table class="table table-hover panel-body">
|
<table class="table table-hover panel-body">
|
||||||
{% for service in services %}
|
{% for service in services %}
|
||||||
{% include 'dcim/inc/service.html' %}
|
{% include 'ipam/inc/service.html' %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
{% else %}
|
{% else %}
|
||||||
@ -206,7 +206,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.ipam.add_service %}
|
{% if perms.ipam.add_service %}
|
||||||
<div class="panel-footer text-right">
|
<div class="panel-footer text-right">
|
||||||
<a href="{% url 'dcim:service_assign' device=device.pk %}" class="btn btn-xs btn-primary">
|
<a href="{% url 'dcim:device_service_assign' device=device.pk %}" class="btn btn-xs btn-primary">
|
||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Assign service
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Assign service
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -14,12 +14,12 @@
|
|||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
{% if perms.ipam.change_service %}
|
{% if perms.ipam.change_service %}
|
||||||
<a href="{% url 'ipam:service_edit' pk=service.pk %}" class="btn btn-info btn-xs" title="Edit service">
|
<a href="{% url 'ipam:service_edit' pk=service.pk %}" class="btn btn-info btn-xs" title="Edit service">
|
||||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-pencil"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.ipam.delete_service %}
|
{% if perms.ipam.delete_service %}
|
||||||
<a href="{% url 'ipam:service_delete' pk=service.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
<a href="{% url 'ipam:service_delete' pk=service.pk %}?return_url={{ service.parent.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||||
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete service"></i>
|
<i class="glyphicon glyphicon-trash" title="Delete service"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
@ -5,12 +5,21 @@
|
|||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading"><strong>Service</strong></div>
|
<div class="panel-heading"><strong>Service</strong></div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="form-group">
|
{% if obj.device %}
|
||||||
<label class="col-md-3 control-label">Device</label>
|
<div class="form-group">
|
||||||
<div class="col-md-9">
|
<label class="col-md-3 control-label required">Device</label>
|
||||||
<p class="form-control-static">{{ obj.device }}</p>
|
<div class="col-md-9">
|
||||||
|
<p class="form-control-static">{{ obj.device }}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{% else %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-md-3 control-label required">Virtual Machine</label>
|
||||||
|
<div class="col-md-9">
|
||||||
|
<p class="form-control-static">{{ obj.virtual_machine }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% render_field form.name %}
|
{% render_field form.name %}
|
||||||
<div class="form-group form-inline">
|
<div class="form-group form-inline">
|
||||||
<label class="col-md-3 control-label required">Port</label>
|
<label class="col-md-3 control-label required">Port</label>
|
||||||
|
@ -40,7 +40,7 @@
|
|||||||
{% if ip.description %}
|
{% if ip.description %}
|
||||||
<i class="fa fa-fw fa-comment-o" title="{{ ip.description }}"></i>
|
<i class="fa fa-fw fa-comment-o" title="{{ ip.description }}"></i>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if device.primary_ip4 == ip or device.primary_ip6 == ip %}
|
{% if vm.primary_ip4 == ip or vm.primary_ip6 == ip %}
|
||||||
<span class="label label-success">Primary</span>
|
<span class="label label-success">Primary</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
@ -56,12 +56,12 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
{% if perms.ipam.change_ipaddress %}
|
{% if perms.ipam.change_ipaddress %}
|
||||||
<a href="{% url 'ipam:ipaddress_edit' pk=ip.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-info btn-xs">
|
<a href="{% url 'ipam:ipaddress_edit' pk=ip.pk %}?return_url={{ vm.get_absolute_url }}" class="btn btn-info btn-xs">
|
||||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit IP address"></i>
|
<i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit IP address"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.ipam.delete_ipaddress %}
|
{% if perms.ipam.delete_ipaddress %}
|
||||||
<a href="{% url 'ipam:ipaddress_delete' pk=ip.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
<a href="{% url 'ipam:ipaddress_delete' pk=ip.pk %}?return_url={{ vm.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||||
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete IP address"></i>
|
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete IP address"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -158,6 +158,29 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<strong>Services</strong>
|
||||||
|
</div>
|
||||||
|
{% if services %}
|
||||||
|
<table class="table table-hover panel-body">
|
||||||
|
{% for service in services %}
|
||||||
|
{% include 'ipam/inc/service.html' %}
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<div class="panel-body text-muted">
|
||||||
|
None
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if perms.ipam.add_service %}
|
||||||
|
<div class="panel-footer text-right">
|
||||||
|
<a href="{% url 'virtualization:virtualmachine_service_assign' virtualmachine=vm.pk %}" class="btn btn-xs btn-primary">
|
||||||
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Assign service
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
{% include 'inc/custom_fields_panel.html' with custom_fields=vm.get_custom_fields %}
|
{% include 'inc/custom_fields_panel.html' with custom_fields=vm.get_custom_fields %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
|
@ -2,6 +2,7 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
|
from ipam.views import ServiceCreateView
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
|
|
||||||
@ -39,6 +40,7 @@ urlpatterns = [
|
|||||||
url(r'^virtual-machines/(?P<pk>\d+)/$', views.VirtualMachineView.as_view(), name='virtualmachine'),
|
url(r'^virtual-machines/(?P<pk>\d+)/$', views.VirtualMachineView.as_view(), name='virtualmachine'),
|
||||||
url(r'^virtual-machines/(?P<pk>\d+)/edit/$', views.VirtualMachineEditView.as_view(), name='virtualmachine_edit'),
|
url(r'^virtual-machines/(?P<pk>\d+)/edit/$', views.VirtualMachineEditView.as_view(), name='virtualmachine_edit'),
|
||||||
url(r'^virtual-machines/(?P<pk>\d+)/delete/$', views.VirtualMachineDeleteView.as_view(), name='virtualmachine_delete'),
|
url(r'^virtual-machines/(?P<pk>\d+)/delete/$', views.VirtualMachineDeleteView.as_view(), name='virtualmachine_delete'),
|
||||||
|
url(r'^virtual-machines/(?P<virtualmachine>\d+)/services/assign/$', ServiceCreateView.as_view(), name='virtualmachine_service_assign'),
|
||||||
|
|
||||||
# VM interfaces
|
# VM interfaces
|
||||||
# url(r'^virtual-machines/interfaces/add/$', views.VMBulkAddInterfaceView.as_view(), name='vm_bulk_add_interface'),
|
# url(r'^virtual-machines/interfaces/add/$', views.VMBulkAddInterfaceView.as_view(), name='vm_bulk_add_interface'),
|
||||||
|
@ -9,6 +9,7 @@ from django.views.generic import View
|
|||||||
|
|
||||||
from dcim.models import Device, Interface
|
from dcim.models import Device, Interface
|
||||||
from dcim.tables import DeviceTable
|
from dcim.tables import DeviceTable
|
||||||
|
from ipam.models import Service
|
||||||
from utilities.views import (
|
from utilities.views import (
|
||||||
BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, ComponentDeleteView, ComponentEditView,
|
BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, ComponentDeleteView, ComponentEditView,
|
||||||
ObjectDeleteView, ObjectEditView, ObjectListView,
|
ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||||
@ -236,10 +237,12 @@ class VirtualMachineView(View):
|
|||||||
|
|
||||||
vm = get_object_or_404(VirtualMachine.objects.select_related('tenant__group'), pk=pk)
|
vm = get_object_or_404(VirtualMachine.objects.select_related('tenant__group'), pk=pk)
|
||||||
interfaces = Interface.objects.filter(virtual_machine=vm)
|
interfaces = Interface.objects.filter(virtual_machine=vm)
|
||||||
|
services = Service.objects.filter(virtual_machine=vm)
|
||||||
|
|
||||||
return render(request, 'virtualization/virtualmachine.html', {
|
return render(request, 'virtualization/virtualmachine.html', {
|
||||||
'vm': vm,
|
'vm': vm,
|
||||||
'interfaces': interfaces,
|
'interfaces': interfaces,
|
||||||
|
'services': services,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user