mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-21 19:47:20 -06:00
commit
7a64404299
@ -31,6 +31,5 @@ Please see [the documentation](http://netbox.readthedocs.io/en/stable/) for inst
|
|||||||
|
|
||||||
## Alternative Installations
|
## Alternative Installations
|
||||||
|
|
||||||
* [Docker container](https://github.com/digitalocean/netbox-docker)
|
* [Docker container](https://github.com/ninech/netbox-docker) (via [@cimnine](https://github.com/cimnine))
|
||||||
* [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) (via [@ryanmerolle](https://github.com/ryanmerolle))
|
||||||
* [Vagrant deployment](https://github.com/ryanmerolle/netbox-vagrant)
|
|
||||||
|
@ -55,7 +55,7 @@ LDAP_IGNORE_CERT_ERRORS = True
|
|||||||
## User Authentication
|
## User Authentication
|
||||||
|
|
||||||
!!! info
|
!!! 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
|
```python
|
||||||
from django_auth_ldap.config import LDAPSearch
|
from django_auth_ldap.config import LDAPSearch
|
||||||
@ -79,7 +79,7 @@ AUTH_LDAP_USER_ATTR_MAP = {
|
|||||||
|
|
||||||
# User Groups for Permissions
|
# User Groups for Permissions
|
||||||
!!! Info
|
!!! 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
|
```python
|
||||||
from django_auth_ldap.config import LDAPSearch, GroupOfNamesType
|
from django_auth_ldap.config import LDAPSearch, GroupOfNamesType
|
||||||
|
@ -17,6 +17,7 @@ from utilities.forms import (
|
|||||||
ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea,
|
ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea,
|
||||||
SlugField, FilterTreeNodeMultipleChoiceField,
|
SlugField, FilterTreeNodeMultipleChoiceField,
|
||||||
)
|
)
|
||||||
|
from virtualization.models import Cluster
|
||||||
from .formfields import MACAddressFormField
|
from .formfields import MACAddressFormField
|
||||||
from .models import (
|
from .models import (
|
||||||
DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_CONNECTED, ConsolePort,
|
DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_CONNECTED, ConsolePort,
|
||||||
@ -900,11 +901,20 @@ class DeviceCSVForm(BaseDeviceCSVForm):
|
|||||||
required=False,
|
required=False,
|
||||||
help_text='Mounted rack face'
|
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):
|
class Meta(BaseDeviceCSVForm.Meta):
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
|
'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):
|
def clean(self):
|
||||||
@ -940,11 +950,19 @@ class ChildDeviceCSVForm(BaseDeviceCSVForm):
|
|||||||
device_bay_name = forms.CharField(
|
device_bay_name = forms.CharField(
|
||||||
help_text='Name of device bay',
|
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):
|
class Meta(BaseDeviceCSVForm.Meta):
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
|
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
|
||||||
'parent', 'device_bay_name',
|
'parent', 'device_bay_name', 'cluster',
|
||||||
]
|
]
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
@ -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):
|
class BulkDisconnectView(View):
|
||||||
"""
|
"""
|
||||||
An extendable view for disconnection console/power/interface components in bulk.
|
An extendable view for disconnection console/power/interface components in bulk.
|
||||||
|
@ -274,6 +274,7 @@ class TopologyMap(models.Model):
|
|||||||
# Construct the graph
|
# Construct the graph
|
||||||
graph = graphviz.Graph()
|
graph = graphviz.Graph()
|
||||||
graph.graph_attr['ranksep'] = '1'
|
graph.graph_attr['ranksep'] = '1'
|
||||||
|
seen = set()
|
||||||
for i, device_set in enumerate(self.device_sets):
|
for i, device_set in enumerate(self.device_sets):
|
||||||
|
|
||||||
subgraph = graphviz.Graph(name='sg{}'.format(i))
|
subgraph = graphviz.Graph(name='sg{}'.format(i))
|
||||||
@ -288,6 +289,9 @@ class TopologyMap(models.Model):
|
|||||||
devices = []
|
devices = []
|
||||||
for query in device_set.strip(';').split(';'): # Split regexes on semicolons
|
for query in device_set.strip(';').split(';'): # Split regexes on semicolons
|
||||||
devices += Device.objects.filter(name__regex=query).select_related('device_role')
|
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:
|
for d in devices:
|
||||||
bg_color = '#{}'.format(d.device_role.color)
|
bg_color = '#{}'.format(d.device_role.color)
|
||||||
fg_color = '#{}'.format(foreground_color(d.device_role.color))
|
fg_color = '#{}'.format(foreground_color(d.device_role.color))
|
||||||
|
@ -240,12 +240,22 @@ class WritablePrefixSerializer(CustomFieldModelSerializer):
|
|||||||
# IP addresses
|
# 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):
|
class IPAddressSerializer(CustomFieldModelSerializer):
|
||||||
vrf = NestedVRFSerializer()
|
vrf = NestedVRFSerializer()
|
||||||
tenant = NestedTenantSerializer()
|
tenant = NestedTenantSerializer()
|
||||||
status = ChoiceFieldSerializer(choices=IPADDRESS_STATUS_CHOICES)
|
status = ChoiceFieldSerializer(choices=IPADDRESS_STATUS_CHOICES)
|
||||||
role = ChoiceFieldSerializer(choices=IPADDRESS_ROLE_CHOICES)
|
role = ChoiceFieldSerializer(choices=IPADDRESS_ROLE_CHOICES)
|
||||||
interface = InterfaceSerializer()
|
interface = IPAddressInterfaceSerializer()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
@ -262,6 +272,7 @@ class NestedIPAddressSerializer(serializers.ModelSerializer):
|
|||||||
model = IPAddress
|
model = IPAddress
|
||||||
fields = ['id', 'url', 'family', 'address']
|
fields = ['id', 'url', 'family', 'address']
|
||||||
|
|
||||||
|
|
||||||
IPAddressSerializer._declared_fields['nat_inside'] = NestedIPAddressSerializer()
|
IPAddressSerializer._declared_fields['nat_inside'] = NestedIPAddressSerializer()
|
||||||
IPAddressSerializer._declared_fields['nat_outside'] = NestedIPAddressSerializer()
|
IPAddressSerializer._declared_fields['nat_outside'] = NestedIPAddressSerializer()
|
||||||
|
|
||||||
|
@ -151,7 +151,7 @@ class IPAddressViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
|
|||||||
queryset = IPAddress.objects.select_related(
|
queryset = IPAddress.objects.select_related(
|
||||||
'vrf__tenant', 'tenant', 'nat_inside'
|
'vrf__tenant', 'tenant', 'nat_inside'
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
'interface__device'
|
'interface__device', 'interface__virtual_machine'
|
||||||
)
|
)
|
||||||
serializer_class = serializers.IPAddressSerializer
|
serializer_class = serializers.IPAddressSerializer
|
||||||
write_serializer_class = serializers.WritableIPAddressSerializer
|
write_serializer_class = serializers.WritableIPAddressSerializer
|
||||||
|
@ -440,7 +440,7 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
self.get_status_display(),
|
self.get_status_display(),
|
||||||
self.get_role_display(),
|
self.get_role_display(),
|
||||||
self.device.identifier if self.device else None,
|
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,
|
self.interface.name if self.interface else None,
|
||||||
is_primary,
|
is_primary,
|
||||||
self.description,
|
self.description,
|
||||||
@ -452,6 +452,12 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
return self.interface.device
|
return self.interface.device
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def virtual_machine(self):
|
||||||
|
if self.interface:
|
||||||
|
return self.interface.virtual_machine
|
||||||
|
return None
|
||||||
|
|
||||||
def get_status_class(self):
|
def get_status_class(self):
|
||||||
return STATUS_CHOICE_CLASSES[self.status]
|
return STATUS_CHOICE_CLASSES[self.status]
|
||||||
|
|
||||||
|
@ -30,6 +30,10 @@ OBJ_TYPE_CHOICES = (
|
|||||||
('Tenancy', (
|
('Tenancy', (
|
||||||
('tenant', 'Tenants'),
|
('tenant', 'Tenants'),
|
||||||
)),
|
)),
|
||||||
|
('Virtualization', (
|
||||||
|
('cluster', 'Clusters'),
|
||||||
|
('virtualmachine', 'Virtual machines'),
|
||||||
|
)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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__)))
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ from tenancy.models import Tenant
|
|||||||
from tenancy.tables import TenantTable
|
from tenancy.tables import TenantTable
|
||||||
from virtualization.filters import ClusterFilter, VirtualMachineFilter
|
from virtualization.filters import ClusterFilter, VirtualMachineFilter
|
||||||
from virtualization.models import Cluster, VirtualMachine
|
from virtualization.models import Cluster, VirtualMachine
|
||||||
from virtualization.tables import ClusterTable, VirtualMachineTable
|
from virtualization.tables import ClusterTable, VirtualMachineDetailTable
|
||||||
from .forms import SearchForm
|
from .forms import SearchForm
|
||||||
|
|
||||||
|
|
||||||
@ -126,9 +126,11 @@ SEARCH_TYPES = OrderedDict((
|
|||||||
'url': 'virtualization:cluster_list',
|
'url': 'virtualization:cluster_list',
|
||||||
}),
|
}),
|
||||||
('virtualmachine', {
|
('virtualmachine', {
|
||||||
'queryset': VirtualMachine.objects.select_related('cluster', 'tenant', 'platform'),
|
'queryset': VirtualMachine.objects.select_related(
|
||||||
|
'cluster', 'tenant', 'platform', 'primary_ip4', 'primary_ip6',
|
||||||
|
),
|
||||||
'filter': VirtualMachineFilter,
|
'filter': VirtualMachineFilter,
|
||||||
'table': VirtualMachineTable,
|
'table': VirtualMachineDetailTable,
|
||||||
'url': 'virtualization:virtualmachine_list',
|
'url': 'virtualization:virtualmachine_list',
|
||||||
}),
|
}),
|
||||||
))
|
))
|
||||||
|
@ -59,6 +59,7 @@
|
|||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
var device_list = $('#id_devices');
|
var device_list = $('#id_devices');
|
||||||
|
var disabled_indicator = device_list.attr('disabled-indicator');
|
||||||
$('#id_search').autocomplete({
|
$('#id_search').autocomplete({
|
||||||
source: function(request, response) {
|
source: function(request, response) {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
@ -70,7 +71,11 @@
|
|||||||
},
|
},
|
||||||
success: function(data) {
|
success: function(data) {
|
||||||
response($.map(data.results, function(item) {
|
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);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -78,7 +78,7 @@ class VirtualMachineFilter(CustomFieldFilterSet):
|
|||||||
label='Cluster group (ID)',
|
label='Cluster group (ID)',
|
||||||
)
|
)
|
||||||
cluster_group = NullableModelMultipleChoiceFilter(
|
cluster_group = NullableModelMultipleChoiceFilter(
|
||||||
name='cluster__group__slug',
|
name='cluster__group',
|
||||||
queryset=ClusterGroup.objects.all(),
|
queryset=ClusterGroup.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Cluster group (slug)',
|
label='Cluster group (slug)',
|
||||||
@ -88,12 +88,10 @@ class VirtualMachineFilter(CustomFieldFilterSet):
|
|||||||
label='Cluster (ID)',
|
label='Cluster (ID)',
|
||||||
)
|
)
|
||||||
role_id = NullableModelMultipleChoiceFilter(
|
role_id = NullableModelMultipleChoiceFilter(
|
||||||
name='role_id',
|
|
||||||
queryset=DeviceRole.objects.all(),
|
queryset=DeviceRole.objects.all(),
|
||||||
label='Role (ID)',
|
label='Role (ID)',
|
||||||
)
|
)
|
||||||
role = NullableModelMultipleChoiceFilter(
|
role = NullableModelMultipleChoiceFilter(
|
||||||
name='role__slug',
|
|
||||||
queryset=DeviceRole.objects.all(),
|
queryset=DeviceRole.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Role (slug)',
|
label='Role (slug)',
|
||||||
@ -112,7 +110,6 @@ class VirtualMachineFilter(CustomFieldFilterSet):
|
|||||||
label='Platform (ID)',
|
label='Platform (ID)',
|
||||||
)
|
)
|
||||||
platform = NullableModelMultipleChoiceFilter(
|
platform = NullableModelMultipleChoiceFilter(
|
||||||
name='platform',
|
|
||||||
queryset=Platform.objects.all(),
|
queryset=Platform.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Platform (slug)',
|
label='Platform (slug)',
|
||||||
|
@ -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 the Cluster is assigned to a Site, all Devices must be assigned to that Site.
|
||||||
if self.cluster.site is not None:
|
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:
|
if device.site != self.cluster.site:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'devices': "{} belongs to a different site ({}) than the cluster ({})".format(
|
'devices': "{} belongs to a different site ({}) than the cluster ({})".format(
|
||||||
|
@ -24,6 +24,10 @@ VIRTUALMACHINE_STATUS = """
|
|||||||
<span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span>
|
<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 = """
|
VIRTUALMACHINE_PRIMARY_IP = """
|
||||||
{{ record.primary_ip6.address.ip|default:"" }}
|
{{ record.primary_ip6.address.ip|default:"" }}
|
||||||
{% if record.primary_ip6 and record.primary_ip4 %}<br />{% endif %}
|
{% if record.primary_ip6 and record.primary_ip4 %}<br />{% endif %}
|
||||||
@ -93,6 +97,7 @@ class VirtualMachineTable(BaseTable):
|
|||||||
name = tables.LinkColumn()
|
name = tables.LinkColumn()
|
||||||
status = tables.TemplateColumn(template_code=VIRTUALMACHINE_STATUS)
|
status = tables.TemplateColumn(template_code=VIRTUALMACHINE_STATUS)
|
||||||
cluster = tables.LinkColumn('virtualization:cluster', args=[Accessor('cluster.pk')])
|
cluster = tables.LinkColumn('virtualization:cluster', args=[Accessor('cluster.pk')])
|
||||||
|
role = tables.TemplateColumn(VIRTUALMACHINE_ROLE)
|
||||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
|
Loading…
Reference in New Issue
Block a user