Compare commits

...

30 Commits

Author SHA1 Message Date
Jeremy Stretch
7f353e88c9 Merge pull request #153 from digitalocean/develop
Release v1.0.7
2016-06-30 18:20:32 -04:00
Jeremy Stretch
2829303c74 Issue #140: Fixed Unicode bug in message (ObjectEditView) 2016-06-30 18:04:14 -04:00
Jeremy Stretch
c9bf10421b Fixes #136: Trigger error on prefix w/host bits set instead of silently converting it 2016-06-30 17:13:55 -04:00
Jeremy Stretch
d2bcd71b32 Fixes #143: Noted that the lowest occupied U is used for mounting multi-U devices 2016-06-30 16:19:48 -04:00
Jeremy Stretch
3ea12c646a Fixes #109: Hide navbar for anonymous users when LOGIN_REQUIRED = True 2016-06-30 16:02:18 -04:00
Jeremy Stretch
381639d4a7 Wrapped pip updates inside sudo 2016-06-30 14:59:52 -04:00
Jeremy Stretch
cf17088b0a Merge pull request #145 from ryanmerolle/patch-1
standardized app installation commands
2016-06-30 14:38:31 -04:00
ryanmerolle
a165445808 Update getting-started.md 2016-06-30 13:11:43 -04:00
ryanmerolle
66d8c27b1e standardized app installation commands
* Standardized all install commands to use sudo & -y to avoid new user confusion.
* Added required missing installation (apache2 missing in Web Server and gunicorn section)
* Consolidated installs for a section to follow same format (Web Server and gunicorn section nginx)
* Added missing install argument when installing the arbitrary precision calculator language.
2016-06-30 13:02:06 -04:00
Jeremy Stretch
85f3324d97 Fixes #141: Removed invalid character 2016-06-30 11:39:25 -04:00
Jeremy Stretch
a010a6dde5 Corrected instruction for cloning the repo 2016-06-29 23:49:59 -04:00
Jeremy Stretch
1c49909e2c Fixed typo 2016-06-29 23:16:16 -04:00
Jeremy Stretch
019daf5524 Fixes #135: Add button to toggle navbar on small screens 2016-06-29 22:51:10 -04:00
Jeremy Stretch
519ab21ba0 Version bump for next release 2016-06-29 17:48:11 -04:00
Jeremy Stretch
26286b6e36 Merge pull request #125 from digitalocean/develop
Release v1.0.6
2016-06-29 17:43:40 -04:00
Jeremy Stretch
d520d78380 Merge pull request #119 from bellwood/lets-encrypt-nginx-ssl
Update getting-started.md
2016-06-29 17:32:49 -04:00
Jeremy Stretch
46ae4b307c Removed note about graphviz being optional; installing graphviz prevents confusing error messages 2016-06-29 16:44:56 -04:00
Jeremy Stretch
1728d81677 Added a note abotu upgrade.sh to the README 2016-06-29 16:41:23 -04:00
Jeremy Stretch
fc5495eb3b Introduced a script to assist with upgrading NetBox 2016-06-29 15:43:42 -04:00
Jeremy Stretch
004f5c448e Fixes #117: Improved device import validation 2016-06-29 14:53:24 -04:00
Jeremy Stretch
995447ae0b Suppressed '__all__' field name in BulkImportForm validation 2016-06-29 14:52:02 -04:00
bellwood
76baa6fd2d Update getting-started.md
Adding instructions for Let's Encrypt SSL and enabling HTTPS in nginx
2016-06-29 14:38:28 -04:00
Jeremy Stretch
2e27389cda Corrected capitalization of rack face in example 2016-06-29 14:16:07 -04:00
Jeremy Stretch
48d607fb96 Added VERSION to settings and page footer 2016-06-29 14:05:01 -04:00
Jeremy Stretch
b8b173674f Fixed PEP8 error 2016-06-29 13:38:51 -04:00
Jeremy Stretch
d6920eceb1 Merge pull request #100 from pitkley/replace-pydot
Replace pydot with graphviz
2016-06-29 12:53:30 -04:00
Jeremy Stretch
fbbdb3807c Fixes #108: Added search for Sites 2016-06-29 12:06:37 -04:00
Jeremy Stretch
a1953bab8b Added a link to the GitHub issues page to the server error page 2016-06-29 11:04:34 -04:00
Jeremy Stretch
aa000bf26d Fixes #110: Added status field to bulk editing form for Prefixes and VLANs 2016-06-29 10:52:06 -04:00
Pit Kleyersburg
522a0c20e7 Replace pydot by graphviz
This is in an effort to support Python 3: pydot is not compatible with
Python 3, while graphviz is.
2016-06-29 11:25:36 +02:00
21 changed files with 260 additions and 68 deletions

2
.gitignore vendored
View File

@@ -2,5 +2,5 @@
configuration.py
.idea
/*.sh
!upgrade.sh
fabfile.py

View File

@@ -25,6 +25,8 @@ Questions? Comments? Please join us on IRC in **#netbox** on **irc.freenode.net*
Please see docs/getting-started.md for instructions on installing NetBox.
To upgrade NetBox, please download the [latest release](https://github.com/digitalocean/netbox/releases) and run `upgrade.sh`.
# Components
NetBox understands all of the physical and logical building blocks that comprise network infrastructure, and the manners in which they are all related.

View File

@@ -59,12 +59,12 @@ Note that assignment of components from templates occurs only at the time of dev
# Devices
Every piece of hardware which is installed within a rack exists in NetBox as a device. Devices are measured in rack units (U) and whether they are full depth. 0U devices which can be installed in a rack but don't consume vertical rack space (such as a vertically-mounted power distribution unit) can also be defined.
Every piece of hardware which is installed within a rack exists in NetBox as a device. Devices are measured in rack units (U) and depth. 0U devices which can be installed in a rack but don't consume vertical rack space (such as a vertically-mounted power distribution unit) can also be defined.
When assigning a multi-U device to a rack, it is considered to be mounted in the lowest-numbered rack unit which it occupies. For example, a 3U device which occupies U8 through U10 shows as being mounted in U8.
A device is said to be "full depth" if its installation on one rack face prevents the installation of any other device on the opposite face within the same rack unit(s). This could be either because the device is physically too deep to allow a device behind it, or because the installation of an opposing device would impede air flow.
Each device has a physical device type (make and model), which is discussed below.
### Roles
NetBox allows for the definition of arbitrary device roles by which devices can be organized. For example, you might create roles for core switches, distribution switches, and access switches. In the interest of simplicity, device can only belong to one device role.

View File

@@ -15,7 +15,7 @@ The following packages are needed to install PostgreSQL:
* python-psycopg2
```
# apt-get install postgresql libpq-dev python-psycopg2
# sudo apt-get install -y postgresql libpq-dev python-psycopg2
```
## Configuration
@@ -58,14 +58,12 @@ NetBox requires following dependencies:
* libxml2-dev
* libxslt1-dev
* libffi-dev
* graphviz*
* graphviz
```
# apt-get install python2.7 python-dev git python-pip libxml2-dev libxslt1-dev libffi-dev graphviz
# sudo apt-get install -y python2.7 python-dev git python-pip libxml2-dev libxslt1-dev libffi-dev graphviz
```
*graphviz is needed to render topology maps. If you have no need for this feature, graphviz is not required.
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
@@ -92,13 +90,13 @@ Create the base directory for the NetBox installation. For this guide, we'll use
If `git` is not already installed, install it:
```
# sudo apt-get install git
# sudo apt-get install -y git
```
Next, clone the NetBox git repository into the current directory:
Next, clone the **master** branch of the NetBox GitHub repository into the current directory:
```
# git clone https://github.com/digitalocean/netbox.git .
# git clone -b master https://github.com/digitalocean/netbox.git .
Cloning into '.'...
remote: Counting objects: 1994, done.
remote: Compressing objects: 100% (150/150), done.
@@ -113,7 +111,7 @@ Checking connectivity... done.
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.)
```
# pip install -r requirements.txt
# sudo pip install -r requirements.txt
```
## Configuration
@@ -168,6 +166,7 @@ You may use the script located at `netbox/generate_secret_key.py` to generate a
Before NetBox can run, we need to install the database schema. This is done by running `./manage.py migrate` from the `netbox` directory (`/opt/netbox/netbox/` in our example):
```
# cd /opt/netbox/netbox/
# ./manage.py migrate
Operations to perform:
Apply all migrations: dcim, sessions, admin, ipam, utilities, auth, circuits, contenttypes, extras, secrets, users
@@ -236,7 +235,7 @@ If the test service does not run, or you cannot reach the NetBox home page, some
We'll set up a simple HTTP front end using [gunicorn](http://gunicorn.org/) for the purposes of this guide. For web servers, we provide example configurations for both [nginx](https://www.nginx.com/resources/wiki/) and [Apache](http://httpd.apache.org/docs/2.4). (You are of course free to use whichever combination of HTTP and WSGI services you'd like.) We'll also use [supervisord](http://supervisord.org/) for service persistence.
```
# apt-get install gunicorn supervisor
# sudo apt-get install -y gunicorn supervisor
```
## nginx Configuration
@@ -244,7 +243,7 @@ We'll set up a simple HTTP front end using [gunicorn](http://gunicorn.org/) for
The following will serve as a minimal nginx configuration. Be sure to modify your server name and installation path appropriately.
```
# apt-get install nginx
# sudo apt-get install -y nginx
```
Once nginx is installed, proceed with the following configuration:
@@ -287,7 +286,11 @@ Restart the nginx service to use the new configuration.
```
## Apache Configuration
The following configuration should work for Apache. Be sure to modify the `ServerName` appropriately.
```
# sudo apt-get install -y apache2
```
Once Apache is installed, proceed with the following configuration (Be sure to modify the `ServerName` appropriately):
```
<VirtualHost *:80>
@@ -323,7 +326,7 @@ Save the contents of the above example in `/etc/apache2/sites-available/netbox.c
## gunicorn Configuration
Save the following configuration file in the root netbox installation path (in this example, `/opt/netbox/`.) as `gunicorn_config.py`. Be sure to verify the location of the gunicorn executable (e.g. `which gunicorn`) and to update the `pythonpath` variable if needed.
Save the following configuration file in the root netbox installation path (in this example, `/opt/netbox/`) as `gunicorn_config.py`. Be sure to verify the location of the gunicorn executable (e.g. `which gunicorn`) and to update the `pythonpath` variable if needed.
```
command = '/usr/bin/gunicorn'
@@ -354,21 +357,98 @@ At this point, you should be able to connect to the nginx HTTP service at the se
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.
## Let's Encrypt SSL + nginx
To add SSL support to the installation we'll start by installing the arbitrary precision calculator language.
```
# sudo apt-get install -y bc
```
Next we'll clone Let's Encrypt into /opt/:
```
# sudo git clone https://github.com/letsencrypt/letsencrypt /opt/letsencrypt
```
To ensure Let's Encrypt can publicly access the directory it needs for certificate validation you'll need to edit `/etc/nginx/sites-available/netbox` and add:
```
location /.well-known/ {
alias /opt/netbox/netbox/.well-known/;
allow all;
}
```
Then restart nginix:
```
# sudo services nginx restart
```
To create the certificate use the following commands ensuring to change `netbox.example.com` to the domain name of the server:
```
# cd /opt/letsencrypt
# ./letsencrypt-auto certonly -a webroot --webroot-path=/opt/netbox/netbox/ -d netbox.example.com
```
If you wish to add support for the `www` prefix you'd use:
```
# cd /opt/letsencrypt
# ./letsencrypt-auto certonly -a webroot --webroot-path=/opt/netbox/netbox/ -d netbox.example.com -d www.netbox.example.com
```
Make sure you have DNS records setup for the hostnames you use and that they resolve back the netbox server.
You will be prompted for your email address to receive notifications about your SSL and then asked to accept the subscriber agreement.
If successful you'll now have four files in `/etc/letsencrypt/live/netbox.example.com` (remember, your hostname is different)
```
cert.pem
chain.pem
fullchain.pem
privkey.pem
```
Now edit your nginx configuration `/etc/nginx/sites-available/netbox` and at the top edit to the following:
```
#listen 80;
#listen [::]80;
listen 443;
listen [::]443;
ssl on;
ssl_certificate /etc/letsencrypt/live/netbox.example.com/cert.pem;
ssl_certificate_key /etc/letsencrypt/live/netbox.example.com/privkey.pem;
```
If you are not using IPv6 then you do not need `listen [::]443;` The two commented lines are for non-SSL for both IPv4 and IPv6.
Lastly, restart nginx:
```
# sudo services nginx restart
```
You should now have netbox running on a SSL protected connection.
# 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."
As with the initial installation, you can upgrade NetBox by either downloading the latest release package or by cloning the `master` branch of the git repository. Once the new code is in place, run the upgrade script (which may need to be run as root depending on how your environment is configured).
```
# ./manage.py migrate
# ./upgrade.sh
```
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.
This script:
```
# ./manage.py collectstatic
```
* Installs or upgrades any new required Python packages
* Applies any database migrations that were included in the release
* Collects all static files to be served by the HTTP service
Finally, restart the WSGI service to run the new code. If you followed this guide for the initial installation, this is done using `supervisorctl`:

View File

@@ -8,6 +8,27 @@ from .models import (
)
class SiteFilter(django_filters.FilterSet):
q = django_filters.MethodFilter(
action='search',
label='Search',
)
class Meta:
model = Site
fields = ['q', 'name', 'facility', 'asn']
def search(self, queryset, value):
value = value.strip()
qs_filter = Q(name__icontains=value) | Q(facility__icontains=value) | Q(physical_address__icontains=value) | \
Q(shipping_address__icontains=value)
try:
qs_filter |= Q(asn=int(value))
except ValueError:
pass
return queryset.filter(qs_filter)
class RackGroupFilter(django_filters.FilterSet):
site_id = django_filters.ModelMultipleChoiceFilter(
name='site',

View File

@@ -326,10 +326,10 @@ class DeviceForm(forms.ModelForm, BootstrapMixin):
display_field='display_name',
attrs={'filter-for': 'position'}
))
position = forms.TypedChoiceField(required=False, empty_value=None, widget=APISelect(
api_url='/api/dcim/racks/{{rack}}/rack-units/?face={{face}}',
disabled_indicator='device',
))
position = forms.TypedChoiceField(required=False, empty_value=None,
help_text="For multi-U devices, this is the lowest occupied rack unit.",
widget=APISelect(api_url='/api/dcim/racks/{{rack}}/rack-units/?face={{face}}',
disabled_indicator='device'))
manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(),
widget=forms.Select(attrs={'filter-for': 'device_type'}))
device_type = forms.ModelChoiceField(queryset=DeviceType.objects.all(), label='Model', widget=APISelect(
@@ -427,7 +427,7 @@ class DeviceFromCSVForm(forms.ModelForm):
'invalid_choice': 'Invalid site name.',
})
rack_name = forms.CharField()
face = forms.ChoiceField(choices=[('Front', 'Front'), ('Rear', 'Rear')])
face = forms.CharField(required=False)
class Meta:
model = Device
@@ -446,7 +446,7 @@ class DeviceFromCSVForm(forms.ModelForm):
try:
self.instance.device_type = DeviceType.objects.get(manufacturer=manufacturer, model=model_name)
except DeviceType.DoesNotExist:
self.add_error('model_name', "Invalid device type ({})".format(model_name))
self.add_error('model_name', "Invalid device type ({} {})".format(manufacturer, model_name))
# Validate rack
if site and rack_name:
@@ -457,11 +457,15 @@ class DeviceFromCSVForm(forms.ModelForm):
def clean_face(self):
face = self.cleaned_data['face']
if face.lower() == 'front':
return 0
if face.lower() == 'rear':
return 1
raise forms.ValidationError("Invalid rack face ({})".format(face))
if face:
try:
return {
'front': 0,
'rear': 1,
}[face.lower()]
except KeyError:
raise forms.ValidationError('Invalid rack face ({}); must be "front" or "rear".'.format(face))
return face
class DeviceImportForm(BulkImportForm, BootstrapMixin):

View File

@@ -568,7 +568,10 @@ class Device(CreatedUpdatedModel):
raise ValidationError("Must specify rack face with rack position.")
# Validate rack space
rack_face = self.face if not self.device_type.is_full_depth else None
try:
rack_face = self.face if not self.device_type.is_full_depth else None
except DeviceType.DoesNotExist:
raise ValidationError("Must specify device type.")
exclude_list = [self.pk] if self.pk else []
try:
available_units = self.rack.get_available_units(u_height=self.device_type.u_height, rack_face=rack_face,

View File

@@ -61,6 +61,7 @@ def expand_pattern(string):
class SiteListView(ObjectListView):
queryset = Site.objects.all()
filter = filters.SiteFilter
table = tables.SiteTable
template_name = 'dcim/site_list.html'

View File

@@ -1,4 +1,4 @@
import pydot
import graphviz
from rest_framework import generics
from rest_framework.views import APIView
import tempfile
@@ -49,32 +49,30 @@ class TopologyMapView(APIView):
tmap = get_object_or_404(TopologyMap, slug=slug)
# Construct the graph
graph = pydot.Dot(graph_type='graph', ranksep='1')
graph = graphviz.Graph()
graph.graph_attr['ranksep'] = '1'
for i, device_set in enumerate(tmap.device_sets):
subgraph = pydot.Subgraph('sg{}'.format(i), rank='same')
subgraph = graphviz.Graph(name='sg{}'.format(i))
subgraph.graph_attr['rank'] = 'same'
# Add a pseudonode for each device_set to enforce hierarchical layout
subgraph.add_node(pydot.Node('set{}'.format(i), shape='none', width='0', label=''))
subgraph.node('set{}'.format(i), label='', shape='none', width='0')
if i:
graph.add_edge(pydot.Edge('set{}'.format(i - 1), 'set{}'.format(i), style='invis'))
graph.edge('set{}'.format(i - 1), 'set{}'.format(i), style='invis')
# Add each device to the graph
devices = []
for query in device_set.split(','):
devices += Device.objects.filter(name__regex=query)
for d in devices:
node = pydot.Node(d.name)
subgraph.add_node(node)
subgraph.node(d.name)
# Add an invisible connection to each successive device in a set to enforce horizontal order
for j in range(0, len(devices) - 1):
edge = pydot.Edge(devices[j].name, devices[j + 1].name)
# edge.set('style', 'invis') doesn't seem to work for some reason
edge.set_style('invis')
subgraph.add_edge(edge)
subgraph.edge(devices[j].name, devices[j + 1].name, style='invis')
graph.add_subgraph(subgraph)
graph.subgraph(subgraph)
# Compile list of all devices
device_superset = Q()
@@ -87,17 +85,14 @@ class TopologyMapView(APIView):
connections = InterfaceConnection.objects.filter(interface_a__device__in=devices,
interface_b__device__in=devices)
for c in connections:
edge = pydot.Edge(c.interface_a.device.name, c.interface_b.device.name)
graph.add_edge(edge)
graph.edge(c.interface_a.device.name, c.interface_b.device.name)
# Write the image to disk and return
topo_file = tempfile.NamedTemporaryFile()
# Get the image data and return
try:
graph.write(topo_file.name, format='png')
topo_data = graph.pipe(format='png')
except:
return HttpResponse("There was an error generating the requested graph. Ensure that the GraphViz "
"executables have been installed correctly.")
response = HttpResponse(FileWrapper(topo_file), content_type='image/png')
topo_file.close()
response = HttpResponse(topo_data, content_type='image/png')
return response

View File

@@ -10,7 +10,13 @@ from .lookups import (
)
def prefix_validator(prefix):
if prefix.ip != prefix.cidr.ip:
raise ValidationError("{} is not a valid prefix. Did you mean {}?".format(prefix, prefix.cidr))
class BaseIPField(models.Field):
default_validators = [prefix_validator]
def python_type(self):
return IPNetwork

View File

@@ -13,6 +13,10 @@ from .models import (
)
FORM_PREFIX_STATUS_CHOICES = (('', '---------'),) + PREFIX_STATUS_CHOICES
FORM_VLAN_STATUS_CHOICES = (('', '---------'),) + VLAN_STATUS_CHOICES
#
# VRFs
#
@@ -215,6 +219,7 @@ class PrefixBulkEditForm(forms.Form, BootstrapMixin):
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF',
help_text="Select the VRF to assign, or check below to remove VRF assignment")
vrf_global = forms.BooleanField(required=False, label='Set VRF to global')
status = forms.ChoiceField(choices=FORM_PREFIX_STATUS_CHOICES, required=False)
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
description = forms.CharField(max_length=50, required=False)
@@ -444,6 +449,7 @@ class VLANImportForm(BulkImportForm, BootstrapMixin):
class VLANBulkEditForm(forms.Form, BootstrapMixin):
pk = forms.ModelMultipleChoiceField(queryset=VLAN.objects.all(), widget=forms.MultipleHiddenInput)
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
status = forms.ChoiceField(choices=FORM_VLAN_STATUS_CHOICES, required=False)
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)

View File

@@ -11,6 +11,8 @@ except ImportError:
"the documentation.")
VERSION = '1.0.7'
# Import local configuration
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
try:

View File

@@ -17,6 +17,8 @@
<div class="panel-body">
<p>There was a problem with your request. This error has been logged and administrative staff have
been notified. Please return to the home page and try again.</p>
<p>If you are responsible for this installation, please consider
<a href="https://github.com/digitalocean/netbox/issues">filing a bug report</a>.</p>
<div class="text-right">
<a href="/" class="btn btn-primary">Home Page</a>
</div>

View File

@@ -13,9 +13,16 @@
<nav class="navbar navbar-default navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">NetBox</a>
</div>
<div id="navbar" class="navbar-collapse collapse">
{% if request.user.is_authenticated or not settings.LOGIN_REQUIRED %}
<ul class="nav navbar-nav">
<li class="dropdown{% if request.path|startswith:'/dcim/sites/' %} active{% endif %}">
{% if perms.dcim.add_site %}
@@ -201,11 +208,12 @@
</li>
{% endif %}
</ul>
{% endif %}
<ul class="nav navbar-nav navbar-right">
{% if request.user.is_staff %}
<li><a href="{% url 'admin:index' %}"><i class="glyphicon glyphicon-cog" aria-hidden="true"></i> Admin</a></li>
{% endif %}
{% if request.user.is_authenticated %}
{% if request.user.is_staff %}
<li><a href="{% url 'admin:index' %}"><i class="glyphicon glyphicon-cog" aria-hidden="true"></i> Admin</a></li>
{% endif %}
<li><a href="{% url 'users:profile' %}"><i class="glyphicon glyphicon-user" aria-hidden="true"></i> Profile</a></li>
<li><a href="{% url 'logout' %}"><i class="glyphicon glyphicon-log-out" aria-hidden="true"></i> Log out</a></li>
{% else %}
@@ -237,7 +245,7 @@
<div class="container">
<div class="row">
<div class="col-md-4">
<p class="text-muted">{{ settings.HOSTNAME }}</p>
<p class="text-muted">{{ settings.HOSTNAME }} (v{{ settings.VERSION }})</p>
</div>
<div class="col-md-4 text-center">
<p class="text-muted">{% now 'Y-m-d H:i:s T' %}</p>

View File

@@ -68,18 +68,18 @@
</tr>
<tr>
<td>Position (U)</td>
<td>Numeric rack position (optional)</td>
<td>Lowest rack unit occupied by the device (optional)</td>
<td>21</td>
</tr>
<tr>
<td>Face</td>
<td>Rack face; front or rear (optional)</td>
<td>rear</td>
<td>Rear</td>
</tr>
</tbody>
</table>
<h4>Example</h4>
<pre>rack101_sw1,ToR Switch,Juniper,EX4300-48T,Juniper Junos,CAB00577291,Ashburn-VA,R101,21,rear</pre>
<pre>rack101_sw1,ToR Switch,Juniper,EX4300-48T,Juniper Junos,CAB00577291,Ashburn-VA,R101,21,Rear</pre>
</div>
</div>
{% endblock %}

View File

@@ -6,6 +6,26 @@
{% block title %}{{ site }}{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-9">
<ol class="breadcrumb">
<li><a href="{% url 'dcim:site_list' %}">Sites</a></li>
<li>{{ site }}</li>
</ol>
</div>
<div class="col-md-3">
<form action="{% url 'dcim:site_list' %}" method="get">
<div class="input-group">
<input type="text" name="q" class="form-control" placeholder="Search" />
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
</button>
</span>
</div>
</form>
</div>
</div>
<div class="pull-right">
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ site.name }}" data-url="{% url 'dcim-api:site_graphs' pk=site.pk %}" title="Show graphs">
<i class="glyphicon glyphicon-signal" aria-hidden="true"></i>

View File

@@ -14,5 +14,28 @@
{% include 'inc/export_button.html' with obj_type='sites' %}
</div>
<h1>Sites</h1>
{% render_table table 'table.html' %}
<div class="row">
<div class="col-md-9">
{% render_table table 'table.html' %}
</div>
<div class="col-md-3">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Search</strong>
</div>
<div class="panel-body">
<form action="{% url 'dcim:site_list' %}" method="get">
<div class="input-group">
<input type="text" name="q" class="form-control" placeholder="Name" {% if request.GET.q %}value="{{ request.GET.q }}" {% endif %}/>
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
</button>
</span>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -253,6 +253,9 @@ class BulkImportForm(forms.Form):
else:
for field, errors in obj_form.errors.items():
for e in errors:
self.add_error('csv', "Record {} ({}): {}".format(i, field, e))
if field == '__all__':
self.add_error('csv', "Record {}: {}".format(i, e))
else:
self.add_error('csv', "Record {} ({}): {}".format(i, field, e))
self.cleaned_data['csv'] = obj_list

View File

@@ -137,9 +137,9 @@ class ObjectEditView(View):
msg = 'Created ' if obj_created else 'Modified '
msg += self.model._meta.verbose_name
if hasattr(obj, 'get_absolute_url'):
msg += ' <a href="{}">{}</a>'.format(obj.get_absolute_url(), obj)
msg = '{} <a href="{}">{}</a>'.format(msg, obj.get_absolute_url(), obj)
else:
msg += ' {}'.format(obj)
msg = '{} {}'.format(msg, obj)
messages.success(request, msg)
if obj_created:
UserAction.objects.log_create(request.user, obj, msg)

View File

@@ -5,6 +5,7 @@ django-filter==0.13.0
django-rest-swagger==0.3.7
django-tables2==1.2.1
djangorestframework==3.3.3
graphviz==0.4.10
Markdown==2.6.6
ncclient==0.4.7
netaddr==0.7.18
@@ -12,6 +13,5 @@ paramiko==2.0.0
psycopg2==2.6.1
py-gfm==0.1.3
pycrypto==2.6.1
pydot==1.0.2
sqlparse==0.1.19
xmltodict==0.10.2

16
upgrade.sh Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
# This script will prepare NetBox to run after the code has been upgraded to
# its most recent release.
#
# Once the script completes, remember to restart the WSGI service (e.g.
# gunicorn or uWSGI).
# Install any new Python packages
echo "Updating required Python packages (pip install -r requirements.txt --upgrade)..."
sudo pip install -r requirements.txt --upgrade
# Apply any database migrations
./netbox/manage.py migrate
# Collect static files
./netbox/manage.py collectstatic --noinput