Просмотр исходного кода

Merge pull request #1614 from digitalocean/develop

Release v2.2.2
Jeremy Stretch 8 лет назад
Родитель
Сommit
7a64404299

+ 2 - 3
README.md

@@ -31,6 +31,5 @@ Please see [the documentation](http://netbox.readthedocs.io/en/stable/) for inst
 
 ## Alternative Installations
 
-* [Docker container](https://github.com/digitalocean/netbox-docker)
-* [Heroku deployment](https://heroku.com/deploy?template=https://github.com/BILDQUADRAT/netbox/tree/heroku) (via [@mraerino](https://github.com/BILDQUADRAT/netbox/tree/heroku))
-* [Vagrant deployment](https://github.com/ryanmerolle/netbox-vagrant)
+* [Docker container](https://github.com/ninech/netbox-docker) (via [@cimnine](https://github.com/cimnine))
+* [Vagrant deployment](https://github.com/ryanmerolle/netbox-vagrant) (via [@ryanmerolle](https://github.com/ryanmerolle))

+ 2 - 2
docs/installation/ldap.md

@@ -55,7 +55,7 @@ LDAP_IGNORE_CERT_ERRORS = True
 ## User Authentication
 
 !!! info
-    When using Windows Server, `2012 AUTH_LDAP_USER_DN_TEMPLATE` should be set to None.
+    When using Windows Server 2012, `AUTH_LDAP_USER_DN_TEMPLATE` should be set to None.
 
 ```python
 from django_auth_ldap.config import LDAPSearch
@@ -79,7 +79,7 @@ AUTH_LDAP_USER_ATTR_MAP = {
 
 # User Groups for Permissions
 !!! Info
-    When using Microsoft Active Directory, Support for nested Groups can be activated by using `GroupOfNamesType()` instead of `NestedGroupOfNamesType()` for AUTH_LDAP_GROUP_TYPE.
+    When using Microsoft Active Directory, Support for nested Groups can be activated by using `GroupOfNamesType()` instead of `NestedGroupOfNamesType()` for `AUTH_LDAP_GROUP_TYPE`.
 
 ```python
 from django_auth_ldap.config import LDAPSearch, GroupOfNamesType

+ 20 - 2
netbox/dcim/forms.py

@@ -17,6 +17,7 @@ from utilities.forms import (
     ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea,
     SlugField, FilterTreeNodeMultipleChoiceField,
 )
+from virtualization.models import Cluster
 from .formfields import MACAddressFormField
 from .models import (
     DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_CONNECTED, ConsolePort,
@@ -900,11 +901,20 @@ class DeviceCSVForm(BaseDeviceCSVForm):
         required=False,
         help_text='Mounted rack face'
     )
+    cluster = forms.ModelChoiceField(
+        queryset=Cluster.objects.all(),
+        to_field_name='name',
+        required=False,
+        help_text='Virtualization cluster',
+        error_messages={
+            'invalid_choice': 'Invalid cluster name.',
+        }
+    )
 
     class Meta(BaseDeviceCSVForm.Meta):
         fields = [
             'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
-            'site', 'rack_group', 'rack_name', 'position', 'face',
+            'site', 'rack_group', 'rack_name', 'position', 'face', 'cluster',
         ]
 
     def clean(self):
@@ -940,11 +950,19 @@ class ChildDeviceCSVForm(BaseDeviceCSVForm):
     device_bay_name = forms.CharField(
         help_text='Name of device bay',
     )
+    cluster = forms.ModelChoiceField(
+        queryset=Cluster.objects.all(),
+        to_field_name='name',
+        help_text='Virtualization cluster',
+        error_messages={
+            'invalid_choice': 'Invalid cluster name.',
+        }
+    )
 
     class Meta(BaseDeviceCSVForm.Meta):
         fields = [
             'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
-            'parent', 'device_bay_name',
+            'parent', 'device_bay_name', 'cluster',
         ]
 
     def clean(self):

+ 0 - 26
netbox/dcim/views.py

@@ -34,32 +34,6 @@ from .models import (
 )
 
 
-EXPANSION_PATTERN = '\[(\d+-\d+)\]'
-
-
-def xstr(s):
-    """
-    Replace None with an empty string (for CSV export)
-    """
-    return '' if s is None else str(s)
-
-
-def expand_pattern(string):
-    """
-    Expand a numeric pattern into a list of strings. Examples:
-      'ge-0/0/[0-3]' => ['ge-0/0/0', 'ge-0/0/1', 'ge-0/0/2', 'ge-0/0/3']
-      'xe-0/[0-3]/[0-7]' => ['xe-0/0/0', 'xe-0/0/1', 'xe-0/0/2', ... 'xe-0/3/5', 'xe-0/3/6', 'xe-0/3/7']
-    """
-    lead, pattern, remnant = re.split(EXPANSION_PATTERN, string, maxsplit=1)
-    x, y = pattern.split('-')
-    for i in range(int(x), int(y) + 1):
-        if remnant:
-            for string in expand_pattern(remnant):
-                yield "{0}{1}{2}".format(lead, i, string)
-        else:
-            yield "{0}{1}".format(lead, i)
-
-
 class BulkDisconnectView(View):
     """
     An extendable view for disconnection console/power/interface components in bulk.

+ 4 - 0
netbox/extras/models.py

@@ -274,6 +274,7 @@ class TopologyMap(models.Model):
         # Construct the graph
         graph = graphviz.Graph()
         graph.graph_attr['ranksep'] = '1'
+        seen = set()
         for i, device_set in enumerate(self.device_sets):
 
             subgraph = graphviz.Graph(name='sg{}'.format(i))
@@ -288,6 +289,9 @@ class TopologyMap(models.Model):
             devices = []
             for query in device_set.strip(';').split(';'):  # Split regexes on semicolons
                 devices += Device.objects.filter(name__regex=query).select_related('device_role')
+            # Remove duplicate devices
+            devices = [d for d in devices if d.id not in seen]
+            seen.update([d.id for d in devices])
             for d in devices:
                 bg_color = '#{}'.format(d.device_role.color)
                 fg_color = '#{}'.format(foreground_color(d.device_role.color))

+ 12 - 1
netbox/ipam/api/serializers.py

@@ -240,12 +240,22 @@ class WritablePrefixSerializer(CustomFieldModelSerializer):
 # IP addresses
 #
 
+class IPAddressInterfaceSerializer(InterfaceSerializer):
+    virtual_machine = NestedVirtualMachineSerializer()
+
+    class Meta(InterfaceSerializer.Meta):
+        fields = [
+            'id', 'device', 'virtual_machine', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address',
+            'mgmt_only', 'description', 'is_connected', 'interface_connection', 'circuit_termination',
+        ]
+
+
 class IPAddressSerializer(CustomFieldModelSerializer):
     vrf = NestedVRFSerializer()
     tenant = NestedTenantSerializer()
     status = ChoiceFieldSerializer(choices=IPADDRESS_STATUS_CHOICES)
     role = ChoiceFieldSerializer(choices=IPADDRESS_ROLE_CHOICES)
-    interface = InterfaceSerializer()
+    interface = IPAddressInterfaceSerializer()
 
     class Meta:
         model = IPAddress
@@ -262,6 +272,7 @@ class NestedIPAddressSerializer(serializers.ModelSerializer):
         model = IPAddress
         fields = ['id', 'url', 'family', 'address']
 
+
 IPAddressSerializer._declared_fields['nat_inside'] = NestedIPAddressSerializer()
 IPAddressSerializer._declared_fields['nat_outside'] = NestedIPAddressSerializer()
 

+ 1 - 1
netbox/ipam/api/views.py

@@ -151,7 +151,7 @@ class IPAddressViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
     queryset = IPAddress.objects.select_related(
         'vrf__tenant', 'tenant', 'nat_inside'
     ).prefetch_related(
-        'interface__device'
+        'interface__device', 'interface__virtual_machine'
     )
     serializer_class = serializers.IPAddressSerializer
     write_serializer_class = serializers.WritableIPAddressSerializer

+ 7 - 1
netbox/ipam/models.py

@@ -440,7 +440,7 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
             self.get_status_display(),
             self.get_role_display(),
             self.device.identifier if self.device else None,
-            self.virtual_machine.name if self.device else None,
+            self.virtual_machine.name if self.virtual_machine else None,
             self.interface.name if self.interface else None,
             is_primary,
             self.description,
@@ -452,6 +452,12 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
             return self.interface.device
         return None
 
+    @property
+    def virtual_machine(self):
+        if self.interface:
+            return self.interface.virtual_machine
+        return None
+
     def get_status_class(self):
         return STATUS_CHOICE_CLASSES[self.status]
 

+ 4 - 0
netbox/netbox/forms.py

@@ -30,6 +30,10 @@ OBJ_TYPE_CHOICES = (
     ('Tenancy', (
         ('tenant', 'Tenants'),
     )),
+    ('Virtualization', (
+        ('cluster', 'Clusters'),
+        ('virtualmachine', 'Virtual machines'),
+    )),
 )
 
 

+ 1 - 1
netbox/netbox/settings.py

@@ -13,7 +13,7 @@ except ImportError:
     )
 
 
-VERSION = '2.2.1'
+VERSION = '2.2.2'
 
 BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 

+ 5 - 3
netbox/netbox/views.py

@@ -27,7 +27,7 @@ from tenancy.models import Tenant
 from tenancy.tables import TenantTable
 from virtualization.filters import ClusterFilter, VirtualMachineFilter
 from virtualization.models import Cluster, VirtualMachine
-from virtualization.tables import ClusterTable, VirtualMachineTable
+from virtualization.tables import ClusterTable, VirtualMachineDetailTable
 from .forms import SearchForm
 
 
@@ -126,9 +126,11 @@ SEARCH_TYPES = OrderedDict((
         'url': 'virtualization:cluster_list',
     }),
     ('virtualmachine', {
-        'queryset': VirtualMachine.objects.select_related('cluster', 'tenant', 'platform'),
+        'queryset': VirtualMachine.objects.select_related(
+            'cluster', 'tenant', 'platform', 'primary_ip4', 'primary_ip6',
+        ),
         'filter': VirtualMachineFilter,
-        'table': VirtualMachineTable,
+        'table': VirtualMachineDetailTable,
         'url': 'virtualization:virtualmachine_list',
     }),
 ))

+ 6 - 1
netbox/templates/virtualization/cluster_add_devices.html

@@ -59,6 +59,7 @@
 <script type="text/javascript">
     $(document).ready(function() {
         var device_list = $('#id_devices');
+        var disabled_indicator = device_list.attr('disabled-indicator');
         $('#id_search').autocomplete({
             source: function(request, response) {
                 $.ajax({
@@ -70,7 +71,11 @@
                     },
                     success: function(data) {
                         response($.map(data.results, function(item) {
-                            device_list.append('<option value="' + item['id'] + '">' + item['display_name'] + '</option>');
+                            var option = $("<option></option>").attr("value", item['id']).text(item['display_name']);
+                            if (disabled_indicator && item[disabled_indicator]) {
+                                option.attr("disabled", "disabled");
+                            }
+                            device_list.append(option);
                         }));
                     }
                 });

+ 1 - 4
netbox/virtualization/filters.py

@@ -78,7 +78,7 @@ class VirtualMachineFilter(CustomFieldFilterSet):
         label='Cluster group (ID)',
     )
     cluster_group = NullableModelMultipleChoiceFilter(
-        name='cluster__group__slug',
+        name='cluster__group',
         queryset=ClusterGroup.objects.all(),
         to_field_name='slug',
         label='Cluster group (slug)',
@@ -88,12 +88,10 @@ class VirtualMachineFilter(CustomFieldFilterSet):
         label='Cluster (ID)',
     )
     role_id = NullableModelMultipleChoiceFilter(
-        name='role_id',
         queryset=DeviceRole.objects.all(),
         label='Role (ID)',
     )
     role = NullableModelMultipleChoiceFilter(
-        name='role__slug',
         queryset=DeviceRole.objects.all(),
         to_field_name='slug',
         label='Role (slug)',
@@ -112,7 +110,6 @@ class VirtualMachineFilter(CustomFieldFilterSet):
         label='Platform (ID)',
     )
     platform = NullableModelMultipleChoiceFilter(
-        name='platform',
         queryset=Platform.objects.all(),
         to_field_name='slug',
         label='Platform (slug)',

+ 1 - 1
netbox/virtualization/forms.py

@@ -210,7 +210,7 @@ class ClusterAddDevicesForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
 
         # If the Cluster is assigned to a Site, all Devices must be assigned to that Site.
         if self.cluster.site is not None:
-            for device in self.cleaned_data.get('devices'):
+            for device in self.cleaned_data.get('devices', []):
                 if device.site != self.cluster.site:
                     raise ValidationError({
                         'devices': "{} belongs to a different site ({}) than the cluster ({})".format(

+ 5 - 0
netbox/virtualization/tables.py

@@ -24,6 +24,10 @@ VIRTUALMACHINE_STATUS = """
 <span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span>
 """
 
+VIRTUALMACHINE_ROLE = """
+<label class="label" style="background-color: #{{ record.role.color }}">{{ value }}</label>
+"""
+
 VIRTUALMACHINE_PRIMARY_IP = """
 {{ record.primary_ip6.address.ip|default:"" }}
 {% if record.primary_ip6 and record.primary_ip4 %}<br />{% endif %}
@@ -93,6 +97,7 @@ class VirtualMachineTable(BaseTable):
     name = tables.LinkColumn()
     status = tables.TemplateColumn(template_code=VIRTUALMACHINE_STATUS)
     cluster = tables.LinkColumn('virtualization:cluster', args=[Accessor('cluster.pk')])
+    role = tables.TemplateColumn(VIRTUALMACHINE_ROLE)
     tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
 
     class Meta(BaseTable.Meta):