Parcourir la source

Enabled services on virtual machines

Jeremy Stretch il y a 7 ans
Parent
commit
3bb0d523d3

+ 1 - 1
netbox/dcim/urls.py

@@ -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+)/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<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}),
 
     # Console ports

+ 4 - 2
netbox/ipam/api/serializers.py

@@ -12,6 +12,7 @@ from ipam.models import (
 )
 from tenancy.api.serializers import NestedTenantSerializer
 from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer
+from virtualization.api.serializers import NestedVirtualMachineSerializer
 
 
 #
@@ -295,12 +296,13 @@ class AvailableIPSerializer(serializers.Serializer):
 
 class ServiceSerializer(serializers.ModelSerializer):
     device = NestedDeviceSerializer()
+    virtual_machine = NestedVirtualMachineSerializer()
     protocol = ChoiceFieldSerializer(choices=IP_PROTOCOL_CHOICES)
     ipaddresses = NestedIPAddressSerializer(many=True)
 
     class Meta:
         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.
@@ -308,4 +310,4 @@ class WritableServiceSerializer(serializers.ModelSerializer):
 
     class Meta:
         model = Service
-        fields = ['id', 'device', 'name', 'port', 'protocol', 'ipaddresses', 'description']
+        fields = ['id', 'device', 'virtual_machine', 'name', 'port', 'protocol', 'ipaddresses', 'description']

+ 11 - 2
netbox/ipam/forms.py

@@ -861,5 +861,14 @@ class ServiceForm(BootstrapMixin, forms.ModelForm):
 
         super(ServiceForm, self).__init__(*args, **kwargs)
 
-        # Limit IP address choices to those assigned to interfaces of the parent device
-        self.fields['ipaddresses'].queryset = IPAddress.objects.filter(interface__device=self.instance.device)
+        # Limit IP address choices to those assigned to interfaces of the parent device/VM
+        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 - 0
netbox/ipam/migrations/0019_virtualization.py

@@ -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'),
+        ),
+    ]

+ 50 - 11
netbox/ipam/models.py

@@ -588,20 +588,59 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
 @python_2_unicode_compatible
 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
-    to one or more specific IPAddresses belonging to the Device.
+    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('dcim.Device', related_name='services', on_delete=models.CASCADE, verbose_name='device')
-    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('ipam.IPAddress', related_name='services', blank=True,
-                                         verbose_name='IP addresses')
-    description = models.CharField(max_length=100, blank=True)
+    device = models.ForeignKey(
+        to='dcim.Device',
+        on_delete=models.CASCADE,
+        related_name='services',
+        verbose_name='device',
+        null=True,
+        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:
-        ordering = ['device', 'protocol', 'port']
+        ordering = ['protocol', 'port']
 
     def __str__(self):
         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.")

+ 4 - 1
netbox/ipam/views.py

@@ -15,6 +15,7 @@ from utilities.paginator import EnhancedPaginator
 from utilities.views import (
     BulkCreateView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
 )
+from virtualization.models import VirtualMachine
 from . import filters, forms, tables
 from .constants import IPADDRESS_ROLE_ANYCAST
 from .models import (
@@ -838,10 +839,12 @@ class ServiceCreateView(PermissionRequiredMixin, ObjectEditView):
     def alter_obj(self, obj, request, url_args, url_kwargs):
         if 'device' in url_kwargs:
             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
 
     def get_return_url(self, request, obj):
-        return obj.device.get_absolute_url()
+        return obj.parent.get_absolute_url()
 
 
 class ServiceEditView(ServiceCreateView):

+ 2 - 2
netbox/templates/dcim/device.html

@@ -196,7 +196,7 @@
             {% if services %}
                 <table class="table table-hover panel-body">
                     {% for service in services %}
-                        {% include 'dcim/inc/service.html' %}
+                        {% include 'ipam/inc/service.html' %}
                     {% endfor %}
                 </table>
             {% else %}
@@ -206,7 +206,7 @@
             {% endif %}
             {% if perms.ipam.add_service %}
                 <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
                     </a>
                 </div>

+ 3 - 3
netbox/templates/dcim/inc/service.html

@@ -14,12 +14,12 @@
     <td class="text-right">
         {% if perms.ipam.change_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>
         {% endif %}
         {% 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">
-                <i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete service"></i>
+            <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" title="Delete service"></i>
             </a>
         {% endif %}
     </td>

+ 14 - 5
netbox/templates/ipam/service_edit.html

@@ -5,12 +5,21 @@
     <div class="panel panel-default">
         <div class="panel-heading"><strong>Service</strong></div>
         <div class="panel-body">
-            <div class="form-group">
-                <label class="col-md-3 control-label">Device</label>
-                <div class="col-md-9">
-                    <p class="form-control-static">{{ obj.device }}</p>
+            {% if obj.device %}
+                <div class="form-group">
+                    <label class="col-md-3 control-label required">Device</label>
+                    <div class="col-md-9">
+                        <p class="form-control-static">{{ obj.device }}</p>
+                    </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 %}
             <div class="form-group form-inline">
                 <label class="col-md-3 control-label required">Port</label>

+ 3 - 3
netbox/templates/virtualization/inc/interface.html

@@ -40,7 +40,7 @@
             {% if ip.description %}
                 <i class="fa fa-fw fa-comment-o" title="{{ ip.description }}"></i>
             {% 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>
             {% endif %}
         </td>
@@ -56,12 +56,12 @@
         </td>
         <td class="text-right">
             {% 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>
                 </a>
             {% endif %}
             {% 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>
                 </a>
             {% endif %}

+ 23 - 0
netbox/templates/virtualization/virtualmachine.html

@@ -158,6 +158,29 @@
                 </tr>
             </table>
         </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 %}
         <div class="panel panel-default">
             <div class="panel-heading">

+ 2 - 0
netbox/virtualization/urls.py

@@ -2,6 +2,7 @@ from __future__ import unicode_literals
 
 from django.conf.urls import url
 
+from ipam.views import ServiceCreateView
 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+)/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<virtualmachine>\d+)/services/assign/$', ServiceCreateView.as_view(), name='virtualmachine_service_assign'),
 
     # VM interfaces
     # url(r'^virtual-machines/interfaces/add/$', views.VMBulkAddInterfaceView.as_view(), name='vm_bulk_add_interface'),

+ 3 - 0
netbox/virtualization/views.py

@@ -9,6 +9,7 @@ from django.views.generic import View
 
 from dcim.models import Device, Interface
 from dcim.tables import DeviceTable
+from ipam.models import Service
 from utilities.views import (
     BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, ComponentDeleteView, ComponentEditView,
     ObjectDeleteView, ObjectEditView, ObjectListView,
@@ -236,10 +237,12 @@ class VirtualMachineView(View):
 
         vm = get_object_or_404(VirtualMachine.objects.select_related('tenant__group'), pk=pk)
         interfaces = Interface.objects.filter(virtual_machine=vm)
+        services = Service.objects.filter(virtual_machine=vm)
 
         return render(request, 'virtualization/virtualmachine.html', {
             'vm': vm,
             'interfaces': interfaces,
+            'services': services,
         })