Merge pull request #1708 from digitalocean/develop

Release v2.2.5
This commit is contained in:
Jeremy Stretch 2017-11-14 13:25:11 -05:00 committed by GitHub
commit a5a7358d26
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
116 changed files with 862 additions and 628 deletions

View File

@ -4,23 +4,30 @@
remove the "is:open" filter from the search bar to include closed issues. remove the "is:open" filter from the search bar to include closed issues.
Check the appropriate type for your issue below by placing an x between the Check the appropriate type for your issue below by placing an x between the
brackets. If none of the below apply, please raise your issue for brackets. For assistance with installation issues, or for any other issues
discussion on our mailing list: other than those listed below, please raise your topic for discussion on
our mailing list:
https://groups.google.com/forum/#!forum/netbox-discuss https://groups.google.com/forum/#!forum/netbox-discuss
Please note that issues which do not fall under any of the below categories Please note that issues which do not fall under any of the below categories
will be closed. will be closed. Due to an excessive backlog of feature requests, we are
not currently accepting any proposals which extend NetBox's feature scope.
Do not prepend any sort of tag to your issue's title. An administrator will
review your issue and assign labels as appropriate.
---> --->
### Issue type ### Issue type
[ ] Feature request <!-- Requesting the implementation of a new feature --> [ ] Feature request <!-- An enhancement of existing functionality -->
[ ] Bug report <!-- Reporting unexpected or erroneous behavior --> [ ] Bug report <!-- Unexpected or erroneous behavior -->
[ ] Documentation <!-- Proposing a modification to the documentation --> [ ] Documentation <!-- A modification to the documentation -->
<!-- <!--
Please describe the environment in which you are running NetBox. (Be sure Please describe the environment in which you are running NetBox. (Be sure
to verify that you are running the latest stable release of NetBox before to verify that you are running the latest stable release of NetBox before
submitting a bug report.) submitting a bug report.) If you are submitting a bug report and have made
any changes to the code base, please first validate that your bug can be
recreated while running an official release.
--> -->
### Environment ### Environment
* Python version: <!-- Example: 3.5.4 --> * Python version: <!-- Example: 3.5.4 -->
@ -28,8 +35,8 @@
<!-- <!--
BUG REPORTS must include: BUG REPORTS must include:
* A list of the steps needed to reproduce the bug * A list of the steps needed for someone else to reproduce the bug
* A description of the expected behavior * A description of the expected and observed behavior
* Any relevant error messages (screenshots may also help) * Any relevant error messages (screenshots may also help)
FEATURE REQUESTS must include: FEATURE REQUESTS must include:

View File

@ -1,8 +1,8 @@
## Getting Help ## Getting Help
If you encounter any issues installing or using NetBox, try one of the If you encounter any issues installing or using NetBox, try one of the
following resources to get assistance. Please **do not** open a GitHub following resources to get assistance. Please **do not** open a GitHub issue
issue except to report bugs or request features. except to report bugs or request features.
### Mailing List ### Mailing List
@ -13,35 +13,32 @@ installation. You can find us [here](https://groups.google.com/forum/#!forum/net
### Freenode IRC ### Freenode IRC
For real-time discussion, you can join the #netbox channel on [Freenode](https://freenode.net/). For real-time discussion, you can join the #netbox channel on [Freenode](https://freenode.net/).
You can connect to Freenode at irc.freenode.net using an IRC client, or You can connect to Freenode at irc.freenode.net using an IRC client, or you can
you can use their [webchat client](https://webchat.freenode.net/). use their [webchat client](https://webchat.freenode.net/).
## Reporting Bugs ## Reporting Bugs
* First, ensure that you've installed the [latest stable version](https://github.com/digitalocean/netbox/releases) of * First, ensure that you've installed the [latest stable version](https://github.com/digitalocean/netbox/releases) of
NetBox. If you're running an older version, it's possible that the bug NetBox. If you're running an older version, it's possible that the bug has
has already been fixed. already been fixed.
* Next, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues) to see if the bug you've found has * Next, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues) to see if the bug you've found has already
already been reported. If you think you may be experiencing a reported been reported. If you think you may be experiencing a reported issue that
issue that hasn't already been resolved, please click "add a reaction" hasn't already been resolved, please click "add a reaction" in the top right
in the top right corner of the issue and add a thumbs up (+1). You might corner of the issue and add a thumbs up (+1). You mightalso want to add a
also want to add a comment describing how it's affecting your comment describing how it's affecting your installation. This will allow us to
installation. This will allow us to prioritize bugs based on how many prioritize bugs based on how many users are affected.
users are affected.
* If you haven't found an existing issue that describes your suspected * If you haven't found an existing issue that describes your suspected bug,
bug, please inquire about it on the mailing list. **Do not** file an please inquire about it on the mailing list. **Do not** file an issue until you
issue until you have received confirmation that it is in fact a bug. have received confirmation that it is in fact a bug. Invalid issues are very
Invalid issues are very distracting and slow the pace at which NetBox is distracting and slow the pace at which NetBox is developed.
developed.
* When submitting an issue, please be as descriptive as possible. Be * When submitting an issue, please be as descriptive as possible. Be sure to
sure to include: include:
* The environment in which NetBox is running * The environment in which NetBox is running
* The exact steps that can be taken to reproduce the issue (if * The exact steps that can be taken to reproduce the issue (if applicable)
applicable)
* Any error messages generated * Any error messages generated
* Screenshots (if applicable) * Screenshots (if applicable)
@ -49,71 +46,64 @@ sure to include:
The issue will be reviewed by a moderator after submission and the appropriate The issue will be reviewed by a moderator after submission and the appropriate
labels will be applied. labels will be applied.
* Keep in mind that we prioritize bugs based on their severity and how * Keep in mind that we prioritize bugs based on their severity and how much
much work is required to resolve them. It may take some time for someone work is required to resolve them. It may take some time for someone to address
to address your issue. your issue.
## Feature Requests ## Feature Requests
* First, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues) to see if the feature you're * First, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues) to see if the feature you're requesting
requesting is already listed. (Be sure to search closed issues as well, is already listed. (Be sure to search closed issues as well, since some
since some feature requests are rejected.) If the feature you'd like to feature requests have been rejected.) If the feature you'd like to see has
see has already been requested, click "add a reaction" in the top right already been requested and is open, click "add a reaction" in the top right
corner of the issue and add a thumbs up (+1). This ensures that the corner of the issue and add a thumbs up (+1). This ensures that the issue has
issue has a better chance of making it onto the roadmap. Also feel free a better chance of receiving attention. Also feel free to add a comment with
to add a comment with any additional justification for the feature. any additional justification for the feature. (However, note that comments with
(However, note that comments with no substance other than a "+1" will be no substance other than a "+1" will be deleted. Please use GitHub's reactions
deleted. Please use GitHub's reactions feature to indicate your feature to indicate your support.)
support.)
* While suggestions for new features are welcome, it's important to * Due to an excessive backlog of feature requests, we are not currently
limit the scope of NetBox's feature set to avoid feature creep. For accepting any proposals which substantially extend NetBox's functionality
example, the following features would be firmly out of scope for NetBox: beyond its current feature set. This includes the introduction of any new views
or models which have not already been proposed in an existing feature request.
* Ticket management
* Network state monitoring
* Acting as a DNS server
* Acting as an authentication server
* Before filing a new feature request, consider raising your idea on the * Before filing a new feature request, consider raising your idea on the
mailing list first. Feedback you receive there will help validate and mailing list first. Feedback you receive there will help validate and shape the
shape the proposed feature before filing a formal issue. proposed feature before filing a formal issue.
* Good feature requests are very narrowly defined. Be sure to enumerate * Good feature requests are very narrowly defined. Be sure to thoroughly
specific functionality and data schema. The more effort you put into describe the functionality and data model(s) being proposed. The more effort
writing a feature request, the better its chance is of being you put into writing a feature request, the better its chance is of being
implemented. Overly broad feature requests will be closed. implemented. Overly broad feature requests will be closed.
* When submitting a feature request on GitHub, be sure to include the * When submitting a feature request on GitHub, be sure to include the
following: following:
* A detailed description of the proposed functionality * A detailed description of the proposed functionality
* A use case for the feature; who would use it and what value it * A use case for the feature; who would use it and what value it would add
would add to NetBox to NetBox
* A rough description of changes necessary to the database schema * A rough description of changes necessary to the database schema (if
(if applicable) applicable)
* Any third-party libraries or other resources which would be * Any third-party libraries or other resources which would be involved
involved
* Please avoid prepending any sort of tag (e.g. "[Feature]") to the issue title. * Please avoid prepending any sort of tag (e.g. "[Feature]") to the issue
The issue will be reviewed by a moderator after submission and the appropriate title. The issue will be reviewed by a moderator after submission and the
labels will be applied. appropriate labels will be applied.
## Submitting Pull Requests ## Submitting Pull Requests
* Be sure to open an issue before starting work on a pull request, and * Be sure to open an issue before starting work on a pull request, and discuss
discuss your idea with the NetBox maintainers before beginning work. your idea with the NetBox maintainers before beginning work. This will help
This will help prevent wasting time on something that might we might not prevent wasting time on something that might we might not be able to implement.
be able to implement. When suggesting a new feature, also make sure it When suggesting a new feature, also make sure it won't conflict with any work
won't conflict with any work that's already in progress. that's already in progress.
* When submitting a pull request, please be sure to work off of the * When submitting a pull request, please be sure to work off of the `develop`
`develop` branch, rather than `master`. In NetBox, the `develop` branch branch, rather than `master`. The `develop` branch is used for ongoing
is used for ongoing development, while `master` is used for tagging new development, while `master` is used for tagging new stable releases.
stable releases.
* All code submissions should meet the following criteria (CI will * All code submissions should meet the following criteria (CI will enforce
enforce these checks): these checks):
* Python syntax is valid * Python syntax is valid
* All tests pass when run with `./manage.py test` * All tests pass when run with `./manage.py test`

View File

@ -1,16 +1,15 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.shortcuts import get_object_or_404
from rest_framework.decorators import detail_route from rest_framework.decorators import detail_route
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from django.shortcuts import get_object_or_404
from circuits import filters from circuits import filters
from circuits.models import Provider, CircuitTermination, CircuitType, Circuit from circuits.models import Provider, CircuitTermination, CircuitType, Circuit
from extras.models import Graph, GRAPH_TYPE_PROVIDER
from extras.api.serializers import RenderedGraphSerializer from extras.api.serializers import RenderedGraphSerializer
from extras.api.views import CustomFieldModelViewSet from extras.api.views import CustomFieldModelViewSet
from extras.models import Graph, GRAPH_TYPE_PROVIDER
from utilities.api import FieldChoicesViewSet, WritableSerializerMixin from utilities.api import FieldChoicesViewSet, WritableSerializerMixin
from . import serializers from . import serializers

View File

@ -1,7 +1,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import django_filters import django_filters
from django.db.models import Q from django.db.models import Q
from dcim.models import Site from dcim.models import Site

View File

@ -11,7 +11,6 @@ from utilities.forms import (
APISelect, BootstrapMixin, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, FilterChoiceField, APISelect, BootstrapMixin, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, FilterChoiceField,
SmallTextarea, SlugField, SmallTextarea, SlugField,
) )
from .models import Circuit, CircuitTermination, CircuitType, Provider from .models import Circuit, CircuitTermination, CircuitType, Provider

View File

@ -8,8 +8,8 @@ from django.utils.encoding import python_2_unicode_compatible
from dcim.fields import ASNField from dcim.fields import ASNField
from extras.models import CustomFieldModel, CustomFieldValue from extras.models import CustomFieldModel, CustomFieldValue
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.utils import csv_format
from utilities.models import CreatedUpdatedModel from utilities.models import CreatedUpdatedModel
from utilities.utils import csv_format
from .constants import * from .constants import *

View File

@ -1,14 +1,12 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import django_tables2 as tables import django_tables2 as tables
from django_tables2.utils import Accessor
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django_tables2.utils import Accessor
from utilities.tables import BaseTable, ToggleColumn from utilities.tables import BaseTable, ToggleColumn
from .models import Circuit, CircuitType, Provider from .models import Circuit, CircuitType, Provider
CIRCUITTYPE_ACTIONS = """ CIRCUITTYPE_ACTIONS = """
{% if perms.circuit.change_circuittype %} {% if perms.circuit.change_circuittype %}
<a href="{% url 'circuits:circuittype_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a> <a href="{% url 'circuits:circuittype_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>

View File

@ -1,14 +1,15 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from rest_framework import status
from rest_framework.test import APITestCase
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.urls import reverse from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from circuits.constants import TERM_SIDE_A, TERM_SIDE_Z
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
from dcim.models import Site from dcim.models import Site
from extras.models import Graph, GRAPH_TYPE_PROVIDER from extras.constants import GRAPH_TYPE_PROVIDER
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider, TERM_SIDE_A, TERM_SIDE_Z from extras.models import Graph
from users.models import Token from users.models import Token
from utilities.tests import HttpStatusMixin from utilities.tests import HttpStatusMixin

View File

@ -4,7 +4,6 @@ from django.conf.urls import url
from . import views from . import views
app_name = 'circuits' app_name = 'circuits'
urlpatterns = [ urlpatterns = [

View File

@ -15,7 +15,8 @@ from utilities.views import (
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
) )
from . import filters, forms, tables from . import filters, forms, tables
from .models import Circuit, CircuitTermination, CircuitType, Provider, TERM_SIDE_A, TERM_SIDE_Z from .constants import TERM_SIDE_A, TERM_SIDE_Z
from .models import Circuit, CircuitTermination, CircuitType, Provider
# #

View File

@ -1,19 +1,23 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import OrderedDict from collections import OrderedDict
from rest_framework import serializers from rest_framework import serializers
from rest_framework.validators import UniqueTogetherValidator from rest_framework.validators import UniqueTogetherValidator
from ipam.models import IPAddress
from circuits.models import Circuit, CircuitTermination from circuits.models import Circuit, CircuitTermination
from dcim.constants import (
CONNECTION_STATUS_CHOICES, IFACE_FF_CHOICES, IFACE_ORDERING_CHOICES, RACK_FACE_CHOICES, RACK_TYPE_CHOICES,
RACK_WIDTH_CHOICES, STATUS_CHOICES, SUBDEVICE_ROLE_CHOICES,
)
from dcim.models import ( from dcim.models import (
CONNECTION_STATUS_CHOICES, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBay, DeviceBayTemplate, DeviceType, DeviceRole, IFACE_FF_CHOICES, IFACE_ORDERING_CHOICES, Interface, DeviceBayTemplate, DeviceType, DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
InterfaceConnection, InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RACK_FACE_CHOICES, RACK_TYPE_CHOICES, RackReservation, RackRole, Region, Site,
RACK_WIDTH_CHOICES, Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHOICES,
) )
from extras.api.customfields import CustomFieldModelSerializer from extras.api.customfields import CustomFieldModelSerializer
from ipam.models import IPAddress
from tenancy.api.serializers import NestedTenantSerializer from tenancy.api.serializers import NestedTenantSerializer
from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer
from virtualization.models import Cluster from virtualization.models import Cluster

View File

@ -1,28 +1,30 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import OrderedDict from collections import OrderedDict
from django.conf import settings
from django.http import HttpResponseBadRequest, HttpResponseForbidden
from django.shortcuts import get_object_or_404
from rest_framework.decorators import detail_route from rest_framework.decorators import detail_route
from rest_framework.mixins import ListModelMixin from rest_framework.mixins import ListModelMixin
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ViewSet from rest_framework.viewsets import GenericViewSet, ModelViewSet, ViewSet
from django.conf import settings from dcim import filters
from django.http import HttpResponseBadRequest, HttpResponseForbidden
from django.shortcuts import get_object_or_404
from dcim.models import ( from dcim.models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
RackReservation, RackRole, Region, Site, RackReservation, RackRole, Region, Site,
) )
from dcim import filters
from extras.api.serializers import RenderedGraphSerializer from extras.api.serializers import RenderedGraphSerializer
from extras.api.views import CustomFieldModelViewSet from extras.api.views import CustomFieldModelViewSet
from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
from utilities.api import IsAuthenticatedOrLoginNotRequired, FieldChoicesViewSet, ServiceUnavailable, WritableSerializerMixin from utilities.api import (
from .exceptions import MissingFilterException IsAuthenticatedOrLoginNotRequired, FieldChoicesViewSet, ServiceUnavailable, WritableSerializerMixin,
)
from . import serializers from . import serializers
from .exceptions import MissingFilterException
# #
@ -256,12 +258,19 @@ class DeviceViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
device.platform device.platform
)) ))
# Check that NAPALM is installed and verify the configured driver # Check that NAPALM is installed
try: try:
import napalm import napalm
from napalm_base.exceptions import ConnectAuthError, ModuleImportError
except ImportError: except ImportError:
raise ServiceUnavailable("NAPALM is not installed. Please see the documentation for instructions.") raise ServiceUnavailable("NAPALM is not installed. Please see the documentation for instructions.")
# TODO: Remove support for NAPALM < 2.0
try:
from napalm.base.exceptions import ConnectAuthError, ModuleImportError
except ImportError:
from napalm_base.exceptions import ConnectAuthError, ModuleImportError
# Validate the configured driver
try: try:
driver = napalm.get_network_driver(device.platform.napalm_driver) driver = napalm.get_network_driver(device.platform.napalm_driver)
except ModuleImportError: except ModuleImportError:

View File

@ -1,22 +1,23 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import django_filters import django_filters
from netaddr import EUI
from netaddr.core import AddrFormatError
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db.models import Q from django.db.models import Q
from netaddr import EUI
from netaddr.core import AddrFormatError
from extras.filters import CustomFieldFilterSet from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.filters import NullableCharFieldFilter, NumericInFilter from utilities.filters import NullableCharFieldFilter, NumericInFilter
from virtualization.models import Cluster from virtualization.models import Cluster
from .constants import (
IFACE_FF_LAG, NONCONNECTABLE_IFACE_TYPES, STATUS_CHOICES, VIRTUAL_IFACE_TYPES, WIRELESS_IFACE_TYPES,
)
from .models import ( from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, STATUS_CHOICES, IFACE_FF_LAG, Interface, InterfaceConnection, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
InterfaceTemplate, Manufacturer, InventoryItem, NONCONNECTABLE_IFACE_TYPES, Platform, PowerOutlet, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, Region, Site, RackReservation, RackRole, Region, Site,
VIRTUAL_IFACE_TYPES, WIRELESS_IFACE_TYPES,
) )
@ -424,7 +425,8 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Device model (slug)', label='Device model (slug)',
) )
status = django_filters.MultipleChoiceFilter( status = django_filters.MultipleChoiceFilter(
choices=STATUS_CHOICES choices=STATUS_CHOICES,
null_value=None
) )
is_full_depth = django_filters.BooleanFilter( is_full_depth = django_filters.BooleanFilter(
name='device_type__is_full_depth', name='device_type__is_full_depth',

View File

@ -1,9 +1,8 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from netaddr import EUI, AddrFormatError
from django import forms from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from netaddr import EUI, AddrFormatError
# #

View File

@ -1,12 +1,12 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from mptt.forms import TreeNodeChoiceField
import re import re
from django import forms from django import forms
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.postgres.forms.array import SimpleArrayField from django.contrib.postgres.forms.array import SimpleArrayField
from django.db.models import Count, Q from django.db.models import Count, Q
from mptt.forms import TreeNodeChoiceField
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
from ipam.models import IPAddress from ipam.models import IPAddress
@ -19,17 +19,19 @@ from utilities.forms import (
SlugField, FilterTreeNodeMultipleChoiceField, SlugField, FilterTreeNodeMultipleChoiceField,
) )
from virtualization.models import Cluster from virtualization.models import Cluster
from .constants import (
CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_CONNECTED, IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES,
RACK_FACE_CHOICES, RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, RACK_WIDTH_19IN, RACK_WIDTH_23IN, STATUS_CHOICES,
SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT, SUBDEVICE_ROLE_CHOICES,
)
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, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate,
ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType, Interface, Device, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, InventoryItem,
IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate, Manufacturer, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation,
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_FACE_CHOICES, RackRole, Region, Site,
RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, RACK_WIDTH_19IN, RACK_WIDTH_23IN,
Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT, SUBDEVICE_ROLE_CHOICES,
) )
DEVICE_BY_PK_RE = '{\d+\}' DEVICE_BY_PK_RE = '{\d+\}'

View File

@ -1,12 +1,10 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import OrderedDict from collections import OrderedDict
from itertools import count, groupby from itertools import count, groupby
from mptt.models import MPTTModel, TreeForeignKey
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.fields import GenericRelation
from django.contrib.postgres.fields import ArrayField from django.contrib.postgres.fields import ArrayField
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@ -15,9 +13,10 @@ from django.db import models
from django.db.models import Count, Q, ObjectDoesNotExist from django.db.models import Count, Q, ObjectDoesNotExist
from django.urls import reverse from django.urls import reverse
from django.utils.encoding import python_2_unicode_compatible from django.utils.encoding import python_2_unicode_compatible
from mptt.models import MPTTModel, TreeForeignKey
from circuits.models import Circuit from circuits.models import Circuit
from extras.models import CustomFieldModel, CustomField, CustomFieldValue, ImageAttachment from extras.models import CustomFieldModel, CustomFieldValue, ImageAttachment
from extras.rpc import RPC_CLIENTS from extras.rpc import RPC_CLIENTS
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.fields import ColorField, NullableCharField from utilities.fields import ColorField, NullableCharField
@ -1118,6 +1117,15 @@ class ConsoleServerPort(models.Model):
def __str__(self): def __str__(self):
return self.name return self.name
def clean(self):
# Check that the parent device's DeviceType is a console server
device_type = self.device.device_type
if not device_type.is_console_server:
raise ValidationError("The {} {} device type not support assignment of console server ports.".format(
device_type.manufacturer, device_type
))
# #
# Power ports # Power ports
@ -1183,6 +1191,15 @@ class PowerOutlet(models.Model):
def __str__(self): def __str__(self):
return self.name return self.name
def clean(self):
# Check that the parent device's DeviceType is a PDU
device_type = self.device.device_type
if not device_type.is_pdu:
raise ValidationError("The {} {} device type not support assignment of power outlets.".format(
device_type.manufacturer, device_type
))
# #
# Interfaces # Interfaces
@ -1239,6 +1256,13 @@ class Interface(models.Model):
def clean(self): def clean(self):
# Check that the parent device's DeviceType is a network device
device_type = self.device.device_type
if not device_type.is_network_device:
raise ValidationError("The {} {} device type not support assignment of network interfaces.".format(
device_type.manufacturer, device_type
))
# An Interface must belong to a Device *or* to a VirtualMachine # An Interface must belong to a Device *or* to a VirtualMachine
if self.device and self.virtual_machine: if self.device and self.virtual_machine:
raise ValidationError("An interface cannot belong to both a device and a virtual machine.") raise ValidationError("An interface cannot belong to both a device and a virtual machine.")

View File

@ -44,10 +44,10 @@ class InterfaceQuerySet(QuerySet):
TYPE_RE = r"SUBSTRING({} FROM '^([^0-9]+)')" TYPE_RE = r"SUBSTRING({} FROM '^([^0-9]+)')"
ID_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)([0-9]+)$') AS integer)" ID_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)([0-9]+)$') AS integer)"
SLOT_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)([0-9]+)\/') AS integer)" SLOT_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?([0-9]+)\/') AS integer)"
SUBSLOT_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(?:[0-9]+\/)([0-9]+)') AS integer), 0)" SUBSLOT_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:[0-9]+\/)([0-9]+)') AS integer), 0)"
POSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(?:[0-9]+\/){{2}}([0-9]+)') AS integer), 0)" POSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:[0-9]+\/){{2}}([0-9]+)') AS integer), 0)"
SUBPOSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(?:[0-9]+\/){{3}}([0-9]+)') AS integer), 0)" SUBPOSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:[0-9]+\/){{3}}([0-9]+)') AS integer), 0)"
CHANNEL_RE = r"COALESCE(CAST(SUBSTRING({} FROM ':([0-9]+)(\.[0-9]+)?$') AS integer), 0)" CHANNEL_RE = r"COALESCE(CAST(SUBSTRING({} FROM ':([0-9]+)(\.[0-9]+)?$') AS integer), 0)"
VC_RE = r"COALESCE(CAST(SUBSTRING({} FROM '\.([0-9]+)$') AS integer), 0)" VC_RE = r"COALESCE(CAST(SUBSTRING({} FROM '\.([0-9]+)$') AS integer), 0)"

View File

@ -10,7 +10,6 @@ from .models import (
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, Region, Site, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, Region, Site,
) )
REGION_LINK = """ REGION_LINK = """
{% if record.get_children %} {% if record.get_children %}
<span style="padding-left: {{ record.get_ancestors|length }}0px "><i class="fa fa-caret-right"></i> <span style="padding-left: {{ record.get_ancestors|length }}0px "><i class="fa fa-caret-right"></i>

View File

@ -1,16 +1,16 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from rest_framework import status
from rest_framework.test import APITestCase
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.urls import reverse from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from dcim.constants import IFACE_FF_LAG, SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT
from dcim.models import ( from dcim.models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, IFACE_FF_LAG, Interface, InterfaceConnection, InterfaceTemplate, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
Manufacturer, InventoryItem, Platform, PowerPort, PowerPortTemplate, PowerOutlet, PowerOutletTemplate, Rack, RackGroup, InventoryItem, Platform, PowerPort, PowerPortTemplate, PowerOutlet, PowerOutletTemplate, Rack, RackGroup,
RackReservation, RackRole, Region, Site, SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT, RackReservation, RackRole, Region, Site,
) )
from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
from users.models import Token from users.models import Token
@ -1432,7 +1432,7 @@ class ConsoleServerPortTest(HttpStatusMixin, APITestCase):
site = Site.objects.create(name='Test Site 1', slug='test-site-1') site = Site.objects.create(name='Test Site 1', slug='test-site-1')
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1') manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
devicetype = DeviceType.objects.create( devicetype = DeviceType.objects.create(
manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1' manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1', is_console_server=True
) )
devicerole = DeviceRole.objects.create( devicerole = DeviceRole.objects.create(
name='Test Device Role 1', slug='test-device-role-1', color='ff0000' name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
@ -1590,7 +1590,7 @@ class PowerOutletTest(HttpStatusMixin, APITestCase):
site = Site.objects.create(name='Test Site 1', slug='test-site-1') site = Site.objects.create(name='Test Site 1', slug='test-site-1')
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1') manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
devicetype = DeviceType.objects.create( devicetype = DeviceType.objects.create(
manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1' manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1', is_pdu=True
) )
devicerole = DeviceRole.objects.create( devicerole = DeviceRole.objects.create(
name='Test Device Role 1', slug='test-device-role-1', color='ff0000' name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
@ -1667,7 +1667,7 @@ class InterfaceTest(HttpStatusMixin, APITestCase):
site = Site.objects.create(name='Test Site 1', slug='test-site-1') site = Site.objects.create(name='Test Site 1', slug='test-site-1')
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1') manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
devicetype = DeviceType.objects.create( devicetype = DeviceType.objects.create(
manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1' manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1', is_network_device=True
) )
devicerole = DeviceRole.objects.create( devicerole = DeviceRole.objects.create(
name='Test Device Role 1', slug='test-device-role-1', color='ff0000' name='Test Device Role 1', slug='test-device-role-1', color='ff0000'

View File

@ -5,9 +5,8 @@ from django.conf.urls import url
from extras.views import ImageAttachmentEditView from extras.views import ImageAttachmentEditView
from ipam.views import ServiceCreateView from ipam.views import ServiceCreateView
from secrets.views import secret_add from secrets.views import secret_add
from .models import Device, Rack, Site
from . import views from . import views
from .models import Device, Rack, Site
app_name = 'dcim' app_name = 'dcim'
urlpatterns = [ urlpatterns = [

View File

@ -1,6 +1,5 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import re
from natsort import natsorted
from operator import attrgetter from operator import attrgetter
from django.contrib import messages from django.contrib import messages
@ -15,10 +14,11 @@ from django.utils.html import escape
from django.utils.http import urlencode from django.utils.http import urlencode
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.views.generic import View from django.views.generic import View
from natsort import natsorted
from ipam.models import Prefix, Service, VLAN
from circuits.models import Circuit from circuits.models import Circuit
from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE, UserAction from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE, UserAction
from ipam.models import Prefix, Service, VLAN
from utilities.forms import ConfirmationForm from utilities.forms import ConfirmationForm
from utilities.paginator import EnhancedPaginator from utilities.paginator import EnhancedPaginator
from utilities.views import ( from utilities.views import (
@ -26,11 +26,12 @@ from utilities.views import (
ComponentEditView, ObjectDeleteView, ObjectEditView, ObjectListView, ComponentEditView, ObjectDeleteView, ObjectEditView, ObjectListView,
) )
from . import filters, forms, tables from . import filters, forms, tables
from .constants import CONNECTION_STATUS_CONNECTED
from .models import ( from .models import (
CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
RackGroup, RackReservation, RackRole, Region, Site, RackReservation, RackRole, Region, Site,
) )

View File

@ -1,15 +1,14 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from datetime import datetime
from rest_framework import serializers from datetime import datetime
from rest_framework.exceptions import ValidationError
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db import transaction from django.db import transaction
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from extras.models import ( from extras.constants import CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT
CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_SELECT, CustomField, CustomFieldChoice, CustomFieldValue, from extras.models import CustomField, CustomFieldChoice, CustomFieldValue
)
from utilities.api import ValidatedModelSerializer from utilities.api import ValidatedModelSerializer
@ -39,6 +38,15 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
# Data validation # Data validation
if value not in [None, '']: if value not in [None, '']:
# Validate integer
if cf.type == CF_TYPE_INTEGER:
try:
int(value)
except ValueError:
raise ValidationError(
"Invalid value for integer field {}: {}".format(field_name, value)
)
# Validate boolean # Validate boolean
if cf.type == CF_TYPE_BOOLEAN and value not in [True, False, 1, 0]: if cf.type == CF_TYPE_BOOLEAN and value not in [True, False, 1, 0]:
raise ValidationError( raise ValidationError(

View File

@ -1,14 +1,12 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from rest_framework import serializers from rest_framework import serializers
from dcim.api.serializers import NestedDeviceSerializer, NestedRackSerializer, NestedSiteSerializer from dcim.api.serializers import NestedDeviceSerializer, NestedRackSerializer, NestedSiteSerializer
from dcim.models import Device, Rack, Site from dcim.models import Device, Rack, Site
from extras.models import ( from extras.constants import ACTION_CHOICES, GRAPH_TYPE_CHOICES
ACTION_CHOICES, ExportTemplate, Graph, GRAPH_TYPE_CHOICES, ImageAttachment, ReportResult, TopologyMap, UserAction, from extras.models import ExportTemplate, Graph, ImageAttachment, ReportResult, TopologyMap, UserAction
)
from users.api.serializers import NestedUserSerializer from users.api.serializers import NestedUserSerializer
from utilities.api import ChoiceFieldSerializer, ContentTypeFieldSerializer, ValidatedModelSerializer from utilities.api import ChoiceFieldSerializer, ContentTypeFieldSerializer, ValidatedModelSerializer

View File

@ -1,13 +1,12 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from rest_framework.decorators import detail_route
from rest_framework.exceptions import PermissionDenied
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet, ViewSet
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.http import Http404, HttpResponse from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from rest_framework.decorators import detail_route
from rest_framework.exceptions import PermissionDenied
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet, ViewSet
from extras import filters from extras import filters
from extras.models import CustomField, ExportTemplate, Graph, ImageAttachment, ReportResult, TopologyMap, UserAction from extras.models import CustomField, ExportTemplate, Graph, ImageAttachment, ReportResult, TopologyMap, UserAction

View File

@ -1,12 +1,12 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import django_filters import django_filters
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from dcim.models import Site from dcim.models import Site
from .models import CF_TYPE_SELECT, CustomField, Graph, ExportTemplate, TopologyMap, UserAction from .constants import CF_TYPE_SELECT
from .models import CustomField, Graph, ExportTemplate, TopologyMap, UserAction
class CustomFieldFilter(django_filters.Filter): class CustomFieldFilter(django_filters.Filter):

View File

@ -1,14 +1,13 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import OrderedDict from collections import OrderedDict
from django import forms from django import forms
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from utilities.forms import BootstrapMixin, BulkEditForm, LaxURLField from utilities.forms import BootstrapMixin, BulkEditForm, LaxURLField
from .models import ( from .constants import CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL
CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL, CustomField, CustomFieldValue, from .models import CustomField, CustomFieldValue, ImageAttachment
ImageAttachment,
)
def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=False): def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=False):

View File

@ -10,7 +10,6 @@ from django.conf import settings
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.db.models import Model from django.db.models import Model
APPS = ['circuits', 'dcim', 'extras', 'ipam', 'secrets', 'tenancy', 'users', 'virtualization'] APPS = ['circuits', 'dcim', 'extras', 'ipam', 'secrets', 'tenancy', 'users', 'virtualization']
BANNER_TEXT = """### NetBox interactive shell ({node}) BANNER_TEXT = """### NetBox interactive shell ({node})

View File

@ -1,12 +1,12 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from getpass import getpass from getpass import getpass
from ncclient.transport.errors import AuthenticationError
from paramiko import AuthenticationException
from django.conf import settings from django.conf import settings
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from django.db import transaction from django.db import transaction
from ncclient.transport.errors import AuthenticationError
from paramiko import AuthenticationException
from dcim.models import Device, InventoryItem, Site, STATUS_ACTIVE from dcim.models import Device, InventoryItem, Site, STATUS_ACTIVE

View File

@ -3,7 +3,6 @@ from __future__ import unicode_literals
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.utils import timezone from django.utils import timezone
from extras.models import ReportResult
from extras.reports import get_reports from extras.reports import get_reports

View File

@ -2,6 +2,7 @@
# Generated by Django 1.11.4 on 2017-09-26 21:25 # Generated by Django 1.11.4 on 2017-09-26 21:25
from __future__ import unicode_literals from __future__ import unicode_literals
from distutils.version import StrictVersion from distutils.version import StrictVersion
import re
from django.conf import settings from django.conf import settings
import django.contrib.postgres.fields.jsonb import django.contrib.postgres.fields.jsonb
@ -18,7 +19,7 @@ def verify_postgresql_version(apps, schema_editor):
with connection.cursor() as cursor: with connection.cursor() as cursor:
cursor.execute("SELECT VERSION()") cursor.execute("SELECT VERSION()")
row = cursor.fetchone() row = cursor.fetchone()
pg_version = row[0].split()[1] pg_version = re.match('^PostgreSQL (\d+\.\d+(\.\d+)?)', row[0]).group(1)
if StrictVersion(pg_version) < StrictVersion('9.4.0'): if StrictVersion(pg_version) < StrictVersion('9.4.0'):
raise Exception("PostgreSQL 9.4.0 or higher is required ({} found). Upgrade PostgreSQL and then run migrations again.".format(pg_version)) raise Exception("PostgreSQL 9.4.0 or higher is required ({} found). Upgrade PostgreSQL and then run migrations again.".format(pg_version))

View File

@ -1,8 +1,9 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import OrderedDict from collections import OrderedDict
from datetime import date from datetime import date
import graphviz
import graphviz
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType

View File

@ -1,8 +1,9 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import OrderedDict
import importlib import importlib
import inspect import inspect
import pkgutil import pkgutil
from collections import OrderedDict
from django.conf import settings from django.conf import settings
from django.utils import timezone from django.utils import timezone

View File

@ -1,11 +1,11 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import re import re
import time import time
from ncclient import manager
import paramiko import paramiko
import xmltodict import xmltodict
from ncclient import manager
CONNECT_TIMEOUT = 5 # seconds CONNECT_TIMEOUT = 5 # seconds

View File

@ -1,14 +1,14 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from rest_framework import status
from rest_framework.test import APITestCase
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.urls import reverse from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from dcim.models import Device from dcim.models import Device
from extras.models import Graph, GRAPH_TYPE_SITE, ExportTemplate from extras.constants import GRAPH_TYPE_SITE
from extras.models import Graph, ExportTemplate
from users.models import Token from users.models import Token
from utilities.tests import HttpStatusMixin from utilities.tests import HttpStatusMixin

View File

@ -1,19 +1,17 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from datetime import date
from rest_framework import status from datetime import date
from rest_framework.test import APITestCase
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.test import TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from dcim.models import Site from dcim.models import Site
from extras.models import ( from extras.constants import CF_TYPE_TEXT, CF_TYPE_INTEGER, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_SELECT, CF_TYPE_URL
CustomField, CustomFieldValue, CustomFieldChoice, CF_TYPE_TEXT, CF_TYPE_INTEGER, CF_TYPE_BOOLEAN, CF_TYPE_DATE, from extras.models import CustomField, CustomFieldValue, CustomFieldChoice
CF_TYPE_SELECT, CF_TYPE_URL,
)
from users.models import Token from users.models import Token
from utilities.tests import HttpStatusMixin from utilities.tests import HttpStatusMixin

View File

@ -4,7 +4,6 @@ from django.conf.urls import url
from extras import views from extras import views
app_name = 'extras' app_name = 'extras'
urlpatterns = [ urlpatterns = [

View File

@ -1,7 +1,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.http import Http404 from django.http import Http404
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe

View File

@ -1,4 +1,5 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import OrderedDict from collections import OrderedDict
from rest_framework import serializers from rest_framework import serializers
@ -6,10 +7,10 @@ from rest_framework.validators import UniqueTogetherValidator
from dcim.api.serializers import NestedDeviceSerializer, InterfaceSerializer, NestedSiteSerializer from dcim.api.serializers import NestedDeviceSerializer, InterfaceSerializer, NestedSiteSerializer
from extras.api.customfields import CustomFieldModelSerializer from extras.api.customfields import CustomFieldModelSerializer
from ipam.models import ( from ipam.constants import (
Aggregate, IPAddress, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, IP_PROTOCOL_CHOICES, Prefix, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, IP_PROTOCOL_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES,
PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN, VLAN_STATUS_CHOICES, VLANGroup, VRF,
) )
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
from tenancy.api.serializers import NestedTenantSerializer from tenancy.api.serializers import NestedTenantSerializer
from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer
from virtualization.api.serializers import NestedVirtualMachineSerializer from virtualization.api.serializers import NestedVirtualMachineSerializer

View File

@ -1,17 +1,16 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.conf import settings
from django.shortcuts import get_object_or_404
from rest_framework import status from rest_framework import status
from rest_framework.decorators import detail_route from rest_framework.decorators import detail_route
from rest_framework.exceptions import PermissionDenied from rest_framework.exceptions import PermissionDenied
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from django.conf import settings
from django.shortcuts import get_object_or_404
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
from ipam import filters
from extras.api.views import CustomFieldModelViewSet from extras.api.views import CustomFieldModelViewSet
from ipam import filters
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
from utilities.api import FieldChoicesViewSet, WritableSerializerMixin from utilities.api import FieldChoicesViewSet, WritableSerializerMixin
from . import serializers from . import serializers

View File

@ -61,7 +61,7 @@ VLAN_STATUS_CHOICES = (
(VLAN_STATUS_DEPRECATED, 'Deprecated') (VLAN_STATUS_DEPRECATED, 'Deprecated')
) )
# Bootstrap CSS classes for various statuses # Bootstrap CSS classes
STATUS_CHOICE_CLASSES = { STATUS_CHOICE_CLASSES = {
0: 'default', 0: 'default',
1: 'primary', 1: 'primary',
@ -70,6 +70,16 @@ STATUS_CHOICE_CLASSES = {
4: 'warning', 4: 'warning',
5: 'success', 5: 'success',
} }
ROLE_CHOICE_CLASSES = {
10: 'default',
20: 'primary',
30: 'warning',
40: 'success',
41: 'success',
42: 'success',
43: 'success',
44: 'success',
}
# IP protocols (for services) # IP protocols (for services)
IP_PROTOCOL_TCP = 6 IP_PROTOCOL_TCP = 6

View File

@ -1,9 +1,8 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from netaddr import IPNetwork
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from netaddr import IPNetwork
from .formfields import IPFormField from .formfields import IPFormField
from .lookups import ( from .lookups import (

View File

@ -1,20 +1,17 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import django_filters import django_filters
from django.db.models import Q
from netaddr import IPNetwork from netaddr import IPNetwork
from netaddr.core import AddrFormatError from netaddr.core import AddrFormatError
from django.db.models import Q
from dcim.models import Site, Device, Interface from dcim.models import Site, Device, Interface
from extras.filters import CustomFieldFilterSet from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.filters import NumericInFilter from utilities.filters import NumericInFilter
from virtualization.models import VirtualMachine from virtualization.models import VirtualMachine
from .models import ( from .constants import IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES
Aggregate, IPAddress, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
Service, VLAN, VLAN_STATUS_CHOICES, VLANGroup, VRF,
)
class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet): class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
@ -102,9 +99,18 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
method='search', method='search',
label='Search', label='Search',
) )
# TODO: Deprecate in v2.3.0
parent = django_filters.CharFilter( parent = django_filters.CharFilter(
method='search_by_parent', method='search_within_include',
label='Parent prefix', label='Parent prefix (deprecated)',
)
within = django_filters.CharFilter(
method='search_within',
label='Within prefix',
)
within_include = django_filters.CharFilter(
method='search_within_include',
label='Within and including prefix',
) )
mask_length = django_filters.NumberFilter( mask_length = django_filters.NumberFilter(
method='filter_mask_length', method='filter_mask_length',
@ -159,7 +165,8 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Role (slug)', label='Role (slug)',
) )
status = django_filters.MultipleChoiceFilter( status = django_filters.MultipleChoiceFilter(
choices=PREFIX_STATUS_CHOICES choices=PREFIX_STATUS_CHOICES,
null_value=None
) )
class Meta: class Meta:
@ -177,7 +184,17 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
pass pass
return queryset.filter(qs_filter) return queryset.filter(qs_filter)
def search_by_parent(self, queryset, name, value): def search_within(self, queryset, name, value):
value = value.strip()
if not value:
return queryset
try:
query = str(IPNetwork(value).cidr)
return queryset.filter(prefix__net_contained=query)
except (AddrFormatError, ValueError):
return queryset.none()
def search_within_include(self, queryset, name, value):
value = value.strip() value = value.strip()
if not value: if not value:
return queryset return queryset
@ -254,7 +271,8 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Interface (ID)', label='Interface (ID)',
) )
status = django_filters.MultipleChoiceFilter( status = django_filters.MultipleChoiceFilter(
choices=IPADDRESS_STATUS_CHOICES choices=IPADDRESS_STATUS_CHOICES,
null_value=None
) )
role = django_filters.MultipleChoiceFilter( role = django_filters.MultipleChoiceFilter(
choices=IPADDRESS_ROLE_CHOICES choices=IPADDRESS_ROLE_CHOICES
@ -353,7 +371,8 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Role (slug)', label='Role (slug)',
) )
status = django_filters.MultipleChoiceFilter( status = django_filters.MultipleChoiceFilter(
choices=VLAN_STATUS_CHOICES choices=VLAN_STATUS_CHOICES,
null_value=None
) )
class Meta: class Meta:

View File

@ -1,9 +1,8 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from netaddr import IPNetwork, AddrFormatError
from django import forms from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from netaddr import IPNetwork, AddrFormatError
# #

View File

@ -14,11 +14,8 @@ from utilities.forms import (
add_blank_choice, add_blank_choice,
) )
from virtualization.models import VirtualMachine from virtualization.models import VirtualMachine
from .models import ( from .constants import IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES
Aggregate, IPAddress, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
Service, VLAN, VLANGroup, VLAN_STATUS_CHOICES, VRF,
)
IP_FAMILY_CHOICES = [ IP_FAMILY_CHOICES = [
('', 'All'), ('', 'All'),
@ -362,7 +359,7 @@ def prefix_status_choices():
class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm): class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Prefix model = Prefix
q = forms.CharField(required=False, label='Search') q = forms.CharField(required=False, label='Search')
parent = forms.CharField(required=False, label='Parent prefix', widget=forms.TextInput(attrs={ within_include = forms.CharField(required=False, label='Search within', widget=forms.TextInput(attrs={
'placeholder': 'Prefix', 'placeholder': 'Prefix',
})) }))
family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address family') family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address family')
@ -691,6 +688,11 @@ class IPAddressBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
nullable_fields = ['vrf', 'role', 'tenant', 'description'] nullable_fields = ['vrf', 'role', 'tenant', 'description']
class IPAddressAssignForm(BootstrapMixin, forms.Form):
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF')
address = forms.CharField(label='IP Address')
def ipaddress_status_choices(): def ipaddress_status_choices():
status_counts = {} status_counts = {}
for status in IPAddress.objects.values('status').annotate(count=Count('status')).order_by('status'): for status in IPAddress.objects.values('status').annotate(count=Count('status')).order_by('status'):

View File

@ -1,9 +1,8 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import netaddr
import netaddr
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.fields import GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
@ -286,7 +285,7 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
""" """
Return all IPAddresses within this Prefix. Return all IPAddresses within this Prefix.
""" """
return IPAddress.objects.filter(address__net_contained_or_equal=str(self.prefix), vrf=self.vrf) return IPAddress.objects.filter(address__net_host_contained=str(self.prefix), vrf=self.vrf)
def get_available_ips(self): def get_available_ips(self):
""" """
@ -315,9 +314,7 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
child_prefixes = netaddr.IPSet([p.prefix for p in queryset]) child_prefixes = netaddr.IPSet([p.prefix for p in queryset])
return int(float(child_prefixes.size) / self.prefix.size * 100) return int(float(child_prefixes.size) / self.prefix.size * 100)
else: else:
child_count = IPAddress.objects.filter( child_count = self.get_child_ips().count()
address__net_contained_or_equal=str(self.prefix), vrf=self.vrf
).count()
prefix_size = self.prefix.size prefix_size = self.prefix.size
if self.family == 4 and self.prefix.prefixlen < 31 and not self.is_pool: if self.family == 4 and self.prefix.prefixlen < 31 and not self.is_pool:
prefix_size -= 2 prefix_size -= 2
@ -461,6 +458,9 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
def get_status_class(self): def get_status_class(self):
return STATUS_CHOICE_CLASSES[self.status] return STATUS_CHOICE_CLASSES[self.status]
def get_role_class(self):
return ROLE_CHOICE_CLASSES[self.role]
@python_2_unicode_compatible @python_2_unicode_compatible
class VLANGroup(models.Model): class VLANGroup(models.Model):

View File

@ -6,7 +6,6 @@ from django_tables2.utils import Accessor
from utilities.tables import BaseTable, ToggleColumn from utilities.tables import BaseTable, ToggleColumn
from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF
RIR_UTILIZATION = """ RIR_UTILIZATION = """
<div class="progress"> <div class="progress">
{% if record.stats.total %} {% if record.stats.total %}
@ -77,6 +76,10 @@ IPADDRESS_LINK = """
{% endif %} {% endif %}
""" """
IPADDRESS_ASSIGN_LINK = """
<a href="{% url 'ipam:ipaddress_edit' pk=record.pk %}?interface={{ request.GET.interface }}&return_url={{ request.GET.return_url }}">{{ record }}</a>
"""
IPADDRESS_PARENT = """ IPADDRESS_PARENT = """
{% if record.interface %} {% if record.interface %}
<a href="{{ record.interface.parent.get_absolute_url }}">{{ record.interface.parent }}</a> <a href="{{ record.interface.parent.get_absolute_url }}">{{ record.interface.parent }}</a>
@ -269,8 +272,8 @@ class PrefixDetailTable(PrefixTable):
class IPAddressTable(BaseTable): class IPAddressTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
address = tables.TemplateColumn(IPADDRESS_LINK, verbose_name='IP Address') address = tables.TemplateColumn(IPADDRESS_LINK, verbose_name='IP Address')
status = tables.TemplateColumn(STATUS_LABEL)
vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF') vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
status = tables.TemplateColumn(STATUS_LABEL)
tenant = tables.TemplateColumn(TENANT_LINK) tenant = tables.TemplateColumn(TENANT_LINK)
parent = tables.TemplateColumn(IPADDRESS_PARENT, orderable=False) parent = tables.TemplateColumn(IPADDRESS_PARENT, orderable=False)
interface = tables.Column(orderable=False) interface = tables.Column(orderable=False)
@ -294,6 +297,18 @@ class IPAddressDetailTable(IPAddressTable):
) )
class IPAddressAssignTable(BaseTable):
address = tables.TemplateColumn(IPADDRESS_ASSIGN_LINK, verbose_name='IP Address')
status = tables.TemplateColumn(STATUS_LABEL)
parent = tables.TemplateColumn(IPADDRESS_PARENT, orderable=False)
interface = tables.Column(orderable=False)
class Meta(BaseTable.Meta):
model = IPAddress
fields = ('address', 'vrf', 'status', 'role', 'tenant', 'parent', 'interface')
orderable = False
# #
# VLAN groups # VLAN groups
# #

View File

@ -1,16 +1,14 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.contrib.auth.models import User
from django.urls import reverse
from netaddr import IPNetwork from netaddr import IPNetwork
from rest_framework import status from rest_framework import status
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
from django.contrib.auth.models import User
from django.urls import reverse
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
from ipam.models import ( from ipam.constants import IP_PROTOCOL_TCP, IP_PROTOCOL_UDP
Aggregate, IPAddress, IP_PROTOCOL_TCP, IP_PROTOCOL_UDP, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF, from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
)
from users.models import Token from users.models import Token
from utilities.tests import HttpStatusMixin from utilities.tests import HttpStatusMixin

View File

@ -1,7 +1,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import netaddr import netaddr
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.test import TestCase, override_settings from django.test import TestCase, override_settings

View File

@ -60,6 +60,7 @@ urlpatterns = [
url(r'^ip-addresses/import/$', views.IPAddressBulkImportView.as_view(), name='ipaddress_import'), url(r'^ip-addresses/import/$', views.IPAddressBulkImportView.as_view(), name='ipaddress_import'),
url(r'^ip-addresses/edit/$', views.IPAddressBulkEditView.as_view(), name='ipaddress_bulk_edit'), url(r'^ip-addresses/edit/$', views.IPAddressBulkEditView.as_view(), name='ipaddress_bulk_edit'),
url(r'^ip-addresses/delete/$', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'), url(r'^ip-addresses/delete/$', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'),
url(r'^ip-addresses/assign/$', views.IPAddressAssignView.as_view(), name='ipaddress_assign'),
url(r'^ip-addresses/(?P<pk>\d+)/$', views.IPAddressView.as_view(), name='ipaddress'), url(r'^ip-addresses/(?P<pk>\d+)/$', views.IPAddressView.as_view(), name='ipaddress'),
url(r'^ip-addresses/(?P<pk>\d+)/edit/$', views.IPAddressEditView.as_view(), name='ipaddress_edit'), url(r'^ip-addresses/(?P<pk>\d+)/edit/$', views.IPAddressEditView.as_view(), name='ipaddress_edit'),
url(r'^ip-addresses/(?P<pk>\d+)/delete/$', views.IPAddressDeleteView.as_view(), name='ipaddress_delete'), url(r'^ip-addresses/(?P<pk>\d+)/delete/$', views.IPAddressDeleteView.as_view(), name='ipaddress_delete'),

View File

@ -1,14 +1,13 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django_tables2 import RequestConfig
import netaddr import netaddr
from django.conf import settings from django.conf import settings
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
from django.db.models import Count, Q from django.db.models import Count, Q
from django.shortcuts import get_object_or_404, render from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse from django.urls import reverse
from django.views.generic import View from django.views.generic import View
from django_tables2 import RequestConfig
from dcim.models import Device, Interface from dcim.models import Device, Interface
from utilities.paginator import EnhancedPaginator from utilities.paginator import EnhancedPaginator
@ -17,11 +16,8 @@ from utilities.views import (
) )
from virtualization.models import VirtualMachine from virtualization.models import VirtualMachine
from . import filters, forms, tables from . import filters, forms, tables
from .constants import IPADDRESS_ROLE_ANYCAST from .constants import IPADDRESS_ROLE_ANYCAST, PREFIX_STATUS_ACTIVE, PREFIX_STATUS_DEPRECATED, PREFIX_STATUS_RESERVED
from .models import ( from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
Aggregate, IPAddress, PREFIX_STATUS_ACTIVE, PREFIX_STATUS_DEPRECATED, PREFIX_STATUS_RESERVED, Prefix, RIR, Role,
Service, VLAN, VLANGroup, VRF,
)
def add_available_prefixes(parent, prefix_list): def add_available_prefixes(parent, prefix_list):
@ -459,9 +455,7 @@ class PrefixView(View):
aggregate = None aggregate = None
# Count child IP addresses # Count child IP addresses
ipaddress_count = IPAddress.objects.filter( ipaddress_count = prefix.get_child_ips().count()
vrf=prefix.vrf, address__net_host_contained=str(prefix.prefix)
).count()
# Parent prefixes table # Parent prefixes table
parent_prefixes = Prefix.objects.filter( parent_prefixes = Prefix.objects.filter(
@ -517,6 +511,7 @@ class PrefixView(View):
'parent_prefix_table': parent_prefix_table, 'parent_prefix_table': parent_prefix_table,
'child_prefix_table': child_prefix_table, 'child_prefix_table': child_prefix_table,
'duplicate_prefix_table': duplicate_prefix_table, 'duplicate_prefix_table': duplicate_prefix_table,
'bulk_querystring': 'vrf_id={}&within={}'.format(prefix.vrf or '0', prefix.prefix),
'permissions': permissions, 'permissions': permissions,
'return_url': prefix.get_absolute_url(), 'return_url': prefix.get_absolute_url(),
}) })
@ -529,9 +524,7 @@ class PrefixIPAddressesView(View):
prefix = get_object_or_404(Prefix.objects.all(), pk=pk) prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
# Find all IPAddresses belonging to this Prefix # Find all IPAddresses belonging to this Prefix
ipaddresses = IPAddress.objects.filter( ipaddresses = prefix.get_child_ips().select_related(
vrf=prefix.vrf, address__net_host_contained=str(prefix.prefix)
).select_related(
'vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for' 'vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for'
) )
ipaddresses = add_available_ipaddresses(prefix.prefix, ipaddresses, prefix.is_pool) ipaddresses = add_available_ipaddresses(prefix.prefix, ipaddresses, prefix.is_pool)
@ -557,7 +550,7 @@ class PrefixIPAddressesView(View):
'prefix': prefix, 'prefix': prefix,
'ip_table': ip_table, 'ip_table': ip_table,
'permissions': permissions, 'permissions': permissions,
'bulk_querystring': 'vrf_id={}&parent={}'.format(prefix.vrf or '0', prefix.prefix), 'bulk_querystring': 'vrf_id={}&parent={}'.format(prefix.vrf.pk if prefix.vrf else '0', prefix.prefix),
}) })
@ -693,6 +686,51 @@ class IPAddressEditView(IPAddressCreateView):
permission_required = 'ipam.change_ipaddress' permission_required = 'ipam.change_ipaddress'
class IPAddressAssignView(PermissionRequiredMixin, View):
"""
Search for IPAddresses to be assigned to an Interface.
"""
permission_required = 'ipam.change_ipaddress'
def dispatch(self, request, *args, **kwargs):
# Redirect user if an interface has not been provided
if 'interface' not in request.GET:
return redirect('ipam:ipaddress_add')
return super(IPAddressAssignView, self).dispatch(request, *args, **kwargs)
def get(self, request):
form = forms.IPAddressAssignForm()
return render(request, 'ipam/ipaddress_assign.html', {
'form': form,
'return_url': request.GET.get('return_url', ''),
})
def post(self, request):
form = forms.IPAddressAssignForm(request.POST)
table = None
if form.is_valid():
queryset = IPAddress.objects.select_related(
'vrf', 'tenant', 'interface__device', 'interface__virtual_machine'
).filter(
vrf=form.cleaned_data['vrf'],
address__net_host=form.cleaned_data['address'],
)
table = tables.IPAddressAssignTable(queryset)
return render(request, 'ipam/ipaddress_assign.html', {
'form': form,
'table': table,
'return_url': request.GET.get('return_url', ''),
})
class IPAddressDeleteView(PermissionRequiredMixin, ObjectDeleteView): class IPAddressDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'ipam.delete_ipaddress' permission_required = 'ipam.delete_ipaddress'
model = IPAddress model = IPAddress

View File

@ -4,7 +4,6 @@ from django import forms
from utilities.forms import BootstrapMixin from utilities.forms import BootstrapMixin
OBJ_TYPE_CHOICES = ( OBJ_TYPE_CHOICES = (
('', 'All Objects'), ('', 'All Objects'),
('Circuits', ( ('Circuits', (

View File

@ -13,7 +13,7 @@ except ImportError:
) )
VERSION = '2.2.4' VERSION = '2.2.5'
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@ -148,6 +148,7 @@ MIDDLEWARE = (
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'utilities.middleware.ExceptionHandlingMiddleware',
'utilities.middleware.LoginRequiredMiddleware', 'utilities.middleware.LoginRequiredMiddleware',
'utilities.middleware.APIVersionMiddleware', 'utilities.middleware.APIVersionMiddleware',
) )

View File

@ -1,17 +1,14 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from rest_framework_swagger.views import get_swagger_view
from django.conf import settings from django.conf import settings
from django.conf.urls import include, url from django.conf.urls import include, url
from django.contrib import admin from django.contrib import admin
from django.views.static import serve from django.views.static import serve
from rest_framework_swagger.views import get_swagger_view
from netbox.views import APIRootView, handle_500, HomeView, SearchView, trigger_500 from netbox.views import APIRootView, HomeView, SearchView
from users.views import LoginView, LogoutView from users.views import LoginView, LogoutView
handler500 = handle_500
swagger_view = get_swagger_view(title='NetBox API') swagger_view = get_swagger_view(title='NetBox API')
_patterns = [ _patterns = [
@ -48,9 +45,6 @@ _patterns = [
# Serving static media in Django to pipe it through LoginRequiredMiddleware # Serving static media in Django to pipe it through LoginRequiredMiddleware
url(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}), url(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}),
# Error testing
url(r'^500/$', trigger_500),
# Admin # Admin
url(r'^admin/', admin.site.urls), url(r'^admin/', admin.site.urls),

View File

@ -1,13 +1,12 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import OrderedDict
import sys
from rest_framework.views import APIView from collections import OrderedDict
from rest_framework.response import Response
from rest_framework.reverse import reverse
from django.shortcuts import render from django.shortcuts import render
from django.views.generic import View from django.views.generic import View
from rest_framework.response import Response
from rest_framework.reverse import reverse
from rest_framework.views import APIView
from circuits.filters import CircuitFilter, ProviderFilter from circuits.filters import CircuitFilter, ProviderFilter
from circuits.models import Circuit, Provider from circuits.models import Circuit, Provider
@ -30,7 +29,6 @@ from virtualization.models import Cluster, VirtualMachine
from virtualization.tables import ClusterTable, VirtualMachineDetailTable from virtualization.tables import ClusterTable, VirtualMachineDetailTable
from .forms import SearchForm from .forms import SearchForm
SEARCH_MAX_RESULTS = 15 SEARCH_MAX_RESULTS = 15
SEARCH_TYPES = OrderedDict(( SEARCH_TYPES = OrderedDict((
# Circuits # Circuits
@ -247,23 +245,3 @@ class APIRootView(APIView):
('tenancy', reverse('tenancy-api:api-root', request=request, format=format)), ('tenancy', reverse('tenancy-api:api-root', request=request, format=format)),
('virtualization', reverse('virtualization-api:api-root', request=request, format=format)), ('virtualization', reverse('virtualization-api:api-root', request=request, format=format)),
))) )))
def handle_500(request):
"""
Custom server error handler
"""
type_, error, traceback = sys.exc_info()
return render(request, '500.html', {
'exception': str(type_),
'error': error,
}, status=500)
def trigger_500(request):
"""
Hot-wired method of triggering a server error to test reporting
"""
raise Exception(
"Congratulations, you've triggered an exception! Go tell all your friends what an exceptional person you are."
)

View File

@ -1,12 +1,3 @@
"""
WSGI config for do_ipam project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/
"""
import os import os
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application

View File

@ -395,6 +395,9 @@ table.reports td.stats label {
} }
/* Misc */ /* Misc */
.text-nowrap {
white-space: nowrap;
}
.banner-bottom { .banner-bottom {
margin-bottom: 50px; margin-bottom: 50px;
} }

View File

@ -1,21 +1,20 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import base64 import base64
from Crypto.PublicKey import RSA from Crypto.PublicKey import RSA
from django.http import HttpResponseBadRequest
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet, ViewSet from rest_framework.viewsets import ModelViewSet, ViewSet
from django.http import HttpResponseBadRequest
from secrets import filters from secrets import filters
from secrets.exceptions import InvalidKey from secrets.exceptions import InvalidKey
from secrets.models import Secret, SecretRole, SessionKey, UserKey from secrets.models import Secret, SecretRole, SessionKey, UserKey
from utilities.api import FieldChoicesViewSet, WritableSerializerMixin from utilities.api import FieldChoicesViewSet, WritableSerializerMixin
from . import serializers from . import serializers
ERR_USERKEY_MISSING = "No UserKey found for the current user." ERR_USERKEY_MISSING = "No UserKey found for the current user."
ERR_USERKEY_INACTIVE = "UserKey has not been activated for decryption." ERR_USERKEY_INACTIVE = "UserKey has not been activated for decryption."
ERR_PRIVKEY_MISSING = "Private key was not provided." ERR_PRIVKEY_MISSING = "Private key was not provided."

View File

@ -1,12 +1,11 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import django_filters import django_filters
from django.db.models import Q from django.db.models import Q
from .models import Secret, SecretRole
from dcim.models import Device from dcim.models import Device
from utilities.filters import NumericInFilter from utilities.filters import NumericInFilter
from .models import Secret, SecretRole
class SecretRoleFilter(django_filters.FilterSet): class SecretRoleFilter(django_filters.FilterSet):

View File

@ -2,7 +2,6 @@ from __future__ import unicode_literals
from Crypto.Cipher import PKCS1_OAEP from Crypto.Cipher import PKCS1_OAEP
from Crypto.PublicKey import RSA from Crypto.PublicKey import RSA
from django import forms from django import forms
from django.db.models import Count from django.db.models import Count

View File

@ -1,10 +1,10 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import os import os
from Crypto.Cipher import AES, PKCS1_OAEP from Crypto.Cipher import AES, PKCS1_OAEP
from Crypto.PublicKey import RSA from Crypto.PublicKey import RSA
from Crypto.Util import strxor from Crypto.Util import strxor
from django.conf import settings from django.conf import settings
from django.contrib.auth.hashers import make_password, check_password from django.contrib.auth.hashers import make_password, check_password
from django.contrib.auth.models import Group, User from django.contrib.auth.models import Group, User
@ -286,6 +286,9 @@ class Secret(CreatedUpdatedModel):
super(Secret, self).__init__(*args, **kwargs) super(Secret, self).__init__(*args, **kwargs)
def __str__(self): def __str__(self):
if self.role and self.device and self.name:
return '{} for {} ({})'.format(self.role, self.device, self.name)
# Return role and device if no name is set
if self.role and self.device: if self.role and self.device:
return '{} for {}'.format(self.role, self.device) return '{} for {}'.format(self.role, self.device)
return 'Secret' return 'Secret'

View File

@ -3,10 +3,8 @@ from __future__ import unicode_literals
import django_tables2 as tables import django_tables2 as tables
from utilities.tables import BaseTable, ToggleColumn from utilities.tables import BaseTable, ToggleColumn
from .models import SecretRole, Secret from .models import SecretRole, Secret
SECRETROLE_ACTIONS = """ SECRETROLE_ACTIONS = """
{% if perms.secrets.change_secretrole %} {% if perms.secrets.change_secretrole %}
<a href="{% url 'secrets:secretrole_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a> <a href="{% url 'secrets:secretrole_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>

View File

@ -1,18 +1,17 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import base64
from rest_framework import status import base64
from rest_framework.test import APITestCase
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.urls import reverse from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
from secrets.models import Secret, SecretRole, SessionKey, UserKey from secrets.models import Secret, SecretRole, SessionKey, UserKey
from users.models import Token from users.models import Token
from utilities.tests import HttpStatusMixin from utilities.tests import HttpStatusMixin
# Dummy RSA key pair for testing use only # Dummy RSA key pair for testing use only
PRIVATE_KEY = """-----BEGIN RSA PRIVATE KEY----- PRIVATE_KEY = """-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA97wPWxpq5cClRu8Ssq609ZLfyx6E8ln/v/PdFZ7fxxmA4k+z MIIEowIBAAKCAQEA97wPWxpq5cClRu8Ssq609ZLfyx6E8ln/v/PdFZ7fxxmA4k+z

View File

@ -1,14 +1,13 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from Crypto.PublicKey import RSA from Crypto.PublicKey import RSA
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.test import TestCase from django.test import TestCase
from secrets.models import UserKey, Secret, encrypt_master_key, decrypt_master_key, generate_random_key
from secrets.hashers import SecretValidationHasher from secrets.hashers import SecretValidationHasher
from secrets.models import UserKey, Secret, encrypt_master_key, decrypt_master_key, generate_random_key
class UserKeyTestCase(TestCase): class UserKeyTestCase(TestCase):

View File

@ -4,7 +4,6 @@ from django.conf.urls import url
from . import views from . import views
app_name = 'secrets' app_name = 'secrets'
urlpatterns = [ urlpatterns = [

View File

@ -1,4 +1,5 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import base64 import base64
from django.contrib import messages from django.contrib import messages

View File

@ -12,7 +12,7 @@
<body> <body>
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <div class="row">
<div class="col-md-4 col-md-offset-4"> <div class="col-md-6 col-md-offset-3">
<div class="panel panel-danger" style="margin-top: 200px"> <div class="panel panel-danger" style="margin-top: 200px">
<div class="panel-heading"> <div class="panel-heading">
<strong> <strong>
@ -21,13 +21,20 @@
</strong> </strong>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<p>There was a problem with your request. This error has been logged and administrative staff have {% block message %}
been notified. Please return to the home page and try again.</p> <p>
<p>If you are responsible for this installation, please consider There was a problem with your request. Please contact an administrator.
<a href="https://github.com/digitalocean/netbox/issues">filing a bug report</a>. Additional </p>
information is provided below:</p> {% endblock %}
<hr />
<p>
The complete exception is provided below:
</p>
<pre><strong>{{ exception }}</strong><br /> <pre><strong>{{ exception }}</strong><br />
{{ error }}</pre> {{ error }}</pre>
<p>
If further assistance is required, please post to the <a href="https://groups.google.com/forum/#!forum/netbox-discuss">NetBox mailing list</a>.
</p>
<div class="text-right"> <div class="text-right">
<a href="{% url 'home' %}" class="btn btn-primary">Home Page</a> <a href="{% url 'home' %}" class="btn btn-primary">Home Page</a>
</div> </div>

View File

@ -65,7 +65,7 @@
<td> <td>
{% if circuit.tenant %} {% if circuit.tenant %}
{% if circuit.tenant.group %} {% if circuit.tenant.group %}
<a href="{{ circuit.tenant.group.get_absolute_url }}">{{ circuit.tenant.group.name }}</a> <a href="{{ circuit.tenant.group.get_absolute_url }}">{{ circuit.tenant.group }}</a>
<i class="fa fa-angle-right"></i> <i class="fa fa-angle-right"></i>
{% endif %} {% endif %}
<a href="{{ circuit.tenant.get_absolute_url }}">{{ circuit.tenant }}</a> <a href="{{ circuit.tenant.get_absolute_url }}">{{ circuit.tenant }}</a>

View File

@ -7,7 +7,7 @@
{% block content %} {% block content %}
{% include 'dcim/inc/device_header.html' with active_tab='info' %} {% include 'dcim/inc/device_header.html' with active_tab='info' %}
<div class="row"> <div class="row">
<div class="col-md-5"> <div class="col-md-6">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<strong>Device</strong> <strong>Device</strong>
@ -28,10 +28,10 @@
<td> <td>
{% if device.rack %} {% if device.rack %}
{% if device.rack.group %} {% if device.rack.group %}
<a href="{{ device.rack.group.get_absolute_url }}">{{ device.rack.group.name }}</a> <a href="{{ device.rack.group.get_absolute_url }}">{{ device.rack.group }}</a>
<i class="fa fa-angle-right"></i> <i class="fa fa-angle-right"></i>
{% endif %} {% endif %}
<a href="{% url 'dcim:rack' pk=device.rack.pk %}">{{ device.rack.name }}</a>{% if device.rack.facility_id %} ({{ device.rack.facility_id }}){% endif %} <a href="{% url 'dcim:rack' pk=device.rack.pk %}">{{ device.rack }}</a>
{% else %} {% else %}
<span class="text-muted">None</span> <span class="text-muted">None</span>
{% endif %} {% endif %}
@ -42,7 +42,7 @@
<td> <td>
{% if device.parent_bay %} {% if device.parent_bay %}
{% with device.parent_bay.device as parent %} {% with device.parent_bay.device as parent %}
<a href="{{ parent.get_absolute_url }}">{{ parent }}</a> <i class="fa fa-angle-right"></i> {{ device.parent_bay.name }} <a href="{{ parent.get_absolute_url }}">{{ parent }}</a> <i class="fa fa-angle-right"></i> {{ device.parent_bay }}
{% if parent.position %} {% if parent.position %}
(U{{ parent.position }} / {{ parent.get_face_display }}) (U{{ parent.position }} / {{ parent.get_face_display }})
{% endif %} {% endif %}
@ -61,7 +61,7 @@
<td> <td>
{% if device.tenant %} {% if device.tenant %}
{% if device.tenant.group %} {% if device.tenant.group %}
<a href="{{ device.tenant.group.get_absolute_url }}">{{ device.tenant.group.name }}</a> <a href="{{ device.tenant.group.get_absolute_url }}">{{ device.tenant.group }}</a>
<i class="fa fa-angle-right"></i> <i class="fa fa-angle-right"></i>
{% endif %} {% endif %}
<a href="{{ device.tenant.get_absolute_url }}">{{ device.tenant }}</a> <a href="{{ device.tenant.get_absolute_url }}">{{ device.tenant }}</a>
@ -172,6 +172,69 @@
{% with device.get_custom_fields as custom_fields %} {% with device.get_custom_fields as custom_fields %}
{% include 'inc/custom_fields_panel.html' %} {% include 'inc/custom_fields_panel.html' %}
{% endwith %} {% endwith %}
<div class="panel panel-default">
<div class="panel-heading">
<strong>Comments</strong>
</div>
<div class="panel-body">
{% if device.comments %}
{{ device.comments|gfm }}
{% else %}
<span class="text-muted">None</span>
{% endif %}
</div>
</div>
</div>
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Console / Power</strong>
</div>
<table class="table table-hover panel-body component-list">
{% for cp in console_ports %}
{% include 'dcim/inc/consoleport.html' %}
{% empty %}
{% if device.device_type.console_port_templates.exists %}
<tr>
<td colspan="6" class="alert-warning">
<i class="fa fa-fw fa-warning"></i> No console ports defined
{% if perms.dcim.add_consoleport %}
<a href="{% url 'dcim:consoleport_add' pk=device.pk %}" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></a>
{% endif %}
</td>
</tr>
{% endif %}
{% endfor %}
{% for pp in power_ports %}
{% include 'dcim/inc/powerport.html' %}
{% empty %}
{% if device.device_type.power_port_templates.exists %}
<tr>
<td colspan="6" class="alert-warning">
<i class="fa fa-fw fa-warning"></i> No power ports defined
{% if perms.dcim.add_powerport %}
<a href="{% url 'dcim:powerport_add' pk=device.pk %}" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></a>
{% endif %}
</td>
</tr>
{% endif %}
{% endfor %}
</table>
{% if perms.dcim.add_interface or perms.dcim.add_consoleport or perms.dcim.add_powerport %}
<div class="panel-footer text-right">
{% if perms.dcim.add_consoleport %}
<a href="{% url 'dcim:consoleport_add' pk=device.pk %}" class="btn btn-xs btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console port
</a>
{% endif %}
{% if perms.dcim.add_powerport %}
<a href="{% url 'dcim:powerport_add' pk=device.pk %}" class="btn btn-xs btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power port
</a>
{% endif %}
</div>
{% endif %}
</div>
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
@ -224,67 +287,6 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
<div class="panel panel-default">
<div class="panel-heading">
<strong>Console / Power</strong>
</div>
<table class="table table-hover panel-body component-list">
{% for cp in console_ports %}
{% include 'dcim/inc/consoleport.html' %}
{% empty %}
{% if device.device_type.console_port_templates.exists %}
<tr>
<td colspan="6" class="alert-warning">
<i class="fa fa-fw fa-warning"></i> No console ports defined
{% if perms.dcim.add_consoleport %}
<a href="{% url 'dcim:consoleport_add' pk=device.pk %}" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></a>
{% endif %}
</td>
</tr>
{% endif %}
{% endfor %}
{% for pp in power_ports %}
{% include 'dcim/inc/powerport.html' %}
{% empty %}
{% if device.device_type.power_port_templates.exists %}
<tr>
<td colspan="6" class="alert-warning">
<i class="fa fa-fw fa-warning"></i> No power ports defined
{% if perms.dcim.add_powerport %}
<a href="{% url 'dcim:powerport_add' pk=device.pk %}" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></a>
{% endif %}
</td>
</tr>
{% endif %}
{% endfor %}
</table>
{% if perms.dcim.add_interface or perms.dcim.add_consoleport or perms.dcim.add_powerport %}
<div class="panel-footer text-right">
{% if perms.dcim.add_consoleport %}
<a href="{% url 'dcim:consoleport_add' pk=device.pk %}" class="btn btn-xs btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console port
</a>
{% endif %}
{% if perms.dcim.add_powerport %}
<a href="{% url 'dcim:powerport_add' pk=device.pk %}" class="btn btn-xs btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power port
</a>
{% endif %}
</div>
{% endif %}
</div>
<div class="panel panel-default">
<div class="panel-heading">
<strong>Comments</strong>
</div>
<div class="panel-body">
{% if device.comments %}
{{ device.comments|gfm }}
{% else %}
<span class="text-muted">None</span>
{% endif %}
</div>
</div>
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<strong>Images</strong> <strong>Images</strong>
@ -326,7 +328,9 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
<div class="col-md-7"> </div>
<div class="row">
<div class="col-md-12">
{% if device_bays or device.device_type.is_parent_device %} {% if device_bays or device.device_type.is_parent_device %}
{% if perms.dcim.delete_devicebay %} {% if perms.dcim.delete_devicebay %}
<form method="post" action="{% url 'dcim:devicebay_bulk_delete' pk=device.pk %}"> <form method="post" action="{% url 'dcim:devicebay_bulk_delete' pk=device.pk %}">
@ -350,7 +354,7 @@
</div> </div>
<table class="table table-hover panel-body component-list"> <table class="table table-hover panel-body component-list">
{% for devicebay in device_bays %} {% for devicebay in device_bays %}
{% include 'dcim/inc/devicebay.html' with selectable=True %} {% include 'dcim/inc/devicebay.html' %}
{% empty %} {% empty %}
<tr> <tr>
<td colspan="4">No device bays defined</td> <td colspan="4">No device bays defined</td>
@ -405,11 +409,23 @@
</div> </div>
</div> </div>
<table id="interfaces_table" class="table table-hover panel-body component-list"> <table id="interfaces_table" class="table table-hover panel-body component-list">
<tr class="table-headings">
{% if perms.dcim.change_interface or perms.dcim.delete_interface %}
<th></th>
{% endif %}
<th>Name</th>
<th>LAG</th>
<th>Description</th>
<th>MTU</th>
<th>MAC Address</th>
<th colspan="2">Connection</th>
<th></th>
</tr>
{% for iface in interfaces %} {% for iface in interfaces %}
{% include 'dcim/inc/interface.html' with selectable=True %} {% include 'dcim/inc/interface.html' %}
{% empty %} {% empty %}
<tr> <tr>
<td colspan="4">No interfaces defined</td> <td colspan="8">No interfaces defined</td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
@ -467,8 +483,16 @@
</div> </div>
</div> </div>
<table class="table table-hover panel-body component-list"> <table class="table table-hover panel-body component-list">
<tr class="table-headings">
{% if perms.dcim.change_consoleserverport or perms.dcim.delete_consoleserverport %}
<th></th>
{% endif %}
<th>Name</th>
<th colspan="2">Connection</th>
<th></th>
</tr>
{% for csp in cs_ports %} {% for csp in cs_ports %}
{% include 'dcim/inc/consoleserverport.html' with selectable=True %} {% include 'dcim/inc/consoleserverport.html' %}
{% empty %} {% empty %}
<tr> <tr>
<td colspan="4">No console server ports defined</td> <td colspan="4">No console server ports defined</td>
@ -524,12 +548,20 @@
</div> </div>
</div> </div>
<table class="table table-hover panel-body component-list"> <table class="table table-hover panel-body component-list">
<tr class="table-headings">
{% if perms.dcim.change_poweroutlet or perms.dcim.delete_poweroutlet %}
<th></th>
{% endif %}
<th>Name</th>
<th colspan="2">Connection</th>
<th></th>
</tr>
{% for po in power_outlets %} {% for po in power_outlets %}
{% include 'dcim/inc/poweroutlet.html' with selectable=True %} {% include 'dcim/inc/poweroutlet.html' %}
{% empty %} {% empty %}
<tr> <tr>
<td colspan="4">No power outlets defined</td> <td colspan="4">No power outlets defined</td>
</tr> </tr> text-nowrap
{% endfor %} {% endfor %}
</table> </table>
{% if perms.dcim.add_poweroutlet or perms.dcim.delete_poweroutlet %} {% if perms.dcim.add_poweroutlet or perms.dcim.delete_poweroutlet %}

View File

@ -26,7 +26,7 @@
<div class="form-group"> <div class="form-group">
<label class="col-md-3 control-label required">Bay</label> <label class="col-md-3 control-label required">Bay</label>
<div class="col-md-9"> <div class="col-md-9">
<p class="form-control-static">{{ device_bay.name }}</p> <p class="form-control-static">{{ device_bay }}</p>
</div> </div>
</div> </div>
{% render_form form %} {% render_form form %}

View File

@ -1,13 +1,13 @@
<tr class="consoleport{% if cp.cs_port and not cp.connection_status %} info{% endif %}"> <tr class="consoleport{% if cp.cs_port and not cp.connection_status %} info{% endif %}">
<td> <td>
<i class="fa fa-fw fa-keyboard-o"></i> {{ cp.name }} <i class="fa fa-fw fa-keyboard-o"></i> {{ cp }}
</td> </td>
{% if cp.cs_port %} {% if cp.cs_port %}
<td> <td>
<a href="{% url 'dcim:device' pk=cp.cs_port.device.pk %}">{{ cp.cs_port.device }}</a> <a href="{% url 'dcim:device' pk=cp.cs_port.device.pk %}">{{ cp.cs_port.device }}</a>
</td> </td>
<td> <td>
{{ cp.cs_port.name }} {{ cp.cs_port }}
</td> </td>
{% else %} {% else %}
<td colspan="2"> <td colspan="2">

View File

@ -1,5 +1,5 @@
<tr class="consoleserverport{% if csp.connected_console and not csp.connected_console.connection_status %} info{% endif %}"> <tr class="consoleserverport{% if csp.connected_console and not csp.connected_console.connection_status %} info{% endif %}">
{% if selectable and perms.dcim.change_consoleserverport or perms.dcim.delete_consoleserverport %} {% if perms.dcim.change_consoleserverport or perms.dcim.delete_consoleserverport %}
<td class="pk"> <td class="pk">
<input name="pk" type="checkbox" value="{{ csp.pk }}" /> <input name="pk" type="checkbox" value="{{ csp.pk }}" />
</td> </td>
@ -7,20 +7,19 @@
<td> <td>
<i class="fa fa-fw fa-keyboard-o"></i> {{ csp }} <i class="fa fa-fw fa-keyboard-o"></i> {{ csp }}
</td> </td>
<td></td>
{% if csp.connected_console %} {% if csp.connected_console %}
<td> <td>
<a href="{% url 'dcim:device' pk=csp.connected_console.device.pk %}">{{ csp.connected_console.device }}</a> <a href="{% url 'dcim:device' pk=csp.connected_console.device.pk %}">{{ csp.connected_console.device }}</a>
</td> </td>
<td> <td>
{{ csp.connected_console.name }} {{ csp.connected_console }}
</td> </td>
{% else %} {% else %}
<td colspan="2"> <td colspan="2">
<span class="text-muted">Not connected</span> <span class="text-muted">Not connected</span>
</td> </td>
{% endif %} {% endif %}
<td colspan="2" class="text-right"> <td class="text-right">
{% if perms.dcim.change_consoleserverport %} {% if perms.dcim.change_consoleserverport %}
{% if csp.connected_console %} {% if csp.connected_console %}
{% if csp.connected_console.connection_status %} {% if csp.connected_console.connection_status %}

View File

@ -8,7 +8,7 @@
{% endif %} {% endif %}
{% if device.parent_bay %} {% if device.parent_bay %}
<li><a href="{% url 'dcim:device' pk=device.parent_bay.device.pk %}">{{ device.parent_bay.device }}</a></li> <li><a href="{% url 'dcim:device' pk=device.parent_bay.device.pk %}">{{ device.parent_bay.device }}</a></li>
<li>{{ device.parent_bay.name }}</li> <li>{{ device.parent_bay }}</li>
{% endif %} {% endif %}
<li>{{ device }}</li> <li>{{ device }}</li>
</ol> </ol>

View File

@ -1,11 +1,11 @@
<tr class="devicebay"> <tr class="devicebay">
{% if selectable and perms.dcim.change_devicebay or perms.dcim.delete_devicebay %} {% if perms.dcim.change_devicebay or perms.dcim.delete_devicebay %}
<td class="pk"> <td class="pk">
<input name="pk" type="checkbox" value="{{ devicebay.pk }}" /> <input name="pk" type="checkbox" value="{{ devicebay.pk }}" />
</td> </td>
{% endif %} {% endif %}
<td> <td>
<i class="fa fa-fw fa-{% if devicebay.installed_device %}dot-circle-o{% else %}circle-o{% endif %}"></i> {{ devicebay.name }} <i class="fa fa-fw fa-{% if devicebay.installed_device %}dot-circle-o{% else %}circle-o{% endif %}"></i> {{ devicebay }}
</td> </td>
{% if devicebay.installed_device %} {% if devicebay.installed_device %}
<td> <td>

View File

@ -1,21 +1,37 @@
<tr class="interface{% if not iface.enabled %} danger{% elif iface.connection and iface.connection.connection_status or iface.circuit_termination %} success{% elif iface.connection and not iface.connection.connection_status %} info{% elif iface.is_virtual %} warning{% endif %}"> <tr class="interface{% if not iface.enabled %} danger{% elif iface.connection and iface.connection.connection_status or iface.circuit_termination %} success{% elif iface.connection and not iface.connection.connection_status %} info{% elif iface.is_virtual %} warning{% endif %}" id="iface_{{ iface.name }}">
{% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %}
{# Checkbox #}
{% if perms.dcim.change_interface or perms.dcim.delete_interface %}
<td class="pk"> <td class="pk">
<input name="pk" type="checkbox" value="{{ iface.pk }}" /> <input name="pk" type="checkbox" value="{{ iface.pk }}" />
</td> </td>
{% endif %} {% endif %}
{# Icon and name #}
<td>
<span title="{{ iface.get_form_factor_display }}">
<i class="fa fa-fw fa-{% if iface.mgmt_only %}wrench{% elif iface.is_lag %}align-justify{% elif iface.is_virtual %}circle{% elif iface.is_wireless %}wifi{% else %}exchange{% endif %}"></i>
{{ iface }}
</span>
</td>
{# LAG #}
<td> <td>
<i class="fa fa-fw fa-{% if iface.mgmt_only %}wrench{% elif iface.is_virtual %}square{% elif iface.is_wireless %}wifi{% else %}exchange{% endif %}"></i>
<span title="{{ iface.get_form_factor_display }}">{{ iface.name }}</span>
{% if iface.lag %} {% if iface.lag %}
<span class="label label-primary">{{ iface.lag.name }}</span> <a href="#iface_{{ iface.lag }}" class="label label-default">{{ iface.lag }}</a>
{% endif %}
{% if iface.description %}
<i class="fa fa-fw fa-comment-o" title="{{ iface.description }}"></i>
{% endif %} {% endif %}
</td> </td>
{# Description #}
<td>{{ iface.description }}</td>
{# MTU #}
<td>{{ iface.mtu|default:"" }}</td> <td>{{ iface.mtu|default:"" }}</td>
{# MAC address #}
<td>{{ iface.mac_address|default:"" }}</td> <td>{{ iface.mac_address|default:"" }}</td>
{# Connection or type #}
{% if iface.is_lag %} {% if iface.is_lag %}
<td colspan="2" class="text-muted"> <td colspan="2" class="text-muted">
LAG interface<br /> LAG interface<br />
@ -55,7 +71,9 @@
<span class="text-muted">Not connected</span> <span class="text-muted">Not connected</span>
</td> </td>
{% endif %} {% endif %}
<td colspan="2" class="text-right">
{# Buttons #}
<td class="text-right text-nowrap">
{% if show_graphs %} {% if show_graphs %}
{% if iface.circuit_termination or iface.connection %} {% if iface.circuit_termination or iface.connection %}
<button type="button" class="btn btn-primary btn-xs" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ device.name }} - {{ iface.name }}" data-url="{% url 'dcim-api:interface-graphs' pk=iface.pk %}" title="Show graphs"> <button type="button" class="btn btn-primary btn-xs" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ device.name }} - {{ iface.name }}" data-url="{% url 'dcim-api:interface-graphs' pk=iface.pk %}" title="Show graphs">
@ -96,7 +114,7 @@
</a> </a>
{% endif %} {% endif %}
{% endif %} {% endif %}
<a href="{% url 'dcim:interface_edit' pk=iface.pk %}" class="btn btn-info btn-xs" title="Edit interface"> <a href="{% if iface.device %}{% url 'dcim:interface_edit' pk=iface.pk %}{% else %}{% url 'virtualization:interface_edit' pk=iface.pk %}{% endif %}" class="btn btn-info btn-xs" title="Edit interface">
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i> <i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
</a> </a>
{% endif %} {% endif %}
@ -106,62 +124,63 @@
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i> <i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</button> </button>
{% else %} {% else %}
<a href="{% url 'dcim:interface_delete' pk=iface.pk %}" class="btn btn-danger btn-xs" title="Delete interface"> <a href="{% if iface.device %}{% url 'dcim:interface_delete' pk=iface.pk %}{% else %}{% url 'virtualization:interface_delete' pk=iface.pk %}{% endif %}" class="btn btn-danger btn-xs" title="Delete interface">
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i> <i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</a> </a>
{% endif %} {% endif %}
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% with iface.ip_addresses.all as ipaddresses %} {% for ip in iface.ip_addresses.all %}
{% if ipaddresses %} <tr class="ipaddress">
<tr class="ipaddress">
{% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %} {# Placeholder #}
<td></td> {% if perms.dcim.change_interface or perms.dcim.delete_interface %}
<td colspan="6" class="subtable"> <td></td>
{% else %} {% endif %}
<td colspan="7" class="subtable">
{# IP address #}
<td colspan="2">
<a href="{% url 'ipam:ipaddress' pk=ip.pk %}">{{ ip }}</a>
</td>
{# Primary, status, role #}
<td>
{% if device.primary_ip4 == ip or device.primary_ip6 == ip %}
<span class="label label-success">Primary</span>
{% endif %} {% endif %}
<table class="table table-hover"> <span class="label label-{{ ip.get_status_class }}">{{ ip.get_status_display }}</span>
{% for ip in ipaddresses %} {% if ip.role %}
<tr> <span class="label label-{{ ip.get_role_class }}">{{ ip.get_role_display }}</span>
<td> {% endif %}
<a href="{% url 'ipam:ipaddress' pk=ip.pk %}">{{ ip }}</a> </td>
{% if ip.description %}
<i class="fa fa-fw fa-comment-o" title="{{ ip.description }}"></i> {# VRF #}
{% endif %} <td colspan="2">
</td> {% if ip.vrf %}
<td> <a href="{% url 'ipam:vrf' pk=ip.vrf.pk %}" title="{{ ip.vrf.rd }}">{{ ip.vrf.name }}</a>
{% if device.primary_ip4 == ip or device.primary_ip6 == ip %} {% else %}
<span class="label label-success">Primary</span> <span class="text-muted">Global</span>
{% endif %} {% endif %}
</td> </td>
<td>
{% if ip.vrf %} {# Description #}
<a href="{% url 'ipam:vrf' pk=ip.vrf.pk %}">{{ ip.vrf }}</a> <td colspan="2">
{% else %} {{ ip.description }}
<span class="text-muted">Global table</span> </td>
{% endif %}
</td> {# Buttons #}
<td> <td class="text-right text-nowrap">
<span class="label label-{{ ip.get_status_class }}">{{ ip.get_status_display }}</span> {% if perms.ipam.change_ipaddress %}
</td> <a href="{% url 'ipam:ipaddress_edit' pk=ip.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-info btn-xs">
<td class="text-right"> <i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit IP address"></i>
{% if perms.ipam.change_ipaddress %} </a>
<a href="{% url 'ipam:ipaddress_edit' pk=ip.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-info btn-xs"> {% endif %}
<i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit IP address"></i> {% if perms.ipam.delete_ipaddress %}
</a> <a href="{% url 'ipam:ipaddress_delete' pk=ip.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
{% endif %} <i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete IP address"></i>
{% if perms.ipam.delete_ipaddress %} </a>
<a href="{% url 'ipam:ipaddress_delete' pk=ip.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs"> {% endif %}
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete IP address"></i> </td>
</a> </tr>
{% endif %} {% endfor %}
</td>
</tr>
{% endfor %}
</table>
</td>
</tr>
{% endif %}
{% endwith %}

View File

@ -1,5 +1,5 @@
<tr> <tr>
<td style="padding-left: {{ indent|add:5 }}px">{{ item.name }}</td> <td style="padding-left: {{ indent|add:5 }}px">{{ item }}</td>
<td>{% if not item.discovered %}<i class="fa fa-asterisk" title="Manually created"></i>{% endif %}</td> <td>{% if not item.discovered %}<i class="fa fa-asterisk" title="Manually created"></i>{% endif %}</td>
<td>{{ item.manufacturer|default:"" }}</td> <td>{{ item.manufacturer|default:"" }}</td>
<td>{{ item.part_id }}</td> <td>{{ item.part_id }}</td>

View File

@ -1,26 +1,25 @@
<tr class="poweroutlet{% if po.connected_port and not po.connected_port.connection_status %} info{% endif %}"> <tr class="poweroutlet{% if po.connected_port and not po.connected_port.connection_status %} info{% endif %}">
{% if selectable and perms.dcim.change_poweroutlet or perms.dcim.delete_poweroutlet %} {% if perms.dcim.change_poweroutlet or perms.dcim.delete_poweroutlet %}
<td class="pk"> <td class="pk">
<input name="pk" type="checkbox" value="{{ po.pk }}" /> <input name="pk" type="checkbox" value="{{ po.pk }}" />
</td> </td>
{% endif %} {% endif %}
<td> <td>
<i class="fa fa-fw fa-bolt"></i> {{ po.name }} <i class="fa fa-fw fa-bolt"></i> {{ po }}
</td> </td>
<td></td>
{% if po.connected_port %} {% if po.connected_port %}
<td> <td>
<a href="{% url 'dcim:device' pk=po.connected_port.device.pk %}">{{ po.connected_port.device }}</a> <a href="{% url 'dcim:device' pk=po.connected_port.device.pk %}">{{ po.connected_port.device }}</a>
</td> </td>
<td> <td>
{{ po.connected_port.name }} {{ po.connected_port }}
</td> </td>
{% else %} {% else %}
<td colspan="2"> <td colspan="2">
<span class="text-muted">Not connected</span> <span class="text-muted">Not connected</span>
</td> </td>
{% endif %} {% endif %}
<td colspan="2" class="text-right"> <td class="text-right">
{% if perms.dcim.change_poweroutlet %} {% if perms.dcim.change_poweroutlet %}
{% if po.connected_port %} {% if po.connected_port %}
{% if po.connected_port.connection_status %} {% if po.connected_port.connection_status %}

View File

@ -1,13 +1,13 @@
<tr class="powerport{% if pp.power_outlet and not pp.connection_status %} info{% endif %}"> <tr class="powerport{% if pp.power_outlet and not pp.connection_status %} info{% endif %}">
<td> <td>
<i class="fa fa-fw fa-bolt"></i> {{ pp.name }} <i class="fa fa-fw fa-bolt"></i> {{ pp }}
</td> </td>
{% if pp.power_outlet %} {% if pp.power_outlet %}
<td> <td>
<a href="{% url 'dcim:device' pk=pp.power_outlet.device.pk %}">{{ pp.power_outlet.device }}</a> <a href="{% url 'dcim:device' pk=pp.power_outlet.device.pk %}">{{ pp.power_outlet.device }}</a>
</td> </td>
<td> <td>
{{ pp.power_outlet.name }} {{ pp.power_outlet }}
</td> </td>
{% else %} {% else %}
<td colspan="2"> <td colspan="2">

View File

@ -49,7 +49,7 @@
</a> </a>
{% endif %} {% endif %}
</div> </div>
<h1>{% block title %}Rack {{ rack.name }}{% endblock %}</h1> <h1>{% block title %}Rack {{ rack }}{% endblock %}</h1>
{% include 'inc/created_updated.html' with obj=rack %} {% include 'inc/created_updated.html' with obj=rack %}
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
@ -72,7 +72,7 @@
<td>Group</td> <td>Group</td>
<td> <td>
{% if rack.group %} {% if rack.group %}
<a href="{% url 'dcim:rack_list' %}?site={{ rack.site.slug }}&group={{ rack.group.slug }}">{{ rack.group.name }}</a> <a href="{% url 'dcim:rack_list' %}?site={{ rack.site.slug }}&group={{ rack.group.slug }}">{{ rack.group }}</a>
{% else %} {% else %}
<span class="text-muted">None</span> <span class="text-muted">None</span>
{% endif %} {% endif %}
@ -93,7 +93,7 @@
<td> <td>
{% if rack.tenant %} {% if rack.tenant %}
{% if rack.tenant.group %} {% if rack.tenant.group %}
<a href="{{ rack.tenant.group.get_absolute_url }}">{{ rack.tenant.group.name }}</a> <a href="{{ rack.tenant.group.get_absolute_url }}">{{ rack.tenant.group }}</a>
<i class="fa fa-angle-right"></i> <i class="fa fa-angle-right"></i>
{% endif %} {% endif %}
<a href="{{ rack.tenant.get_absolute_url }}">{{ rack.tenant }}</a> <a href="{{ rack.tenant.get_absolute_url }}">{{ rack.tenant }}</a>
@ -173,7 +173,7 @@
{% for device in nonracked_devices %} {% for device in nonracked_devices %}
<tr{% if device.device_type.u_height %} class="warning"{% endif %}> <tr{% if device.device_type.u_height %} class="warning"{% endif %}>
<td> <td>
<a href="{% url 'dcim:device' pk=device.pk %}">{{ device.name }}</a> <a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a>
</td> </td>
<td>{{ device.device_role }}</td> <td>{{ device.device_role }}</td>
<td>{{ device.device_type.full_name }}</td> <td>{{ device.device_type.full_name }}</td>

View File

@ -76,7 +76,7 @@
<td> <td>
{% if site.tenant %} {% if site.tenant %}
{% if site.tenant.group %} {% if site.tenant.group %}
<a href="{{ site.tenant.group.get_absolute_url }}">{{ site.tenant.group.name }}</a> <a href="{{ site.tenant.group.get_absolute_url }}">{{ site.tenant.group }}</a>
<i class="fa fa-angle-right"></i> <i class="fa fa-angle-right"></i>
{% endif %} {% endif %}
<a href="{{ site.tenant.get_absolute_url }}">{{ site.tenant }}</a> <a href="{{ site.tenant.get_absolute_url }}">{{ site.tenant }}</a>
@ -221,7 +221,7 @@
<table class="table table-hover panel-body"> <table class="table table-hover panel-body">
{% for rg in rack_groups %} {% for rg in rack_groups %}
<tr> <tr>
<td><i class="fa fa-fw fa-folder-o"></i> <a href="{{ rg.get_absolute_url }}">{{ rg.name }}</a></td> <td><i class="fa fa-fw fa-folder-o"></i> <a href="{{ rg.get_absolute_url }}">{{ rg }}</a></td>
<td>{{ rg.rack_count }}</td> <td>{{ rg.rack_count }}</td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@ -0,0 +1,18 @@
{% extends '500.html' %}
{% block message %}
<p>
A module import error occurred during this request. Common causes include the following:
</p>
<p>
<i class="fa fa-warning"></i> <strong>Missing required packages</strong> - This installation of NetBox might be missing one or more required
Python packages. These packages are listed in <code>requirements.txt</code> and are normally installed as part
of the installation or upgrade process. To verify installed packages, run <code>pip freeze</code> from the
console and compare the output to the list of required packages.
</p>
<p>
<i class="fa fa-warning"></i> <strong>WSGI service not restarted after upgrade</strong> - If this installation has recently been upgraded,
check that the WSGI service (e.g. gunicorn or uWSGI) has been restarted. This ensures that the new code is
running.
</p>
{% endblock %}

View File

@ -0,0 +1,12 @@
{% extends '500.html' %}
{% block message %}
<p>
A file permission error was detected while processing this request. Common causes include the following:
</p>
<p>
<i class="fa fa-warning"></i> <strong>Insufficient write permission to the media root</strong> - The configured
media root is <code>{{ settings.MEDIA_ROOT }}</code>. Ensure that the user NetBox runs as has access to write
files to all locations within this path.
</p>
{% endblock %}

View File

@ -0,0 +1,17 @@
{% extends '500.html' %}
{% block message %}
<p>
A database programming error was detected while processing this request. Common causes include the following:
</p>
<p>
<i class="fa fa-warning"></i> <strong>Database migrations missing</strong> - When upgrading to a new NetBox release, the upgrade script must
be run to apply any new database migrations. You can run migrations manually by executing
<code>python3 manage.py migrate</code> from the command line.
</p>
<p>
<i class="fa fa-warning"></i> <strong>Unsupported PostgreSQL version</strong> - Ensure that PostgreSQL version 9.4 or higher is in use. You
can check this by connecting to the database using NetBox's credentials and issuing a query for
<code>SELECT VERSION()</code>.
</p>
{% endblock %}

View File

@ -1,4 +1,16 @@
{% load helpers %}
<ul class="nav nav-tabs" style="margin-bottom: 20px"> <ul class="nav nav-tabs" style="margin-bottom: 20px">
<li role="presentation"{% if active_tab == 'add' %} class="active"{% endif %}><a href="{% url 'ipam:ipaddress_add' %}">Individual</a></li> <li role="presentation"{% if active_tab == 'add' %} class="active"{% endif %}>
<li role="presentation"{% if active_tab == 'bulk_add' %} class="active"{% endif %}><a href="{% url 'ipam:ipaddress_bulk_add' %}">Bulk</a></li> <a href="{% url 'ipam:ipaddress_add' %}{% querystring request %}">New IP</a>
</li>
{% if 'interface' in request.GET %}
<li role="presentation"{% if active_tab == 'assign' %} class="active"{% endif %}>
<a href="{% url 'ipam:ipaddress_assign' %}{% querystring request %}">Assign IP</a>
</li>
{% else %}
<li role="presentation"{% if active_tab == 'bulk_add' %} class="active"{% endif %}>
<a href="{% url 'ipam:ipaddress_bulk_add' %}{% querystring request %}">Bulk Create</a>
</li>
{% endif %}
</ul> </ul>

View File

@ -0,0 +1,48 @@
{% extends 'utilities/obj_edit.html' %}
{% load static from staticfiles %}
{% load form_helpers %}
{% load helpers %}
{% block content %}
<form action="{% querystring request %}" method="post" class="form form-horizontal">
{% csrf_token %}
{% for field in form.hidden_fields %}
{{ field }}
{% endfor %}
<div class="row">
<div class="col-md-6 col-md-offset-3">
<h3>Assign an IP Address</h3>
{% include 'ipam/inc/ipadress_edit_header.html' with active_tab='assign' %}
{% if form.non_field_errors %}
<div class="panel panel-danger">
<div class="panel-heading"><strong>Errors</strong></div>
<div class="panel-body">
{{ form.non_field_errors }}
</div>
</div>
{% endif %}
<div class="panel panel-default">
<div class="panel-heading"><strong>Select IP Address</strong></div>
<div class="panel-body">
{% render_field form.vrf %}
{% render_field form.address %}
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6 col-md-offset-3 text-right">
<button type="submit" class="btn btn-primary">Search</button>
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
</div>
</div>
</form>
{% if table %}
<div class="row">
<div class="col-md-10 col-md-offset-1" style="margin-top: 20px">
<h3>Search Results</h3>
{% include 'utilities/obj_table.html' with table_template='panel_table.html' %}
</div>
</div>
{% endif %}
{% endblock %}

View File

@ -30,13 +30,13 @@
<td> <td>
{% if prefix.tenant %} {% if prefix.tenant %}
{% if prefix.tenant.group %} {% if prefix.tenant.group %}
<a href="{{ prefix.tenant.group.get_absolute_url }}">{{ prefix.tenant.group.name }}</a> <a href="{{ prefix.tenant.group.get_absolute_url }}">{{ prefix.tenant.group }}</a>
<i class="fa fa-angle-right"></i> <i class="fa fa-angle-right"></i>
{% endif %} {% endif %}
<a href="{{ prefix.tenant.get_absolute_url }}">{{ prefix.tenant }}</a> <a href="{{ prefix.tenant.get_absolute_url }}">{{ prefix.tenant }}</a>
{% elif prefix.vrf.tenant %} {% elif prefix.vrf.tenant %}
{% if prefix.vrf.tenant.group %} {% if prefix.vrf.tenant.group %}
<a href="{{ prefix.vrf.tenant.group.get_absolute_url }}">{{ prefix.vrf.tenant.group.name }}</a> <a href="{{ prefix.vrf.tenant.group.get_absolute_url }}">{{ prefix.vrf.tenant.group }}</a>
<i class="fa fa-angle-right"></i> <i class="fa fa-angle-right"></i>
{% endif %} {% endif %}
<a href="{{ prefix.vrf.tenant.get_absolute_url }}">{{ prefix.vrf.tenant }}</a> <a href="{{ prefix.vrf.tenant.get_absolute_url }}">{{ prefix.vrf.tenant }}</a>
@ -75,7 +75,7 @@
<td> <td>
{% if prefix.vlan %} {% if prefix.vlan %}
{% if prefix.vlan.group %} {% if prefix.vlan.group %}
<a href="{{ prefix.vlan.group.get_absolute_url }}">{{ prefix.vlan.group.name }}</a> <a href="{{ prefix.vlan.group.get_absolute_url }}">{{ prefix.vlan.group }}</a>
<i class="fa fa-angle-right"></i> <i class="fa fa-angle-right"></i>
{% endif %} {% endif %}
<a href="{% url 'ipam:vlan' pk=prefix.vlan.pk %}">{{ prefix.vlan.display_name }}</a> <a href="{% url 'ipam:vlan' pk=prefix.vlan.pk %}">{{ prefix.vlan.display_name }}</a>

View File

@ -9,9 +9,9 @@
<li><a href="{% url 'ipam:vlan_list' %}?site={{ vlan.site.slug }}">{{ vlan.site }}</a></li> <li><a href="{% url 'ipam:vlan_list' %}?site={{ vlan.site.slug }}">{{ vlan.site }}</a></li>
{% endif %} {% endif %}
{% if vlan.group %} {% if vlan.group %}
<li><a href="{% url 'ipam:vlan_list' %}?group={{ vlan.group.slug }}">{{ vlan.group.name }}</a></li> <li><a href="{% url 'ipam:vlan_list' %}?group={{ vlan.group.slug }}">{{ vlan.group }}</a></li>
{% endif %} {% endif %}
<li>{{ vlan.name }} ({{ vlan.vid }})</li> <li>{{ vlan }}</li>
</ol> </ol>
</div> </div>
<div class="col-sm-4 col-md-3"> <div class="col-sm-4 col-md-3">
@ -68,7 +68,7 @@
<td>Group</td> <td>Group</td>
<td> <td>
{% if vlan.group %} {% if vlan.group %}
<a href="{{ vlan.group.get_absolute_url }}">{{ vlan.group.name }}</a> <a href="{{ vlan.group.get_absolute_url }}">{{ vlan.group }}</a>
{% else %} {% else %}
<span class="text-muted">None</span> <span class="text-muted">None</span>
{% endif %} {% endif %}
@ -87,7 +87,7 @@
<td> <td>
{% if vlan.tenant %} {% if vlan.tenant %}
{% if vlan.tenant.group %} {% if vlan.tenant.group %}
<a href="{{ vlan.tenant.group.get_absolute_url }}">{{ vlan.tenant.group.name }}</a> <a href="{{ vlan.tenant.group.get_absolute_url }}">{{ vlan.tenant.group }}</a>
<i class="fa fa-angle-right"></i> <i class="fa fa-angle-right"></i>
{% endif %} {% endif %}
<a href="{{ vlan.tenant.get_absolute_url }}">{{ vlan.tenant }}</a> <a href="{{ vlan.tenant.get_absolute_url }}">{{ vlan.tenant }}</a>

View File

@ -1,70 +0,0 @@
<tr class="interface{% if not iface.enabled %} danger{% endif %}">
{% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %}
<td class="pk">
<input name="pk" type="checkbox" value="{{ iface.pk }}" />
</td>
{% endif %}
<td>
<i class="fa fa-fw fa-square"></i> <span>{{ iface.name }}</span>
{% if iface.description %}
<i class="fa fa-fw fa-comment-o" title="{{ iface.description }}"></i>
{% endif %}
</td>
<td>{{ iface.mtu|default:"" }}</td>
<td>{{ iface.mac_address|default:"" }}</td>
<td class="text-right">
{% if perms.ipam.add_ipaddress %}
<a href="{% url 'ipam:ipaddress_add' %}?interface={{ iface.pk }}&return_url={{ vm.get_absolute_url }}" class="btn btn-xs btn-success" title="Add IP address">
<i class="glyphicon glyphicon-plus" aria-hidden="true"></i>
</a>
{% endif %}
{% if perms.dcim.change_interface %}
<a href="{% url 'virtualization:interface_edit' pk=iface.pk %}" class="btn btn-info btn-xs" title="Edit interface">
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
</a>
{% endif %}
{% if perms.dcim.delete_interface %}
<a href="{% url 'virtualization:interface_delete' pk=iface.pk %}" class="btn btn-danger btn-xs" title="Delete interface">
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</a>
{% endif %}
</td>
</tr>
{% for ip in iface.ip_addresses.all %}
<tr class="ipaddress">
{% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %}
<td></td>
{% endif %}
<td>
<a href="{% url 'ipam:ipaddress' pk=ip.pk %}">{{ ip }}</a>
{% if ip.description %}
<i class="fa fa-fw fa-comment-o" title="{{ ip.description }}"></i>
{% endif %}
{% if vm.primary_ip4 == ip or vm.primary_ip6 == ip %}
<span class="label label-success">Primary</span>
{% endif %}
</td>
<td class="text-right">
{% if ip.vrf %}
<a href="{% url 'ipam:vrf' pk=ip.vrf.pk %}">{{ ip.vrf }}</a>
{% else %}
<span class="text-muted">Global</span>
{% endif %}
</td>
<td>
<span class="label label-{{ ip.get_status_class }}">{{ ip.get_status_display }}</span>
</td>
<td class="text-right">
{% if perms.ipam.change_ipaddress %}
<a href="{% url 'ipam:ipaddress_edit' pk=ip.pk %}?return_url={{ vm.get_absolute_url }}" class="btn btn-info btn-xs">
<i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit IP address"></i>
</a>
{% endif %}
{% if perms.ipam.delete_ipaddress %}
<a href="{% url 'ipam:ipaddress_delete' pk=ip.pk %}?return_url={{ vm.get_absolute_url }}" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete IP address"></i>
</a>
{% endif %}
</td>
</tr>
{% endfor %}

View File

@ -41,7 +41,7 @@
<h1>{% block title %}{{ vm }}{% endblock %}</h1> <h1>{% block title %}{{ vm }}{% endblock %}</h1>
{% include 'inc/created_updated.html' with obj=vm %} {% include 'inc/created_updated.html' with obj=vm %}
<div class="row"> <div class="row">
<div class="col-md-5"> <div class="col-md-6">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<strong>Virtual Machine</strong> <strong>Virtual Machine</strong>
@ -49,7 +49,7 @@
<table class="table table-hover panel-body attr-table"> <table class="table table-hover panel-body attr-table">
<tr> <tr>
<td>Name</td> <td>Name</td>
<td>{{ vm.name }}</td> <td>{{ vm }}</td>
</tr> </tr>
<tr> <tr>
<td>Status</td> <td>Status</td>
@ -82,7 +82,7 @@
<td> <td>
{% if vm.tenant %} {% if vm.tenant %}
{% if vm.tenant.group %} {% if vm.tenant.group %}
<a href="{{ vm.tenant.group.get_absolute_url }}">{{ vm.tenant.group.name }}</a> <a href="{{ vm.tenant.group.get_absolute_url }}">{{ vm.tenant.group }}</a>
<i class="fa fa-angle-right"></i> <i class="fa fa-angle-right"></i>
{% endif %} {% endif %}
<a href="{{ vm.tenant.get_absolute_url }}">{{ vm.tenant }}</a> <a href="{{ vm.tenant.get_absolute_url }}">{{ vm.tenant }}</a>
@ -123,6 +123,21 @@
</tr> </tr>
</table> </table>
</div> </div>
{% include 'inc/custom_fields_panel.html' with custom_fields=vm.get_custom_fields %}
<div class="panel panel-default">
<div class="panel-heading">
<strong>Comments</strong>
</div>
<div class="panel-body">
{% if vm.comments %}
{{ vm.comments|gfm }}
{% else %}
<span class="text-muted">None</span>
{% endif %}
</div>
</div>
</div>
<div class="col-md-6">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<strong>Cluster</strong> <strong>Cluster</strong>
@ -204,21 +219,10 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% include 'inc/custom_fields_panel.html' with custom_fields=vm.get_custom_fields %}
<div class="panel panel-default">
<div class="panel-heading">
<strong>Comments</strong>
</div>
<div class="panel-body">
{% if vm.comments %}
{{ vm.comments|gfm }}
{% else %}
<span class="text-muted">None</span>
{% endif %}
</div>
</div>
</div> </div>
<div class="col-md-7"> </div>
<div class="row">
<div class="col-md-12">
{% if perms.dcim.change_interface or perms.dcim.delete_interface %} {% if perms.dcim.change_interface or perms.dcim.delete_interface %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
@ -244,11 +248,23 @@
</div> </div>
</div> </div>
<table id="interfaces_table" class="table table-hover panel-body component-list"> <table id="interfaces_table" class="table table-hover panel-body component-list">
<tr class="table-headings">
{% if perms.dcim.change_interface or perms.dcim.delete_interface %}
<th></th>
{% endif %}
<th>Name</th>
<th>LAG</th>
<th>Description</th>
<th>MTU</th>
<th>MAC Address</th>
<th colspan="2">Connection</th>
<th></th>
</tr>
{% for iface in interfaces %} {% for iface in interfaces %}
{% include 'virtualization/inc/interface.html' with selectable=True %} {% include 'dcim/inc/interface.html' with device=vm %}
{% empty %} {% empty %}
<tr> <tr>
<td colspan="4">No interfaces defined</td> <td colspan="6">No interfaces defined</td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>

View File

@ -1,7 +1,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import django_filters import django_filters
from django.db.models import Q from django.db.models import Q
from extras.filters import CustomFieldFilterSet from extras.filters import CustomFieldFilterSet

View File

@ -3,10 +3,8 @@ from __future__ import unicode_literals
import django_tables2 as tables import django_tables2 as tables
from utilities.tables import BaseTable, ToggleColumn from utilities.tables import BaseTable, ToggleColumn
from .models import Tenant, TenantGroup from .models import Tenant, TenantGroup
TENANTGROUP_ACTIONS = """ TENANTGROUP_ACTIONS = """
{% if perms.tenancy.change_tenantgroup %} {% if perms.tenancy.change_tenantgroup %}
<a href="{% url 'tenancy:tenantgroup_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a> <a href="{% url 'tenancy:tenantgroup_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>

View File

@ -1,10 +1,9 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from rest_framework import status
from rest_framework.test import APITestCase
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.urls import reverse from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from tenancy.models import Tenant, TenantGroup from tenancy.models import Tenant, TenantGroup
from users.models import Token from users.models import Token

View File

@ -4,7 +4,6 @@ from django.conf.urls import url
from . import views from . import views
app_name = 'tenancy' app_name = 'tenancy'
urlpatterns = [ urlpatterns = [

View File

@ -13,8 +13,8 @@ from utilities.views import (
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
) )
from virtualization.models import VirtualMachine from virtualization.models import VirtualMachine
from .models import Tenant, TenantGroup
from . import filters, forms, tables from . import filters, forms, tables
from .models import Tenant, TenantGroup
# #

View File

@ -1,7 +1,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.contrib.auth.models import User from django.contrib.auth.models import User
from rest_framework import serializers from rest_framework import serializers

View File

@ -1,7 +1,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.contrib.auth.forms import AuthenticationForm, PasswordChangeForm as DjangoPasswordChangeForm
from django import forms from django import forms
from django.contrib.auth.forms import AuthenticationForm, PasswordChangeForm as DjangoPasswordChangeForm
from utilities.forms import BootstrapMixin from utilities.forms import BootstrapMixin
from .models import Token from .models import Token

View File

@ -1,12 +1,13 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import binascii import binascii
import os import os
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.validators import MinLengthValidator from django.core.validators import MinLengthValidator
from django.db import models from django.db import models
from django.utils.encoding import python_2_unicode_compatible
from django.utils import timezone from django.utils import timezone
from django.utils.encoding import python_2_unicode_compatible
@python_2_unicode_compatible @python_2_unicode_compatible

View File

@ -4,7 +4,6 @@ from django.conf.urls import url
from . import views from . import views
app_name = 'user' app_name = 'user'
urlpatterns = [ urlpatterns = [

View File

@ -1,17 +1,16 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import OrderedDict from collections import OrderedDict
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.http import Http404 from django.http import Http404
from rest_framework.exceptions import APIException from rest_framework.exceptions import APIException
from rest_framework.permissions import BasePermission from rest_framework.permissions import BasePermission
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import Field, ModelSerializer, ValidationError from rest_framework.serializers import Field, ModelSerializer, ValidationError
from rest_framework.viewsets import ViewSet from rest_framework.viewsets import ViewSet
WRITE_OPERATIONS = ['create', 'update', 'partial_update', 'delete'] WRITE_OPERATIONS = ['create', 'update', 'partial_update', 'delete']

View File

@ -5,7 +5,6 @@ from django.db import models
from .forms import ColorSelect from .forms import ColorSelect
validate_color = RegexValidator('^[0-9a-f]{6}$', 'Enter a valid hexadecimal RGB color code.', 'invalid') validate_color = RegexValidator('^[0-9a-f]{6}$', 'Enter a valid hexadecimal RGB color code.', 'invalid')

View File

@ -1,8 +1,8 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import django_filters
import itertools import itertools
import django_filters
from django import forms from django import forms
from django.utils.encoding import force_text from django.utils.encoding import force_text

Some files were not shown because too many files have changed in this diff Show More