1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Merge pull request #106 from digitalocean/develop

Release v1.0.5
This commit is contained in:
Jeremy Stretch
2016-06-29 10:00:46 -04:00
committed by GitHub
10 changed files with 218 additions and 50 deletions

View File

@ -49,8 +49,14 @@ Even if it's not quite right for NetBox, we may be able to point you to a tool b
* A rough description of any changes necessary to the database schema (if applicable)
* Any third-party libraries or other resources which would be involved
# Submitting Pull Requests
## Submitting Pull Requests
When submitting a pull request, please be sure to work off of branch `develop`, rather than branch `master`.
* When submitting a pull request, please be sure to work off of branch `develop`, rather than branch `master`.
In NetBox, the `develop` branch is used for ongoing development, while `master` is used for tagging new
stable releases.
* All code submissions should meet the following criteria (CI will enforce these checks):
* Python syntax is valid
* All tests pass when run with `./manage.py test netbox/`
* PEP 8 compliance is enforced, with the exception that lines may be greater than 80 characters in length

View File

@ -48,11 +48,12 @@ You can verify that authentication works using the following command:
# NetBox
## Dependencies
## Installation
NetBox requires following dependencies:
* python2.7
* python-dev
* git
* python-pip
* libxml2-dev
* libxslt1-dev
@ -65,7 +66,21 @@ You can verify that authentication works using the following command:
*graphviz is needed to render topology maps. If you have no need for this feature, graphviz is not required.
## Clone the Git Repository
You may opt to install NetBox either from a numbered release or by cloning the master branch of its repository on GitHub.
### Option A: Download a Release
Download the [latest stable release](https://github.com/digitalocean/netbox/releases) from GitHub as a tarball or ZIP archive. Extract it to your desired path. In this example, we'll use `/opt/netbox`.
```
# wget https://github.com/digitalocean/netbox/archive/vX.Y.Z.tar.gz
# tar -xzf vX.Y.Z.tar.gz -C /opt
# cd /opt/
# ln -s netbox-1.0.4/ netbox
# cd /opt/netbox/
```
### Option B: Clone the Git Repository
Create the base directory for the NetBox installation. For this guide, we'll use `/opt/netbox`.
@ -74,6 +89,12 @@ Create the base directory for the NetBox installation. For this guide, we'll use
# cd /opt/netbox/
```
If `git` is not already installed, install it:
```
# sudo apt-get install git
```
Next, clone the NetBox git repository into the current directory:
```
@ -87,6 +108,8 @@ Resolving deltas: 100% (1495/1495), done.
Checking connectivity... done.
```
### Install Python Packages
Install the necessary Python packages using pip. (If you encounter any compilation errors during this step, ensure that you've installed all of the required dependencies.)
```
@ -264,38 +287,38 @@ Restart the nginx service to use the new configuration.
```
## Apache Configuration
If you're feeling adventurous, or you already have Apache installed and can't run a dual-stack on your server, the following configuration should work for Apache:
The following configuration should work for Apache. Be sure to modify the `ServerName` appropriately.
```
<VirtualHost *:80>
ProxyPreserveHost On
ServerName netbox.example.com
Alias /static/ /opt/netbox/netbox/static
Alias /static /opt/netbox/netbox/static
<Directory /opt/netbox/netbox/static>
Options Indexes FollowSymLinks MultiViews
AllowOverride None
Order allow,deny
Allow from all
# Uncomment the line below if running Apache 2.4
#Require all granted
Require all granted
</Directory>
<Location /static>
ProxyPass !
</Location>
ProxyPass / http://127.0.0.1:8001
ProxyPassReverse / http://127.0.0.1:8001
ProxyPass / http://127.0.0.1:8001/
ProxyPassReverse / http://127.0.0.1:8001/
</VirtualHost>
```
Save the contents of the above example in `/etc/apache2/sites-available/netbox.conf` and reload Apache:
Save the contents of the above example in `/etc/apache2/sites-available/netbox.conf`, enable the `proxy` and `proxy_http` modules, and reload Apache:
```
# a2ensite netbox; service apache2 restart
# a2enmod proxy
# a2enmod proxy_http
# a2ensite netbox
# service apache2 restart
```
## gunicorn Configuration
@ -329,4 +352,26 @@ Finally, restart the supervisor service to detect and run the gunicorn service:
At this point, you should be able to connect to the nginx HTTP service at the server name or IP address you provided. If you are unable to connect, check that the nginx service is running and properly configured. If you receive a 502 (bad gateway) error, this indicates that gunicorn is misconfigured or not running.
Please keep in mind that the configurations provided here are a bare minimum to get NetBox up and running. You will almost certainly want to make some changes to better suit your production environment.
Please keep in mind that the configurations provided here are bare minimums required to get NetBox up and running. You will almost certainly want to make some changes to better suit your production environment.
# Upgrading
As with the initial installation, you can upgrade NetBox by either downloading the lastest release package or by cloning the `master` branch of the git repository. Several important steps are required before running the new code.
First, apply any database migrations that were included with the release. Not all releases include database migrations (in fact, most don't), so don't worry if this command returns "No migrations to apply."
```
# ./manage.py migrate
```
Second, collect any static file that have changed into the root static path. As with database migrations, not all releases will include changes to static files.
```
# ./manage.py collectstatic
```
Finally, restart the WSGI service to run the new code. If you followed this guide for the initial installation, this is done using `supervisorctl`:
```
# sudo supervisorctl restart netbox
```

View File

@ -52,15 +52,13 @@ A prefix may optionally be assigned to one VLAN; a VLAN may have multiple prefix
### Statuses
Each prefix is assigned an operational status. This may be one of the following:
Each prefix is assigned an operational status. This is one of the following:
* Container - A summary of child prefixes
* Active - Provisioned and in use
* Reserved - Earmarked for future use
* Deprecated - No longer in use
NetBox provides several statuses by default, but you are free to change them to suit the needs of your organization.
### Roles
Whereas a status describes a prefix's operational state, a role describes its function. For example, roles might include:
@ -71,7 +69,7 @@ Whereas a status describes a prefix's operational state, a role describes its fu
* Lab
* Out-of-band
Role assignment is optional. And like statuses, you are free to create your own.
Role assignment is optional and you are free to create as many as you'd like.
---

View File

@ -386,10 +386,13 @@ class DeviceForm(forms.ModelForm, BootstrapMixin):
# Rack position
try:
pk = self.instance.pk if self.instance.pk else None
if self.is_bound and self.data.get('rack') and str(self.data.get('face')):
position_choices = Rack.objects.get(pk=self.data['rack']).get_rack_units(face=self.data.get('face'))
position_choices = Rack.objects.get(pk=self.data['rack'])\
.get_rack_units(face=self.data.get('face'), exclude=pk)
elif self.initial.get('rack') and str(self.initial.get('face')):
position_choices = Rack.objects.get(pk=self.initial['rack']).get_rack_units(face=self.initial.get('face'))
position_choices = Rack.objects.get(pk=self.initial['rack'])\
.get_rack_units(face=self.initial.get('face'), exclude=pk)
else:
position_choices = []
except Rack.DoesNotExist:

View File

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-06-28 17:21
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0002_auto_20160622_1821'),
]
operations = [
migrations.AlterField(
model_name='interface',
name='form_factor',
field=models.PositiveSmallIntegerField(choices=[[0, b'Virtual'], [800, b'10/100M (100BASE-TX)'], [1000, b'1GE (1000BASE-T)'], [1100, b'1GE (SFP)'], [1150, b'10GE (10GBASE-T)'], [1200, b'10GE (SFP+)'], [1300, b'10GE (XFP)'], [1400, b'40GE (QSFP+)']], default=1200),
),
migrations.AlterField(
model_name='interfacetemplate',
name='form_factor',
field=models.PositiveSmallIntegerField(choices=[[0, b'Virtual'], [800, b'10/100M (100BASE-TX)'], [1000, b'1GE (1000BASE-T)'], [1100, b'1GE (SFP)'], [1150, b'10GE (10GBASE-T)'], [1200, b'10GE (SFP+)'], [1300, b'10GE (XFP)'], [1400, b'40GE (QSFP+)']], default=1200),
),
]

View File

@ -45,14 +45,16 @@ IFACE_FF_VIRTUAL = 0
IFACE_FF_100M_COPPER = 800
IFACE_FF_1GE_COPPER = 1000
IFACE_FF_SFP = 1100
IFACE_FF_10GE_COPPER = 1150
IFACE_FF_SFP_PLUS = 1200
IFACE_FF_XFP = 1300
IFACE_FF_QSFP_PLUS = 1400
IFACE_FF_CHOICES = [
[IFACE_FF_VIRTUAL, 'Virtual'],
[IFACE_FF_100M_COPPER, '10/100M (Copper)'],
[IFACE_FF_1GE_COPPER, '1GE (Copper)'],
[IFACE_FF_100M_COPPER, '10/100M (100BASE-TX)'],
[IFACE_FF_1GE_COPPER, '1GE (1000BASE-T)'],
[IFACE_FF_SFP, '1GE (SFP)'],
[IFACE_FF_10GE_COPPER, '10GE (10GBASE-T)'],
[IFACE_FF_SFP_PLUS, '10GE (SFP+)'],
[IFACE_FF_XFP, '10GE (XFP)'],
[IFACE_FF_QSFP_PLUS, '40GE (QSFP+)'],
@ -83,6 +85,48 @@ RPC_CLIENT_CHOICES = [
]
def order_interfaces(queryset, sql_col, primary_ordering=tuple()):
"""
Attempt to match interface names by their slot/position identifiers and order according. Matching is done using the
following pattern:
{a}/{b}/{c}:{d}
Interfaces are ordered first by field a, then b, then c, and finally d. Leading text (which typically indicates the
interface's type) is ignored. If any fields are not contained by an interface name, those fields are treated as
None. 'None' is ordered after all other values. For example:
et-0/0/0
et-0/0/1
et-0/1/0
xe-0/1/1:0
xe-0/1/1:1
xe-0/1/1:2
xe-0/1/1:3
et-0/1/2
...
et-0/1/9
et-0/1/10
et-0/1/11
et-1/0/0
et-1/0/1
...
vlan1
vlan10
:param queryset: The base queryset to be ordered
:param sql_col: Table and name of the SQL column which contains the interface name (ex: ''dcim_interface.name')
:param primary_ordering: A tuple of fields which take ordering precedence before the interface name (optional)
"""
ordering = primary_ordering + ('_id1', '_id2', '_id3', '_id4')
return queryset.extra(select={
'_id1': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+\/[0-9]+(:[0-9]+)?$') AS integer)".format(sql_col),
'_id2': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+(:[0-9]+)?$') AS integer)".format(sql_col),
'_id3': "CAST(SUBSTRING({} FROM '([0-9]+)(:[0-9]+)?$') AS integer)".format(sql_col),
'_id4': "CAST(SUBSTRING({} FROM ':([0-9]+)$') AS integer)".format(sql_col),
}).order_by(*ordering)
class Site(CreatedUpdatedModel):
"""
A Site represents a geographic location within a network; typically a building or campus. The optional facility
@ -213,12 +257,13 @@ class Rack(CreatedUpdatedModel):
return "{} ({})".format(self.name, self.facility_id)
return self.name
def get_rack_units(self, face=RACK_FACE_FRONT, remove_redundant=False):
def get_rack_units(self, face=RACK_FACE_FRONT, exclude=None, remove_redundant=False):
"""
Return a list of rack units as dictionaries. Example: {'device': None, 'face': 0, 'id': 48, 'name': 'U48'}
Each key 'device' is either a Device or None. By default, multi-U devices are repeated for each U they occupy.
:param face: Rack face (front or rear)
:param exclude: PK of a Device to exclude (optional); helpful when relocating a Device within a Rack
:param remove_redundant: If True, rack units occupied by a device already listed will be omitted
"""
@ -229,7 +274,9 @@ class Rack(CreatedUpdatedModel):
# Add devices to rack units list
if self.pk:
for device in Device.objects.select_related('device_type__manufacturer', 'device_role')\
.filter(rack=self, position__gt=0).filter(Q(face=face) | Q(device_type__is_full_depth=True)):
.exclude(pk=exclude)\
.filter(rack=self, position__gt=0)\
.filter(Q(face=face) | Q(device_type__is_full_depth=True)):
if remove_redundant:
elevation[device.position]['device'] = device
for u in range(device.position + 1, device.position + device.device_type.u_height):
@ -408,6 +455,13 @@ class PowerOutletTemplate(models.Model):
return self.name
class InterfaceTemplateManager(models.Manager):
def get_queryset(self):
qs = super(InterfaceTemplateManager, self).get_queryset()
return order_interfaces(qs, 'dcim_interfacetemplate.name', ('device_type',))
class InterfaceTemplate(models.Model):
"""
A template for a physical data interface on a new Device.
@ -417,6 +471,8 @@ class InterfaceTemplate(models.Model):
form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_SFP_PLUS)
mgmt_only = models.BooleanField(default=False, verbose_name='Management only')
objects = InterfaceTemplateManager()
class Meta:
ordering = ['device_type', 'name']
unique_together = ['device_type', 'name']
@ -708,18 +764,8 @@ class PowerOutlet(models.Model):
class InterfaceManager(models.Manager):
def get_queryset(self):
"""
Cast up to three interface slot/position IDs as independent integers and order appropriately. This ensures that
interfaces are ordered numerically without regard to type. For example:
xe-0/0/0, xe-0/0/1, xe-0/0/2 ... et-0/0/47, et-0/0/48, et-0/0/49 ...
instead of:
et-0/0/48, et-0/0/49, et-0/0/50 ... et-0/0/53, xe-0/0/0, xe-0/0/1 ...
"""
return super(InterfaceManager, self).get_queryset().extra(select={
'_id1': "CAST(SUBSTRING(dcim_interface.name FROM '([0-9]+)\/([0-9]+)\/([0-9]+)$') AS integer)",
'_id2': "CAST(SUBSTRING(dcim_interface.name FROM '([0-9]+)\/([0-9]+)$') AS integer)",
'_id3': "CAST(SUBSTRING(dcim_interface.name FROM '([0-9]+)$') AS integer)",
}).order_by('device', '_id1', '_id2', '_id3')
qs = super(InterfaceManager, self).get_queryset()
return order_interfaces(qs, 'dcim_interface.name', ('device',))
def virtual(self):
return self.get_queryset().filter(form_factor=IFACE_FF_VIRTUAL)

View File

@ -75,11 +75,13 @@ def site(request, slug):
'vlan_count': VLAN.objects.filter(site=site).count(),
'circuit_count': Circuit.objects.filter(site=site).count(),
}
rack_groups = RackGroup.objects.filter(site=site).annotate(rack_count=Count('racks'))
topology_maps = TopologyMap.objects.filter(site=site)
return render(request, 'dcim/site.html', {
'site': site,
'stats': stats,
'rack_groups': rack_groups,
'topology_maps': topology_maps,
})
@ -1514,7 +1516,10 @@ def module_add(request, pk):
module.device = device
module.save()
messages.success(request, "Added module {} to {}".format(module.name, module.device.name))
return redirect('dcim:device_inventory', pk=module.device.pk)
if '_addanother' in request.POST:
return redirect('dcim:module_add', pk=module.device.pk)
else:
return redirect('dcim:device_inventory', pk=module.device.pk)
else:
form = forms.ModuleForm()

View File

@ -1,7 +1,7 @@
from rest_framework import generics
from ipam.models import VRF, Role, RIR, Aggregate, Prefix, IPAddress, VLAN
from ipam.filters import AggregateFilter, PrefixFilter, IPAddressFilter, VLANFilter
from ipam.filters import AggregateFilter, PrefixFilter, IPAddressFilter, VLANFilter, VRFFilter
from . import serializers
@ -12,6 +12,7 @@ class VRFListView(generics.ListAPIView):
"""
queryset = VRF.objects.all()
serializer_class = serializers.VRFSerializer
filter_class = VRFFilter
class VRFDetailView(generics.RetrieveAPIView):

View File

@ -46,9 +46,14 @@ class PrefixFilter(django_filters.FilterSet):
action='search_by_parent',
label='Parent prefix',
)
vrf = django_filters.MethodFilter(
action='_vrf',
label='VRF',
)
# Duplicate of `vrf` for backward-compatibility
vrf_id = django_filters.MethodFilter(
action='vrf',
label='VRF (ID)',
action='_vrf',
label='VRF',
)
site_id = django_filters.ModelMultipleChoiceFilter(
name='site',
@ -84,7 +89,7 @@ class PrefixFilter(django_filters.FilterSet):
class Meta:
model = Prefix
fields = ['family', 'site_id', 'site', 'vrf_id', 'vrf', 'vlan_id', 'vlan_vid', 'status', 'role_id', 'role']
fields = ['family', 'site_id', 'site', 'vrf', 'vrf_id', 'vlan_id', 'vlan_vid', 'status', 'role_id', 'role']
def search(self, queryset, value):
value = value.strip()
@ -104,7 +109,7 @@ class PrefixFilter(django_filters.FilterSet):
except AddrFormatError:
return queryset.none()
def vrf(self, queryset, value):
def _vrf(self, queryset, value):
if str(value) == '':
return queryset
try:
@ -121,10 +126,14 @@ class IPAddressFilter(django_filters.FilterSet):
action='search',
label='Search',
)
vrf_id = django_filters.ModelMultipleChoiceFilter(
name='vrf',
queryset=VRF.objects.all(),
label='VRF (ID)',
vrf = django_filters.MethodFilter(
action='_vrf',
label='VRF',
)
# Duplicate of `vrf` for backward-compatibility
vrf_id = django_filters.MethodFilter(
action='_vrf',
label='VRF',
)
device_id = django_filters.ModelMultipleChoiceFilter(
name='interface__device',
@ -155,6 +164,17 @@ class IPAddressFilter(django_filters.FilterSet):
except AddrFormatError:
return queryset.none()
def _vrf(self, queryset, value):
if str(value) == '':
return queryset
try:
vrf_id = int(value)
except ValueError:
return queryset.none()
if vrf_id == 0:
return queryset.filter(vrf__isnull=True)
return queryset.filter(vrf__pk=value)
class VLANFilter(django_filters.FilterSet):
site_id = django_filters.ModelMultipleChoiceFilter(

View File

@ -124,6 +124,25 @@
</tr>
</table>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<strong>Rack Groups</strong>
</div>
{% if rack_groups %}
<table class="table table-hover panel-body">
{% for rg in rack_groups %}
<tr>
<td><i class="fa fa-fw fa-folder"></i> <a href="{{ rg.get_absolute_url }}">{{ rg.name }}</a></td>
<td>{{ rg.rack_count }}</td>
</tr>
{% endfor %}
</table>
{% else %}
<div class="panel-body text-muted">
None
</div>
{% endif %}
</div>
<div class="panel panel-default">
<div class="panel-heading">
<strong>Topology Maps</strong>
@ -132,7 +151,7 @@
<table class="table table-hover panel-body">
{% for tm in topology_maps %}
<tr>
<td><i class="fa fa-fw fa-map text-success"></i> <a href="{% url 'dcim-api:topology_map' slug=tm.slug %}" target="_blank">{{ tm }}</a></td>
<td><i class="fa fa-fw fa-map"></i> <a href="{% url 'dcim-api:topology_map' slug=tm.slug %}" target="_blank">{{ tm }}</a></td>
<td>{{ tm.description }}</td>
</tr>
{% endfor %}