mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-21 17:38:05 +01:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e5f537cc5 | ||
|
|
7918f85cdd | ||
|
|
df1147d941 | ||
|
|
65bc91e9de | ||
|
|
9aa0972a8c | ||
|
|
4cd6f99cbd | ||
|
|
df01947c9e | ||
|
|
4dd31497e5 | ||
|
|
f958bc0580 | ||
|
|
0a22821209 | ||
|
|
a4cbfd7d5b | ||
|
|
f0fb60734a | ||
|
|
e334c64a7c | ||
|
|
6e068770ea | ||
|
|
d5d4eb9fd5 | ||
|
|
ab880e1053 | ||
|
|
1ea8f04c23 | ||
|
|
0b37d4f5e6 | ||
|
|
c6e66a073d | ||
|
|
eade3cbd6b | ||
|
|
7cf437e11b | ||
|
|
19a302774a | ||
|
|
a35d927235 | ||
|
|
1cd20861f2 | ||
|
|
e78263637a | ||
|
|
215c31e7a0 | ||
|
|
5935a8843e |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,6 +1,6 @@
|
||||
*.pyc
|
||||
configuration.py
|
||||
.idea
|
||||
*.sh
|
||||
/*.sh
|
||||
fabfile.py
|
||||
|
||||
|
||||
7
.travis.yml
Normal file
7
.travis.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
language: python
|
||||
python:
|
||||
- "2.7"
|
||||
install:
|
||||
- pip install -r requirements.txt
|
||||
script:
|
||||
- ./scripts/cibuild.sh
|
||||
@@ -48,3 +48,9 @@ Even if it's not quite right for NetBox, we may be able to point you to a tool b
|
||||
* A use case for the feature; who would use it and what value it would add to NetBox
|
||||
* 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
|
||||
|
||||
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.
|
||||
|
||||
19
README.md
19
README.md
@@ -1,7 +1,26 @@
|
||||
# NetBox
|
||||
|
||||
NetBox is an IP address management (IPAM) and data center infrastructure management (DCIM) tool. Initially conceived by the network engineering team at [DigitalOcean](https://www.digitalocean.com/), NetBox was developed specifically to address the needs of network and infrastructure engineers.
|
||||
|
||||
NetBox runs as a web application atop the [Django](https://www.djangoproject.com/) Python framework with a [PostgreSQL](http://www.postgresql.org/) database. For a complete list of requirements, see `requirements.txt`. The code is available [on GitHub](https://github.com/digitalocean/netbox).
|
||||
|
||||
Questions? Comments? Please join us on IRC in **#netbox** on **irc.freenode.net**!
|
||||
|
||||
### Build Status
|
||||
|
||||
| | python 2.7 |
|
||||
|-------------|------------|
|
||||
| **master** | [](https://travis-ci.org/digitalocean/netbox) |
|
||||
| **develop** | [](https://travis-ci.org/digitalocean/netbox) |
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
# Installation
|
||||
|
||||
Please see docs/getting-started.md for instructions on installing NetBox.
|
||||
|
||||
@@ -206,20 +206,26 @@ Now if we navigate to the name or IP of the server (as defined in `ALLOWED_HOSTS
|
||||
|
||||
If the test service does not run, or you cannot reach the NetBox home page, something has gone wrong. Do not proceed with the rest of this guide until the installation has been corrected.
|
||||
|
||||
# nginx and gunicorn
|
||||
# Web Server and gunicorn
|
||||
|
||||
## Installation
|
||||
|
||||
We'll set up a simple HTTP front end using [nginx](https://www.nginx.com/resources/wiki/) and [gunicorn](http://gunicorn.org/) for the purposes of this guide. (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.
|
||||
We'll set up a simple HTTP front end using [gunicorn](http://gunicorn.org/) for the purposes of this guide. For web servers, we have 2 configurations ready to go - we provide instructions 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 nginx gunicorn supervisor
|
||||
# apt-get install gunicorn supervisor
|
||||
```
|
||||
|
||||
## nginx Configuration
|
||||
|
||||
The following will serve as a minimal nginx configuration. Be sure to modify your server name and installation path appropriately.
|
||||
|
||||
```
|
||||
# apt-get install nginx
|
||||
```
|
||||
|
||||
Once nginx is installed, proceed with the following configuration:
|
||||
|
||||
```
|
||||
server {
|
||||
listen 80;
|
||||
@@ -256,13 +262,47 @@ Restart the nginx service to use the new configuration.
|
||||
# service nginx restart
|
||||
* Restarting nginx nginx
|
||||
```
|
||||
## Apache Configuration
|
||||
|
||||
If you're feeling adventurous, or you already have Apache installed and can't run a dual-stack on your server - an Apache configuration has been created:
|
||||
|
||||
```
|
||||
<VirtualHost *:80>
|
||||
ProxyPreserveHost On
|
||||
|
||||
ServerName netbox.totallycool.tld
|
||||
|
||||
Alias /static/ /opt/netbox/static/static
|
||||
|
||||
<Directory /opt/netbox/netbox/static>
|
||||
Options Indexes FollowSymLinks MultiViews
|
||||
AllowOverride None
|
||||
Order allow,deny
|
||||
Allow from all
|
||||
#Require all granted [UNCOMMENT THIS IF RUNNING APACHE 2.4]
|
||||
</Directory>
|
||||
|
||||
<Location /static>
|
||||
ProxyPass !
|
||||
</Location>
|
||||
|
||||
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`, add in the newly saved configuration and reload Apache:
|
||||
|
||||
```
|
||||
# a2ensite netbox; service apache2 restart
|
||||
```
|
||||
|
||||
## 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 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/local/bin/gunicorn'
|
||||
command = '/usr/bin/gunicorn'
|
||||
pythonpath = '/opt/netbox/netbox'
|
||||
bind = '127.0.0.1:8001'
|
||||
workers = 3
|
||||
@@ -289,3 +329,4 @@ 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.
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ Any prefixes you create in NetBox (discussed below) will be automatically organi
|
||||
|
||||
Regional Internet Registries (RIRs) are responsible for the allocation of global address space. The five RIRs are ARIN, RIPE, APNIC, LACNIC, and AFRINIC. However, some address space has been set aside for private or internal use only, such as defined in RFCs 1918 and 6598. NetBox considers these RFCs as a sort of RIR as well; that is, an authority which "owns" certain address space.
|
||||
|
||||
Each aggregate must be assigned to one RIR. NetBox by default will be populated with the RIRs listed above, however you are free to remove these and/or create your own if you choose.
|
||||
Each aggregate must be assigned to one RIR. You are free to define whichever RIRs you choose (or create your own).
|
||||
|
||||
---
|
||||
|
||||
|
||||
BIN
docs/screenshot1.png
Normal file
BIN
docs/screenshot1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 98 KiB |
BIN
docs/screenshot2.png
Normal file
BIN
docs/screenshot2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 134 KiB |
BIN
docs/screenshot3.png
Normal file
BIN
docs/screenshot3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 112 KiB |
@@ -47,6 +47,12 @@ class RackFilter(django_filters.FilterSet):
|
||||
queryset=RackGroup.objects.all(),
|
||||
label='Group (ID)',
|
||||
)
|
||||
group = django_filters.ModelMultipleChoiceFilter(
|
||||
name='group',
|
||||
queryset=RackGroup.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Group',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Rack
|
||||
|
||||
@@ -138,24 +138,23 @@ class RackForm(forms.ModelForm, BootstrapMixin):
|
||||
class RackFromCSVForm(forms.ModelForm):
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Site not found.'})
|
||||
group = forms.ModelChoiceField(queryset=RackGroup.objects.all(), required=False, to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Group not found.'})
|
||||
group_name = forms.CharField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = Rack
|
||||
fields = ['site', 'group', 'name', 'facility_id', 'u_height']
|
||||
fields = ['site', 'group_name', 'name', 'facility_id', 'u_height']
|
||||
|
||||
def clean(self):
|
||||
|
||||
site = self.cleaned_data.get('site')
|
||||
group = self.cleaned_data.get('group')
|
||||
group = self.cleaned_data.get('group_name')
|
||||
|
||||
# Validate device type
|
||||
# Validate rack group
|
||||
if site and group:
|
||||
try:
|
||||
self.instance.group = RackGroup.objects.get(site=site, name=group)
|
||||
except RackGroup.DoesNotExist:
|
||||
self.add_error('group', "Invalid rack group ({})".format(group))
|
||||
self.add_error('group_name', "Invalid rack group ({})".format(group))
|
||||
|
||||
|
||||
class RackImportForm(BulkImportForm, BootstrapMixin):
|
||||
|
||||
@@ -183,6 +183,17 @@ class Rack(CreatedUpdatedModel):
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:rack', args=[self.pk])
|
||||
|
||||
def clean(self):
|
||||
|
||||
# Validate that Rack is tall enough to house the installed Devices
|
||||
if self.pk:
|
||||
top_device = Device.objects.filter(rack=self).exclude(position__isnull=True).order_by('-position').first()
|
||||
if top_device:
|
||||
min_height = top_device.position + top_device.device_type.u_height - 1
|
||||
if self.u_height < min_height:
|
||||
raise ValidationError("Rack must be at least {}U tall with currently installed devices."
|
||||
.format(min_height))
|
||||
|
||||
def to_csv(self):
|
||||
return ','.join([
|
||||
self.site.name,
|
||||
|
||||
@@ -353,7 +353,7 @@ class ComponentTemplateCreateView(View):
|
||||
|
||||
if not form.errors:
|
||||
self.model.objects.bulk_create(component_templates)
|
||||
messages.success(request, "Added {} compontent(s) to {}".format(len(component_templates), devicetype))
|
||||
messages.success(request, "Added {} component(s) to {}".format(len(component_templates), devicetype))
|
||||
if '_addanother' in request.POST:
|
||||
return redirect(request.path)
|
||||
else:
|
||||
|
||||
@@ -395,16 +395,24 @@ def ipaddress(request, pk):
|
||||
|
||||
ipaddress = get_object_or_404(IPAddress.objects.select_related('interface__device'), pk=pk)
|
||||
|
||||
# Parent prefixes table
|
||||
parent_prefixes = Prefix.objects.filter(vrf=ipaddress.vrf, prefix__net_contains=str(ipaddress.address.ip))
|
||||
related_ips = IPAddress.objects.select_related('interface__device').exclude(pk=ipaddress.pk)\
|
||||
.filter(vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address))
|
||||
parent_prefixes_table = tables.PrefixBriefTable(parent_prefixes)
|
||||
|
||||
# Duplicate IPs table
|
||||
duplicate_ips = IPAddress.objects.filter(vrf=ipaddress.vrf, address=str(ipaddress.address))\
|
||||
.exclude(pk=ipaddress.pk).select_related('interface__device', 'nat_inside')
|
||||
duplicate_ips_table = tables.IPAddressBriefTable(duplicate_ips)
|
||||
|
||||
# Related IP table
|
||||
related_ips = IPAddress.objects.select_related('interface__device').exclude(address=str(ipaddress.address))\
|
||||
.filter(vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address))
|
||||
related_ips_table = tables.IPAddressBriefTable(related_ips)
|
||||
RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(related_ips_table)
|
||||
|
||||
return render(request, 'ipam/ipaddress.html', {
|
||||
'ipaddress': ipaddress,
|
||||
'parent_prefixes': parent_prefixes,
|
||||
'parent_prefixes_table': parent_prefixes_table,
|
||||
'duplicate_ips_table': duplicate_ips_table,
|
||||
'related_ips_table': related_ips_table,
|
||||
})
|
||||
|
||||
|
||||
@@ -219,7 +219,7 @@
|
||||
{% if settings.MAINTENANCE_MODE %}
|
||||
<div class="alert alert-warning text-center" role="alert">
|
||||
<h4><i class="fa fa-exclamation-triangle"></i> Maintenance Mode</h4>
|
||||
<p>The application is currently in maintenance mode.</p>
|
||||
<p>NetBox is currently in maintenance mode. Functionality may be limited.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for message in messages %}
|
||||
@@ -246,7 +246,7 @@
|
||||
<p class="text-muted">
|
||||
<i class="fa fa-fw fa-book text-primary"></i> <a href="{% url 'docs_root' %}">Docs</a> ·
|
||||
<i class="fa fa-fw fa-cloud text-primary"></i> <a href="/api/docs/">API</a> ·
|
||||
<i class="fa fa-fw fa-code text-primary"></i><a href="https://github.com/digitalocean/netbox">Code</a>
|
||||
<i class="fa fa-fw fa-code text-primary"></i> <a href="https://github.com/digitalocean/netbox">Code</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -119,31 +119,14 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Parent Prefixes</strong>
|
||||
</div>
|
||||
{% if parent_prefixes %}
|
||||
<table class="table table-hover panel-body">
|
||||
{% for p in parent_prefixes %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url 'ipam:prefix' pk=p.pk %}">{{ p }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{% if p.site %}
|
||||
<a href="{% url 'dcim:site' slug=p.site.slug %}">{{ p.site }}</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ p.status }}</td>
|
||||
<td>{{ p.role }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="panel-body text-muted">None</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% with heading='Parent Prefixes' %}
|
||||
{% render_table parent_prefixes_table 'panel_table.html' %}
|
||||
{% endwith %}
|
||||
{% if duplicate_ips_table.rows %}
|
||||
{% with heading='Duplicate IP Addresses' panel_class='danger' %}
|
||||
{% render_table duplicate_ips_table 'panel_table.html' %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% with heading='Related IP Addresses' %}
|
||||
{% render_table related_ips_table 'panel_table.html' %}
|
||||
{% endwith %}
|
||||
|
||||
@@ -19,11 +19,11 @@ def expand_pattern(string):
|
||||
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:
|
||||
if re.search(EXPANSION_PATTERN, remnant):
|
||||
for string in expand_pattern(remnant):
|
||||
yield "{}{}{}".format(lead, i, string)
|
||||
else:
|
||||
yield "{}{}".format(lead, i)
|
||||
yield "{}{}{}".format(lead, i, remnant)
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -120,7 +120,7 @@ class ObjectEditView(View):
|
||||
'obj': obj,
|
||||
'obj_type': self.model._meta.verbose_name,
|
||||
'form': form,
|
||||
'cancel_url': reverse(self.cancel_url) if self.cancel_url else obj.get_absolute_url(),
|
||||
'cancel_url': obj.get_absolute_url() if obj else reverse(self.cancel_url),
|
||||
})
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
@@ -157,7 +157,7 @@ class ObjectEditView(View):
|
||||
'obj': obj,
|
||||
'obj_type': self.model._meta.verbose_name,
|
||||
'form': form,
|
||||
'cancel_url': reverse(self.cancel_url) if self.cancel_url else obj.get_absolute_url(),
|
||||
'cancel_url': obj.get_absolute_url() if obj else reverse(self.cancel_url),
|
||||
})
|
||||
|
||||
|
||||
|
||||
28
scripts/cibuild.sh
Executable file
28
scripts/cibuild.sh
Executable file
@@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Exit code starts at 0 but is modified if any checks fail
|
||||
EXIT=0
|
||||
|
||||
# Output a line prefixed with a timestamp
|
||||
info()
|
||||
{
|
||||
echo "$(date +'%F %T') |"
|
||||
}
|
||||
|
||||
# Track number of seconds required to run script
|
||||
START=$(date +%s)
|
||||
echo "$(info) starting build checks."
|
||||
|
||||
# Syntax check all python source files
|
||||
SYNTAX=$(find . -name "*.py" -type f -exec python -m py_compile {} \; 2>&1)
|
||||
if [[ ! -z $SYNTAX ]]; then
|
||||
echo -e "$SYNTAX"
|
||||
echo -e "\n$(info) detected one or more syntax errors, failing build."
|
||||
EXIT=1
|
||||
fi
|
||||
|
||||
# Show build duration
|
||||
END=$(date +%s)
|
||||
echo "$(info) exiting with code $EXIT after $(($END - $START)) seconds."
|
||||
|
||||
exit $EXIT
|
||||
Reference in New Issue
Block a user