Skip to content

Commit

Permalink
Merge pull request #550 from digitalocean/develop
Browse files Browse the repository at this point in the history
Release v1.6.0
  • Loading branch information
jeremystretch authored Sep 13, 2016
2 parents 58e3d5a + 9591fb9 commit aeec678
Show file tree
Hide file tree
Showing 83 changed files with 1,349 additions and 562 deletions.
12 changes: 12 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
sudo: required

services:
- docker

env:
- DOCKER_TAG=$TRAVIS_TAG

language: python
python:
- "2.7"
Expand All @@ -6,3 +14,7 @@ install:
- pip install pep8
script:
- ./scripts/cibuild.sh
after_success:
- if [ ! -z "$TRAVIS_TAG" ] && [ "$TRAVIS_PULL_REQUEST" == "false" ]; then
./scripts/docker-build.sh;
fi
27 changes: 7 additions & 20 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,24 +1,11 @@
FROM ubuntu:14.04
FROM python:2.7-wheezy

RUN apt-get update && apt-get install -y \
python2.7 \
python-dev \
git \
python-pip \
libxml2-dev \
libxslt1-dev \
libffi-dev \
graphviz \
libpq-dev \
build-essential \
gunicorn \
--no-install-recommends \
&& rm -rf /var/lib/apt/lists/* \
&& mkdir -p /opt/netbox \
&& cd /opt/netbox \
&& git clone --depth 1 https://github.com/digitalocean/netbox.git -b master . \
&& pip install -r requirements.txt \
&& apt-get purge -y --auto-remove git build-essential
WORKDIR /opt/netbox

ARG BRANCH=master
ARG URL=https://github.com/digitalocean/netbox.git
RUN git clone --depth 1 $URL -b $BRANCH . && \
pip install gunicorn==17.5 && pip install -r requirements.txt

ADD docker/docker-entrypoint.sh /docker-entrypoint.sh
ADD netbox/netbox/configuration.docker.py /opt/netbox/netbox/netbox/configuration.py
Expand Down
29 changes: 28 additions & 1 deletion docs/data-model/extras.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,31 @@
This section entails features of NetBox which are not crucial to its primary functions, but that provide additional value.
This section entails features of NetBox which are not crucial to its primary functions, but provide additional value.

# Custom Fields

Each object in NetBox is represented in the database as a discrete table, and each attribute of an object exists as a column within its table. For example, sites are stored in the `dcim_site` table, which has columns named `name`, `facility`, `physical_address` and so on. As new attributes are added to objects throughout the development of NetBox, tables are expanded to include new rows.

However, some users might want to associate with objects attributes that are somewhat esoteric in nature, and that would not make sense to include in the core NetBox database schema. For instance, suppose your organization needs to associate each device with a ticket number pointing to the support ticket that was opened to have it installed. This is certainly a legitimate use for NetBox, but it's perhaps not a common enough need to warrant expanding the internal data schema. Instead, you can create a custom field to hold this data.

Custom fields must be created through the admin UI under Extras > Custom Fields. To create a new custom field, select the object(s) to which you want it to apply, and the type of field it will be. NetBox supports six field types:

* Free-form text (up to 255 characters)
* Integer
* Boolean (true/false)
* Date
* URL
* Selection

Assign the field a name. This should be a simple database-friendly string, e.g. `tps_report`. You may optionally assign the field a human-friendly label (e.g. "TPS report") as well; the label will be displayed on forms. If a description is provided, it will appear beneath the field in a form.

Marking the field as required will require the user to provide a value for the field when creating a new object or when saving an existing object. A default value for the field may also be provided. Use "true" or "false" for boolean fields. (The default value has no effect for selection fields.)

When creating a selection field, you should create at least two choices. These choices will be arranged first by weight, with lower weights appearing higher in the list, and then alphabetically.

## Using Custom Fields

When a single object is edited, the form will include any custom fields which have been defined for the object type. These fields are included in the "Custom Fields" panel. On the backend, each custom field value is saved separately from the core object as an independent database call, so it's best to avoid adding too many custom fields per object.

When editing multiple objects, custom field values are saved in bulk. There is no significant difference in overhead when saving a custom field value for 100 objects versus one object. However, the bulk operation must be performed separately for each custom field.

# Export Templates

Expand Down
10 changes: 6 additions & 4 deletions netbox/circuits/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,20 @@

from circuits.models import Provider, CircuitType, Circuit
from dcim.api.serializers import SiteNestedSerializer, InterfaceNestedSerializer
from extras.api.serializers import CustomFieldSerializer
from tenancy.api.serializers import TenantNestedSerializer


#
# Providers
#

class ProviderSerializer(serializers.ModelSerializer):
class ProviderSerializer(CustomFieldSerializer, serializers.ModelSerializer):

class Meta:
model = Provider
fields = ['id', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']
fields = ['id', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments',
'custom_fields']


class ProviderNestedSerializer(ProviderSerializer):
Expand Down Expand Up @@ -43,7 +45,7 @@ class Meta(CircuitTypeSerializer.Meta):
# Circuits
#

class CircuitSerializer(serializers.ModelSerializer):
class CircuitSerializer(CustomFieldSerializer, serializers.ModelSerializer):
provider = ProviderNestedSerializer()
type = CircuitTypeNestedSerializer()
tenant = TenantNestedSerializer()
Expand All @@ -53,7 +55,7 @@ class CircuitSerializer(serializers.ModelSerializer):
class Meta:
model = Circuit
fields = ['id', 'cid', 'provider', 'type', 'tenant', 'site', 'interface', 'install_date', 'port_speed',
'upstream_speed', 'commit_rate', 'xconnect_id', 'comments']
'upstream_speed', 'commit_rate', 'xconnect_id', 'comments', 'custom_fields']


class CircuitNestedSerializer(CircuitSerializer):
Expand Down
19 changes: 11 additions & 8 deletions netbox/circuits/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,23 @@
from circuits.models import Provider, CircuitType, Circuit
from circuits.filters import CircuitFilter

from extras.api.views import CustomFieldModelAPIView
from . import serializers


class ProviderListView(generics.ListAPIView):
class ProviderListView(CustomFieldModelAPIView, generics.ListAPIView):
"""
List all providers
"""
queryset = Provider.objects.all()
queryset = Provider.objects.prefetch_related('custom_field_values__field')
serializer_class = serializers.ProviderSerializer


class ProviderDetailView(generics.RetrieveAPIView):
class ProviderDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
"""
Retrieve a single provider
"""
queryset = Provider.objects.all()
queryset = Provider.objects.prefetch_related('custom_field_values__field')
serializer_class = serializers.ProviderSerializer


Expand All @@ -38,18 +39,20 @@ class CircuitTypeDetailView(generics.RetrieveAPIView):
serializer_class = serializers.CircuitTypeSerializer


class CircuitListView(generics.ListAPIView):
class CircuitListView(CustomFieldModelAPIView, generics.ListAPIView):
"""
List circuits (filterable)
"""
queryset = Circuit.objects.select_related('type', 'tenant', 'provider', 'site', 'interface__device')
queryset = Circuit.objects.select_related('type', 'tenant', 'provider', 'site', 'interface__device')\
.prefetch_related('custom_field_values__field')
serializer_class = serializers.CircuitSerializer
filter_class = CircuitFilter


class CircuitDetailView(generics.RetrieveAPIView):
class CircuitDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
"""
Retrieve a single circuit
"""
queryset = Circuit.objects.select_related('type', 'tenant', 'provider', 'site', 'interface__device')
queryset = Circuit.objects.select_related('type', 'tenant', 'provider', 'site', 'interface__device')\
.prefetch_related('custom_field_values__field')
serializer_class = serializers.CircuitSerializer
5 changes: 3 additions & 2 deletions netbox/circuits/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
from django.db.models import Q

from dcim.models import Site
from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant
from .models import Provider, Circuit, CircuitType


class ProviderFilter(django_filters.FilterSet):
class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
q = django_filters.MethodFilter(
action='search',
label='Search',
Expand Down Expand Up @@ -36,7 +37,7 @@ def search(self, queryset, value):
)


class CircuitFilter(django_filters.FilterSet):
class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
q = django_filters.MethodFilter(
action='search',
label='Search',
Expand Down
15 changes: 9 additions & 6 deletions netbox/circuits/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from django.db.models import Count

from dcim.models import Site, Device, Interface, Rack, IFACE_FF_VIRTUAL
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
from tenancy.forms import bulkedit_tenant_choices
from tenancy.models import Tenant
from utilities.forms import (
Expand All @@ -15,7 +16,7 @@
# Providers
#

class ProviderForm(forms.ModelForm, BootstrapMixin):
class ProviderForm(BootstrapMixin, CustomFieldForm):
slug = SlugField()
comments = CommentField()

Expand Down Expand Up @@ -46,7 +47,7 @@ class ProviderImportForm(BulkImportForm, BootstrapMixin):
csv = CSVDataField(csv_form=ProviderFromCSVForm)


class ProviderBulkEditForm(forms.Form, BootstrapMixin):
class ProviderBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
pk = forms.ModelMultipleChoiceField(queryset=Provider.objects.all(), widget=forms.MultipleHiddenInput)
asn = forms.IntegerField(required=False, label='ASN')
account = forms.CharField(max_length=30, required=False, label='Account number')
Expand All @@ -61,7 +62,8 @@ def provider_site_choices():
return [(s.slug, s.name) for s in site_choices]


class ProviderFilterForm(forms.Form, BootstrapMixin):
class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Provider
site = forms.MultipleChoiceField(required=False, choices=provider_site_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))

Expand All @@ -82,7 +84,7 @@ class Meta:
# Circuits
#

class CircuitForm(forms.ModelForm, BootstrapMixin):
class CircuitForm(BootstrapMixin, CustomFieldForm):
site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.Select(attrs={'filter-for': 'rack'}))
rack = forms.ModelChoiceField(queryset=Rack.objects.all(), required=False, label='Rack',
widget=APISelect(api_url='/api/dcim/racks/?site_id={{site}}',
Expand Down Expand Up @@ -177,7 +179,7 @@ class CircuitImportForm(BulkImportForm, BootstrapMixin):
csv = CSVDataField(csv_form=CircuitFromCSVForm)


class CircuitBulkEditForm(forms.Form, BootstrapMixin):
class CircuitBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
pk = forms.ModelMultipleChoiceField(queryset=Circuit.objects.all(), widget=forms.MultipleHiddenInput)
type = forms.ModelChoiceField(queryset=CircuitType.objects.all(), required=False)
provider = forms.ModelChoiceField(queryset=Provider.objects.all(), required=False)
Expand Down Expand Up @@ -207,7 +209,8 @@ def circuit_site_choices():
return [(s.slug, u'{} ({})'.format(s.name, s.circuit_count)) for s in site_choices]


class CircuitFilterForm(forms.Form, BootstrapMixin):
class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Circuit
type = forms.MultipleChoiceField(required=False, choices=circuit_type_choices)
provider = forms.MultipleChoiceField(required=False, choices=circuit_provider_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
Expand Down
8 changes: 6 additions & 2 deletions netbox/circuits/models.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
from django.contrib.contenttypes.fields import GenericRelation
from django.core.urlresolvers import reverse
from django.db import models

from dcim.fields import ASNField
from dcim.models import Site, Interface
from extras.models import CustomFieldModel, CustomFieldValue
from tenancy.models import Tenant
from utilities.models import CreatedUpdatedModel


class Provider(CreatedUpdatedModel):
class Provider(CreatedUpdatedModel, CustomFieldModel):
"""
Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
stores information pertinent to the user's relationship with the Provider.
Expand All @@ -20,6 +22,7 @@ class Provider(CreatedUpdatedModel):
noc_contact = models.TextField(blank=True, verbose_name='NOC contact')
admin_contact = models.TextField(blank=True, verbose_name='Admin contact')
comments = models.TextField(blank=True)
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')

class Meta:
ordering = ['name']
Expand Down Expand Up @@ -58,7 +61,7 @@ def get_absolute_url(self):
return "{}?type={}".format(reverse('circuits:circuit_list'), self.slug)


class Circuit(CreatedUpdatedModel):
class Circuit(CreatedUpdatedModel, CustomFieldModel):
"""
A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
circuits. Each circuit is also assigned a CircuitType and a Site. A Circuit may be terminated to a specific device
Expand All @@ -78,6 +81,7 @@ class Circuit(CreatedUpdatedModel):
xconnect_id = models.CharField(max_length=50, blank=True, verbose_name='Cross-connect ID')
pp_info = models.CharField(max_length=100, blank=True, verbose_name='Patch panel/port(s)')
comments = models.TextField(blank=True)
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')

class Meta:
ordering = ['provider', 'cid']
Expand Down
22 changes: 0 additions & 22 deletions netbox/circuits/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,6 @@ class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView):
template_name = 'circuits/provider_bulk_edit.html'
default_redirect_url = 'circuits:provider_list'

def update_objects(self, pk_list, form):

fields_to_update = {}
for field in ['asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']:
if form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]

return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)


class ProviderBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'circuits.delete_provider'
Expand Down Expand Up @@ -159,19 +150,6 @@ class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView):
template_name = 'circuits/circuit_bulk_edit.html'
default_redirect_url = 'circuits:circuit_list'

def update_objects(self, pk_list, form):

fields_to_update = {}
if form.cleaned_data['tenant'] == 0:
fields_to_update['tenant'] = None
elif form.cleaned_data['tenant']:
fields_to_update['tenant'] = form.cleaned_data['tenant']
for field in ['type', 'provider', 'port_speed', 'commit_rate', 'comments']:
if form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]

return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)


class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'circuits.delete_circuit'
Expand Down
Loading

0 comments on commit aeec678

Please sign in to comment.