Compare commits

..

23 Commits

Author SHA1 Message Date
Jeremy Stretch
7a64404299 Merge pull request #1614 from digitalocean/develop
Release v2.2.2
2017-10-17 11:24:02 -04:00
Jeremy Stretch
2afa6ed2cb Release v2.2.2 2017-10-17 11:21:58 -04:00
Jeremy Stretch
34f1a9ebfb Fixes #1579: Devices already assigned to a cluster cannot be added to a different cluster 2017-10-17 09:59:35 -04:00
Jeremy Stretch
6f2f8697ae Fixes #1609: Added missing virtual_machine field to IP address interface serializer 2017-10-17 09:23:53 -04:00
Jeremy Stretch
6ec9d1d6ce Merge pull request #1598 from candlerb/candlerb/1498
Avoid creating repeated graph nodes where device matches multiple regexps
2017-10-16 17:16:46 -04:00
Jeremy Stretch
047f22e110 Fixes #1605: Added clusters and virtual machines to object list for global search 2017-10-16 16:44:15 -04:00
root
5fc3eac0f6 Avoid creating repeated graph nodes where device matches multiple regexps
Fixes #1498
2017-10-16 10:13:39 +00:00
Jeremy Stretch
34259d5d9d Removed deprecated xstr and expand_pattern functions 2017-10-13 14:29:55 -04:00
Jeremy Stretch
91b6ebb0c0 Closes #1580: Allow cluster assignment when bulk importing devices 2017-10-13 14:19:41 -04:00
Jeremy Stretch
60b4f1f89f Fixes #1585: Fixed slug-based filtering of virtual machines 2017-10-13 12:14:19 -04:00
Jeremy Stretch
023ff6834a Designated new Docker build repo; removed stale Heroku build repo 2017-10-13 11:50:06 -04:00
Jeremy Stretch
d00cab0b0a Merge branch 'develop' of github.com:digitalocean/netbox into develop 2017-10-13 10:54:24 -04:00
Jeremy Stretch
17493ff655 Closes #1587: Add primary IP column for virtual machines in global search results 2017-10-13 10:53:25 -04:00
Jeremy Stretch
6c27e6c4fe Fixes #1584: Colorized virtual machine role column 2017-10-13 10:45:34 -04:00
Jeremy Stretch
4cb0be4df3 Fixes #1582: Add virtual_machine attribute to IPAddress 2017-10-13 10:42:45 -04:00
Jeremy Stretch
40f555a3b6 Merge pull request #1545 from digitalocean/mdl-ldap-docs-formatting
Minor LDAP documentation formatting cleanup
2017-10-12 23:42:29 -04:00
Jeremy Stretch
881fdbe893 Post-release version bump 2017-10-12 16:39:51 -04:00
Jeremy Stretch
2bda399982 Merge pull request #1577 from digitalocean/develop
Release v2.2.1
2017-10-12 16:11:17 -04:00
Jeremy Stretch
75d840fa1a PEP8 fix 2017-10-12 16:07:13 -04:00
Jeremy Stretch
800bdd8fc5 Release v2.2.1 2017-10-12 16:04:01 -04:00
Jeremy Stretch
b04ade8060 Fixes #1576: Move PostgreSQL validation logic into the relevant migration 2017-10-12 16:02:15 -04:00
Jeremy Stretch
7f4d96f33e Post-release version bump 2017-10-12 14:01:52 -04:00
Matt Layher
a01c9ff379 Minor LDAP documentation formatting cleanup 2017-09-29 13:22:42 -04:00
17 changed files with 92 additions and 65 deletions

View File

@@ -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))

View File

@@ -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

View File

@@ -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):

View File

@@ -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.

View File

@@ -1,11 +1,30 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-09-26 21:25
from __future__ import unicode_literals
from distutils.version import StrictVersion
from django.conf import settings
import django.contrib.postgres.fields.jsonb
from django.db import migrations, models
from django.db import connection, migrations, models
import django.db.models.deletion
from django.db.utils import OperationalError
def verify_postgresql_version(apps, schema_editor):
"""
Verify that PostgreSQL is version 9.4 or higher.
"""
try:
with connection.cursor() as cursor:
cursor.execute("SELECT VERSION()")
row = cursor.fetchone()
pg_version = row[0].split()[1]
if StrictVersion(pg_version) < StrictVersion('9.4.0'):
raise Exception("PostgreSQL 9.4.0 or higher is required ({} found). Upgrade PostgreSQL and then run migrations again.".format(pg_version))
# Skip if the database is missing (e.g. for CI testing) or misconfigured.
except OperationalError:
pass
class Migration(migrations.Migration):
@@ -16,6 +35,7 @@ class Migration(migrations.Migration):
]
operations = [
migrations.RunPython(verify_postgresql_version),
migrations.CreateModel(
name='ReportResult',
fields=[

View File

@@ -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))

View File

@@ -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()

View File

@@ -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

View File

@@ -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]

View File

@@ -1,18 +0,0 @@
from distutils.version import StrictVersion
from django.db import connection
from django.db.utils import OperationalError
# NetBox v2.2 and later requires PostgreSQL 9.4 or higher.
try:
with connection.cursor() as cursor:
cursor.execute("SELECT VERSION()")
row = cursor.fetchone()
pg_version = row[0].split()[1]
if StrictVersion(pg_version) < StrictVersion('9.4.0'):
raise Exception("PostgreSQL 9.4.0 or higher is required. ({} found)".format(pg_version))
# Skip if the database is missing (e.g. for CI testing) or misconfigured.
except OperationalError:
pass

View File

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

View File

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

View File

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

View File

@@ -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);
}));
}
});

View File

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

View File

@@ -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(

View File

@@ -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):