diff --git a/.gitignore b/.gitignore index 4fc377333..a86cd73ae 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ *.pyc /netbox/netbox/configuration.py +/netbox/netbox/ldap_config.py /netbox/static .idea /*.sh diff --git a/.travis.yml b/.travis.yml index e8822e5e3..b23c9d8fc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,9 @@ env: language: python python: - "2.7" + - "3.4" + - "3.5" + - "3.6" install: - pip install -r requirements.txt - pip install pep8 diff --git a/docs/installation/netbox.md b/docs/installation/netbox.md index fa476b865..78bcd93d3 100644 --- a/docs/installation/netbox.md +++ b/docs/installation/netbox.md @@ -3,14 +3,14 @@ **Debian/Ubuntu** ```no-highlight -# apt-get install -y python2.7 python-dev python-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev +# apt-get install -y python3 python3-dev python3-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev ``` **CentOS/RHEL** ```no-highlight # yum install -y epel-release -# yum install -y gcc python2 python-devel python-pip libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel +# yum install -y gcc python3 python3-devel python3-pip libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel ``` You may opt to install NetBox either from a numbered release or by cloning the master branch of its repository on GitHub. diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index 7f6cc4f21..6a0380dd5 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -1,6 +1,7 @@ from django.contrib.contenttypes.fields import GenericRelation from django.core.urlresolvers import reverse from django.db import models +from django.utils.encoding import python_2_unicode_compatible from dcim.fields import ASNField from extras.models import CustomFieldModel, CustomFieldValue @@ -33,6 +34,7 @@ def humanize_speed(speed): return '{} Kbps'.format(speed) +@python_2_unicode_compatible class Provider(CreatedUpdatedModel, CustomFieldModel): """ Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model @@ -51,7 +53,7 @@ class Provider(CreatedUpdatedModel, CustomFieldModel): class Meta: ordering = ['name'] - def __unicode__(self): + def __str__(self): return self.name def get_absolute_url(self): @@ -67,6 +69,7 @@ class Provider(CreatedUpdatedModel, CustomFieldModel): ]) +@python_2_unicode_compatible class CircuitType(models.Model): """ Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named @@ -78,13 +81,14 @@ class CircuitType(models.Model): class Meta: ordering = ['name'] - def __unicode__(self): + def __str__(self): return self.name def get_absolute_url(self): return "{}?type={}".format(reverse('circuits:circuit_list'), self.slug) +@python_2_unicode_compatible class Circuit(CreatedUpdatedModel, CustomFieldModel): """ A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple @@ -105,7 +109,7 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel): ordering = ['provider', 'cid'] unique_together = ['provider', 'cid'] - def __unicode__(self): + def __str__(self): return u'{} {}'.format(self.provider, self.cid) def get_absolute_url(self): @@ -141,6 +145,7 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel): commit_rate_human.admin_order_field = 'commit_rate' +@python_2_unicode_compatible class CircuitTermination(models.Model): circuit = models.ForeignKey('Circuit', related_name='terminations', on_delete=models.CASCADE) term_side = models.CharField(max_length=1, choices=TERM_SIDE_CHOICES, verbose_name='Termination') @@ -156,7 +161,7 @@ class CircuitTermination(models.Model): ordering = ['circuit', 'term_side'] unique_together = ['circuit', 'term_side'] - def __unicode__(self): + def __str__(self): return u'{} (Side {})'.format(self.circuit, self.get_term_side_display()) def get_peer_termination(self): diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index cb35ff881..9931ffd8b 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -13,7 +13,7 @@ from utilities.forms import ( SlugField, ) -from formfields import MACAddressFormField +from .formfields import MACAddressFormField from .models import ( DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType, diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 6274f3a53..d29ca745d 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -8,6 +8,7 @@ from django.core.urlresolvers import reverse from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from django.db.models import Count, Q, ObjectDoesNotExist +from django.utils.encoding import python_2_unicode_compatible from circuits.models import Circuit from extras.models import CustomFieldModel, CustomField, CustomFieldValue @@ -199,6 +200,7 @@ class SiteManager(NaturalOrderByManager): return self.natural_order_by('name') +@python_2_unicode_compatible class Site(CreatedUpdatedModel, CustomFieldModel): """ A Site represents a geographic location within a network; typically a building or campus. The optional facility @@ -222,7 +224,7 @@ class Site(CreatedUpdatedModel, CustomFieldModel): class Meta: ordering = ['name'] - def __unicode__(self): + def __str__(self): return self.name def get_absolute_url(self): @@ -265,6 +267,7 @@ class Site(CreatedUpdatedModel, CustomFieldModel): # Racks # +@python_2_unicode_compatible class RackGroup(models.Model): """ Racks can be grouped as subsets within a Site. The scope of a group will depend on how Sites are defined. For @@ -282,13 +285,14 @@ class RackGroup(models.Model): ['site', 'slug'], ] - def __unicode__(self): + def __str__(self): return u'{} - {}'.format(self.site.name, self.name) def get_absolute_url(self): return "{}?group_id={}".format(reverse('dcim:rack_list'), self.pk) +@python_2_unicode_compatible class RackRole(models.Model): """ Racks can be organized by functional role, similar to Devices. @@ -300,7 +304,7 @@ class RackRole(models.Model): class Meta: ordering = ['name'] - def __unicode__(self): + def __str__(self): return self.name def get_absolute_url(self): @@ -313,6 +317,7 @@ class RackManager(NaturalOrderByManager): return self.natural_order_by('site__name', 'name') +@python_2_unicode_compatible class Rack(CreatedUpdatedModel, CustomFieldModel): """ Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face. @@ -343,7 +348,7 @@ class Rack(CreatedUpdatedModel, CustomFieldModel): ['site', 'facility_id'], ] - def __unicode__(self): + def __str__(self): return self.display_name def get_absolute_url(self): @@ -442,7 +447,7 @@ class Rack(CreatedUpdatedModel, CustomFieldModel): devices = self.devices.select_related('device_type').filter(position__gte=1).exclude(pk__in=exclude) # Initialize the rack unit skeleton - units = range(1, self.u_height + 1) + units = list(range(1, self.u_height + 1)) # Remove units consumed by installed devices for d in devices: @@ -477,6 +482,7 @@ class Rack(CreatedUpdatedModel, CustomFieldModel): # Device Types # +@python_2_unicode_compatible class Manufacturer(models.Model): """ A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell. @@ -487,13 +493,14 @@ class Manufacturer(models.Model): class Meta: ordering = ['name'] - def __unicode__(self): + def __str__(self): return self.name def get_absolute_url(self): return "{}?manufacturer={}".format(reverse('dcim:devicetype_list'), self.slug) +@python_2_unicode_compatible class DeviceType(models.Model, CustomFieldModel): """ A DeviceType represents a particular make (Manufacturer) and model of device. It specifies rack height and depth, as @@ -538,7 +545,7 @@ class DeviceType(models.Model, CustomFieldModel): ['manufacturer', 'slug'], ] - def __unicode__(self): + def __str__(self): return self.model def __init__(self, *args, **kwargs): @@ -608,6 +615,7 @@ class DeviceType(models.Model, CustomFieldModel): return bool(self.subdevice_role is False) +@python_2_unicode_compatible class ConsolePortTemplate(models.Model): """ A template for a ConsolePort to be created for a new Device. @@ -619,10 +627,11 @@ class ConsolePortTemplate(models.Model): ordering = ['device_type', 'name'] unique_together = ['device_type', 'name'] - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class ConsoleServerPortTemplate(models.Model): """ A template for a ConsoleServerPort to be created for a new Device. @@ -634,10 +643,11 @@ class ConsoleServerPortTemplate(models.Model): ordering = ['device_type', 'name'] unique_together = ['device_type', 'name'] - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class PowerPortTemplate(models.Model): """ A template for a PowerPort to be created for a new Device. @@ -649,10 +659,11 @@ class PowerPortTemplate(models.Model): ordering = ['device_type', 'name'] unique_together = ['device_type', 'name'] - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class PowerOutletTemplate(models.Model): """ A template for a PowerOutlet to be created for a new Device. @@ -664,7 +675,7 @@ class PowerOutletTemplate(models.Model): ordering = ['device_type', 'name'] unique_together = ['device_type', 'name'] - def __unicode__(self): + def __str__(self): return self.name @@ -706,6 +717,7 @@ class InterfaceManager(models.Manager): }).order_by(*ordering) +@python_2_unicode_compatible class InterfaceTemplate(models.Model): """ A template for a physical data interface on a new Device. @@ -721,10 +733,11 @@ class InterfaceTemplate(models.Model): ordering = ['device_type', 'name'] unique_together = ['device_type', 'name'] - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class DeviceBayTemplate(models.Model): """ A template for a DeviceBay to be created for a new parent Device. @@ -736,7 +749,7 @@ class DeviceBayTemplate(models.Model): ordering = ['device_type', 'name'] unique_together = ['device_type', 'name'] - def __unicode__(self): + def __str__(self): return self.name @@ -744,6 +757,7 @@ class DeviceBayTemplate(models.Model): # Devices # +@python_2_unicode_compatible class DeviceRole(models.Model): """ Devices are organized by functional role; for example, "Core Switch" or "File Server". Each DeviceRole is assigned a @@ -756,13 +770,14 @@ class DeviceRole(models.Model): class Meta: ordering = ['name'] - def __unicode__(self): + def __str__(self): return self.name def get_absolute_url(self): return "{}?role={}".format(reverse('dcim:device_list'), self.slug) +@python_2_unicode_compatible class Platform(models.Model): """ Platform refers to the software or firmware running on a Device; for example, "Cisco IOS-XR" or "Juniper Junos". @@ -776,7 +791,7 @@ class Platform(models.Model): class Meta: ordering = ['name'] - def __unicode__(self): + def __str__(self): return self.name def get_absolute_url(self): @@ -789,6 +804,7 @@ class DeviceManager(NaturalOrderByManager): return self.natural_order_by('name') +@python_2_unicode_compatible class Device(CreatedUpdatedModel, CustomFieldModel): """ A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType, @@ -828,7 +844,7 @@ class Device(CreatedUpdatedModel, CustomFieldModel): ordering = ['name'] unique_together = ['rack', 'position', 'face'] - def __unicode__(self): + def __str__(self): return self.display_name def get_absolute_url(self): @@ -968,6 +984,7 @@ class Device(CreatedUpdatedModel, CustomFieldModel): return RPC_CLIENTS.get(self.platform.rpc_client) +@python_2_unicode_compatible class ConsolePort(models.Model): """ A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts. @@ -982,7 +999,7 @@ class ConsolePort(models.Model): ordering = ['device', 'name'] unique_together = ['device', 'name'] - def __unicode__(self): + def __str__(self): return self.name # Used for connections export @@ -1011,6 +1028,7 @@ class ConsoleServerPortManager(models.Manager): }).order_by('device', 'name_as_integer') +@python_2_unicode_compatible class ConsoleServerPort(models.Model): """ A physical port within a Device (typically a designated console server) which provides access to ConsolePorts. @@ -1023,10 +1041,11 @@ class ConsoleServerPort(models.Model): class Meta: unique_together = ['device', 'name'] - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class PowerPort(models.Model): """ A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets. @@ -1041,7 +1060,7 @@ class PowerPort(models.Model): ordering = ['device', 'name'] unique_together = ['device', 'name'] - def __unicode__(self): + def __str__(self): return self.name # Used for connections export @@ -1064,6 +1083,7 @@ class PowerOutletManager(models.Manager): }).order_by('device', 'name_padded') +@python_2_unicode_compatible class PowerOutlet(models.Model): """ A physical power outlet (output) within a Device which provides power to a PowerPort. @@ -1076,10 +1096,11 @@ class PowerOutlet(models.Model): class Meta: unique_together = ['device', 'name'] - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Interface(models.Model): """ A physical data interface within a Device. An Interface can connect to exactly one other Interface via the creation @@ -1099,7 +1120,7 @@ class Interface(models.Model): ordering = ['device', 'name'] unique_together = ['device', 'name'] - def __unicode__(self): + def __str__(self): return self.name def clean(self): @@ -1176,6 +1197,7 @@ class InterfaceConnection(models.Model): ]) +@python_2_unicode_compatible class DeviceBay(models.Model): """ An empty space within a Device which can house a child device @@ -1189,7 +1211,7 @@ class DeviceBay(models.Model): ordering = ['device', 'name'] unique_together = ['device', 'name'] - def __unicode__(self): + def __str__(self): return u'{} - {}'.format(self.device.name, self.name) def clean(self): @@ -1205,6 +1227,7 @@ class DeviceBay(models.Model): raise ValidationError("Cannot install a device into itself.") +@python_2_unicode_compatible class Module(models.Model): """ A Module represents a piece of hardware within a Device, such as a line card or power supply. Modules are used only @@ -1223,5 +1246,5 @@ class Module(models.Model): ordering = ['device__id', 'parent__id', 'name'] unique_together = ['device', 'parent', 'name'] - def __unicode__(self): + def __str__(self): return self.name diff --git a/netbox/dcim/tests/test_apis.py b/netbox/dcim/tests/test_apis.py index 3cef01701..0739b86ce 100644 --- a/netbox/dcim/tests/test_apis.py +++ b/netbox/dcim/tests/test_apis.py @@ -65,7 +65,7 @@ class SiteTest(APITestCase): def test_get_list(self, endpoint='/{}api/dcim/sites/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) for i in content: self.assertEqual( @@ -75,7 +75,7 @@ class SiteTest(APITestCase): def test_get_detail(self, endpoint='/{}api/dcim/sites/1/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual( sorted(content.keys()), @@ -84,9 +84,9 @@ class SiteTest(APITestCase): def test_get_site_list_rack(self, endpoint='/{}api/dcim/sites/1/racks/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) - for i in json.loads(response.content): + for i in json.loads(response.content.decode('utf-8')): self.assertEqual( sorted(i.keys()), sorted(self.rack_fields), @@ -99,9 +99,9 @@ class SiteTest(APITestCase): def test_get_site_list_graphs(self, endpoint='/{}api/dcim/sites/1/graphs/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) - for i in json.loads(response.content): + for i in json.loads(response.content.decode('utf-8')): self.assertEqual( sorted(i.keys()), sorted(self.graph_fields), @@ -159,7 +159,7 @@ class RackTest(APITestCase): def test_get_list(self, endpoint='/{}api/dcim/racks/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) for i in content: self.assertEqual( @@ -173,7 +173,7 @@ class RackTest(APITestCase): def test_get_detail(self, endpoint='/{}api/dcim/racks/1/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual( sorted(content.keys()), @@ -202,7 +202,7 @@ class ManufacturersTest(APITestCase): def test_get_list(self, endpoint='/{}api/dcim/manufacturers/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) for i in content: self.assertEqual( @@ -212,7 +212,7 @@ class ManufacturersTest(APITestCase): def test_get_detail(self, endpoint='/{}api/dcim/manufacturers/1/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual( sorted(content.keys()), @@ -250,7 +250,7 @@ class DeviceTypeTest(APITestCase): def test_get_list(self, endpoint='/{}api/dcim/device-types/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) for i in content: self.assertEqual( @@ -261,7 +261,7 @@ class DeviceTypeTest(APITestCase): def test_detail_list(self, endpoint='/{}api/dcim/device-types/1/'.format(settings.BASE_PATH)): # TODO: details returns list view. # response = self.client.get(endpoint) - # content = json.loads(response.content) + # content = json.loads(response.content.decode('utf-8')) # self.assertEqual(response.status_code, status.HTTP_200_OK) # self.assertEqual( # sorted(content.keys()), @@ -284,7 +284,7 @@ class DeviceRolesTest(APITestCase): def test_get_list(self, endpoint='/{}api/dcim/device-roles/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) for i in content: self.assertEqual( @@ -294,7 +294,7 @@ class DeviceRolesTest(APITestCase): def test_get_detail(self, endpoint='/{}api/dcim/device-roles/1/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual( sorted(content.keys()), @@ -312,7 +312,7 @@ class PlatformsTest(APITestCase): def test_get_list(self, endpoint='/{}api/dcim/platforms/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) for i in content: self.assertEqual( @@ -322,7 +322,7 @@ class PlatformsTest(APITestCase): def test_get_detail(self, endpoint='/{}api/dcim/platforms/1/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual( sorted(content.keys()), @@ -360,7 +360,7 @@ class DeviceTest(APITestCase): def test_get_list(self, endpoint='/{}api/dcim/devices/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) for device in content: self.assertEqual( @@ -425,7 +425,7 @@ class DeviceTest(APITestCase): ] response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) device = content[0] self.assertEqual( @@ -435,7 +435,7 @@ class DeviceTest(APITestCase): def test_get_detail(self, endpoint='/{}api/dcim/devices/1/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual( sorted(content.keys()), @@ -453,7 +453,7 @@ class ConsoleServerPortsTest(APITestCase): def test_get_list(self, endpoint='/{}api/dcim/devices/9/console-server-ports/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) for console_port in content: self.assertEqual( @@ -475,7 +475,7 @@ class ConsolePortsTest(APITestCase): def test_get_list(self, endpoint='/{}api/dcim/devices/1/console-ports/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) for console_port in content: self.assertEqual( @@ -493,7 +493,7 @@ class ConsolePortsTest(APITestCase): def test_get_detail(self, endpoint='/{}api/dcim/console-ports/1/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual( sorted(content.keys()), @@ -514,7 +514,7 @@ class PowerPortsTest(APITestCase): def test_get_list(self, endpoint='/{}api/dcim/devices/1/power-ports/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) for i in content: self.assertEqual( @@ -528,7 +528,7 @@ class PowerPortsTest(APITestCase): def test_get_detail(self, endpoint='/{}api/dcim/power-ports/1/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual( sorted(content.keys()), @@ -549,7 +549,7 @@ class PowerOutletsTest(APITestCase): def test_get_list(self, endpoint='/{}api/dcim/devices/11/power-outlets/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) for i in content: self.assertEqual( @@ -599,7 +599,7 @@ class InterfaceTest(APITestCase): def test_get_list(self, endpoint='/{}api/dcim/devices/1/interfaces/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) for i in content: self.assertEqual( @@ -613,7 +613,7 @@ class InterfaceTest(APITestCase): def test_get_detail(self, endpoint='/{}api/dcim/interfaces/1/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual( sorted(content.keys()), @@ -625,19 +625,19 @@ class InterfaceTest(APITestCase): ) def test_get_graph_list(self, endpoint='/{}api/dcim/interfaces/1/graphs/'.format(settings.BASE_PATH)): - response = self.client.get(endpoint) - content = json.loads(response.content) - self.assertEqual(response.status_code, status.HTTP_200_OK) - for i in content: - self.assertEqual( - sorted(i.keys()), - sorted(SiteTest.graph_fields), - ) + response = self.client.get(endpoint) + content = json.loads(response.content.decode('utf-8')) + self.assertEqual(response.status_code, status.HTTP_200_OK) + for i in content: + self.assertEqual( + sorted(i.keys()), + sorted(SiteTest.graph_fields), + ) def test_get_interface_connections(self, endpoint='/{}api/dcim/interface-connections/4/' .format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual( sorted(content.keys()), @@ -659,7 +659,7 @@ class RelatedConnectionsTest(APITestCase): def test_get_list(self, endpoint=('/{}api/dcim/related-connections/?peer-device=test1-edge1&peer-interface=xe-0/0/3' .format(settings.BASE_PATH))): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual( sorted(content.keys()), diff --git a/netbox/extras/api/renderers.py b/netbox/extras/api/renderers.py index 1b1f4ac27..0fd35c762 100644 --- a/netbox/extras/api/renderers.py +++ b/netbox/extras/api/renderers.py @@ -50,7 +50,7 @@ class FlatJSONRenderer(renderers.BaseRenderer): def render(self, data, media_type=None, renderer_context=None): def flatten(entry): - for key, val in entry.iteritems(): + for key, val in entry.items(): if isinstance(val, dict): for child_key, child_val in flatten(val): yield "{}_{}".format(key, child_key), child_val diff --git a/netbox/extras/models.py b/netbox/extras/models.py index d45e4846f..f06d0aa29 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -8,6 +8,7 @@ from django.core.validators import ValidationError from django.db import models from django.http import HttpResponse from django.template import Template, Context +from django.utils.encoding import python_2_unicode_compatible from django.utils.safestring import mark_safe @@ -93,6 +94,7 @@ class CustomFieldModel(object): return OrderedDict([(field, None) for field in fields]) +@python_2_unicode_compatible class CustomField(models.Model): obj_type = models.ManyToManyField(ContentType, related_name='custom_fields', verbose_name='Object(s)', limit_choices_to={'model__in': CUSTOMFIELD_MODELS}, @@ -114,7 +116,7 @@ class CustomField(models.Model): class Meta: ordering = ['weight', 'name'] - def __unicode__(self): + def __str__(self): return self.label or self.name.replace('_', ' ').capitalize() def serialize_value(self, value): @@ -153,6 +155,7 @@ class CustomField(models.Model): return serialized_value +@python_2_unicode_compatible class CustomFieldValue(models.Model): field = models.ForeignKey('CustomField', related_name='values') obj_type = models.ForeignKey(ContentType, related_name='+', on_delete=models.PROTECT) @@ -164,7 +167,7 @@ class CustomFieldValue(models.Model): ordering = ['obj_type', 'obj_id'] unique_together = ['field', 'obj_type', 'obj_id'] - def __unicode__(self): + def __str__(self): return u'{} {}'.format(self.obj, self.field) @property @@ -183,6 +186,7 @@ class CustomFieldValue(models.Model): super(CustomFieldValue, self).save(*args, **kwargs) +@python_2_unicode_compatible class CustomFieldChoice(models.Model): field = models.ForeignKey('CustomField', related_name='choices', limit_choices_to={'type': CF_TYPE_SELECT}, on_delete=models.CASCADE) @@ -193,7 +197,7 @@ class CustomFieldChoice(models.Model): ordering = ['field', 'weight', 'value'] unique_together = ['field', 'value'] - def __unicode__(self): + def __str__(self): return self.value def clean(self): @@ -207,6 +211,7 @@ class CustomFieldChoice(models.Model): CustomFieldValue.objects.filter(field__type=CF_TYPE_SELECT, serialized_value=str(pk)).delete() +@python_2_unicode_compatible class Graph(models.Model): type = models.PositiveSmallIntegerField(choices=GRAPH_TYPE_CHOICES) weight = models.PositiveSmallIntegerField(default=1000) @@ -217,7 +222,7 @@ class Graph(models.Model): class Meta: ordering = ['type', 'weight', 'name'] - def __unicode__(self): + def __str__(self): return self.name def embed_url(self, obj): @@ -231,6 +236,7 @@ class Graph(models.Model): return template.render(Context({'obj': obj})) +@python_2_unicode_compatible class ExportTemplate(models.Model): content_type = models.ForeignKey(ContentType, limit_choices_to={'model__in': EXPORTTEMPLATE_MODELS}) name = models.CharField(max_length=100) @@ -245,7 +251,7 @@ class ExportTemplate(models.Model): ['content_type', 'name'] ] - def __unicode__(self): + def __str__(self): return u'{}: {}'.format(self.content_type, self.name) def to_response(self, context_dict, filename): @@ -264,6 +270,7 @@ class ExportTemplate(models.Model): return response +@python_2_unicode_compatible class TopologyMap(models.Model): name = models.CharField(max_length=50, unique=True) slug = models.SlugField(unique=True) @@ -278,7 +285,7 @@ class TopologyMap(models.Model): class Meta: ordering = ['name'] - def __unicode__(self): + def __str__(self): return self.name @property @@ -328,6 +335,7 @@ class UserActionManager(models.Manager): self.log_bulk_action(user, content_type, ACTION_BULK_DELETE, message) +@python_2_unicode_compatible class UserAction(models.Model): """ A record of an action (add, edit, or delete) performed on an object by a User. @@ -344,7 +352,7 @@ class UserAction(models.Model): class Meta: ordering = ['-time'] - def __unicode__(self): + def __str__(self): if self.message: return u'{} {}'.format(self.user, self.message) return u'{} {} {}'.format(self.user, self.get_action_display(), self.content_type) diff --git a/netbox/generate_secret_key.py b/netbox/generate_secret_key.py index d935910f6..0e0214dc4 100755 --- a/netbox/generate_secret_key.py +++ b/netbox/generate_secret_key.py @@ -1,8 +1,8 @@ -#!/usr/bin/python +#!/usr/bin/env python # This script will generate a random 50-character string suitable for use as a SECRET_KEY. import os import random charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*(-_=+)' random.seed = (os.urandom(2048)) -print ''.join(random.choice(charset) for c in range(50)) +print(''.join(random.choice(charset) for c in range(50))) diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index b0500d3d3..d37fdec25 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -7,6 +7,7 @@ from django.core.urlresolvers import reverse from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from django.db.models.expressions import RawSQL +from django.utils.encoding import python_2_unicode_compatible from dcim.models import Interface from extras.models import CustomFieldModel, CustomFieldValue @@ -72,6 +73,7 @@ IP_PROTOCOL_CHOICES = ( ) +@python_2_unicode_compatible class VRF(CreatedUpdatedModel, CustomFieldModel): """ A virtual routing and forwarding (VRF) table represents a discrete layer three forwarding domain (e.g. a routing @@ -91,7 +93,7 @@ class VRF(CreatedUpdatedModel, CustomFieldModel): verbose_name = 'VRF' verbose_name_plural = 'VRFs' - def __unicode__(self): + def __str__(self): return self.name def get_absolute_url(self): @@ -107,6 +109,7 @@ class VRF(CreatedUpdatedModel, CustomFieldModel): ]) +@python_2_unicode_compatible class RIR(models.Model): """ A Regional Internet Registry (RIR) is responsible for the allocation of a large portion of the global IP address @@ -122,13 +125,14 @@ class RIR(models.Model): verbose_name = 'RIR' verbose_name_plural = 'RIRs' - def __unicode__(self): + def __str__(self): return self.name def get_absolute_url(self): return "{}?rir={}".format(reverse('ipam:aggregate_list'), self.slug) +@python_2_unicode_compatible class Aggregate(CreatedUpdatedModel, CustomFieldModel): """ An aggregate exists at the root level of the IP address space hierarchy in NetBox. Aggregates are used to organize @@ -144,7 +148,7 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel): class Meta: ordering = ['family', 'prefix'] - def __unicode__(self): + def __str__(self): return str(self.prefix) def get_absolute_url(self): @@ -206,6 +210,7 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel): return int(children_size / self.prefix.size * 100) +@python_2_unicode_compatible class Role(models.Model): """ A Role represents the functional role of a Prefix or VLAN; for example, "Customer," "Infrastructure," or @@ -218,7 +223,7 @@ class Role(models.Model): class Meta: ordering = ['weight', 'name'] - def __unicode__(self): + def __str__(self): return self.name @property @@ -265,6 +270,7 @@ class PrefixQuerySet(NullsFirstQuerySet): return filter(lambda p: p.depth <= limit, queryset) +@python_2_unicode_compatible class Prefix(CreatedUpdatedModel, CustomFieldModel): """ A Prefix represents an IPv4 or IPv6 network, including mask length. Prefixes can optionally be assigned to Sites and @@ -294,7 +300,7 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel): ordering = ['vrf', 'family', 'prefix'] verbose_name_plural = 'prefixes' - def __unicode__(self): + def __str__(self): return str(self.prefix) def get_absolute_url(self): @@ -379,6 +385,7 @@ class IPAddressManager(models.Manager): return qs.annotate(host=RawSQL('INET(HOST(ipam_ipaddress.address))', [])).order_by('family', 'host') +@python_2_unicode_compatible class IPAddress(CreatedUpdatedModel, CustomFieldModel): """ An IPAddress represents an individual IPv4 or IPv6 address and its mask. The mask length should match what is @@ -411,7 +418,7 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel): verbose_name = 'IP address' verbose_name_plural = 'IP addresses' - def __unicode__(self): + def __str__(self): return str(self.address) def get_absolute_url(self): @@ -471,6 +478,7 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel): return STATUS_CHOICE_CLASSES[self.status] +@python_2_unicode_compatible class VLANGroup(models.Model): """ A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique. @@ -488,13 +496,14 @@ class VLANGroup(models.Model): verbose_name = 'VLAN group' verbose_name_plural = 'VLAN groups' - def __unicode__(self): + def __str__(self): return u'{} - {}'.format(self.site.name, self.name) def get_absolute_url(self): return "{}?group_id={}".format(reverse('ipam:vlan_list'), self.pk) +@python_2_unicode_compatible class VLAN(CreatedUpdatedModel, CustomFieldModel): """ A VLAN is a distinct layer two forwarding domain identified by a 12-bit integer (1-4094). Each VLAN must be assigned @@ -526,7 +535,7 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel): verbose_name = 'VLAN' verbose_name_plural = 'VLANs' - def __unicode__(self): + def __str__(self): return self.display_name def get_absolute_url(self): @@ -560,6 +569,7 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel): return STATUS_CHOICE_CLASSES[self.status] +@python_2_unicode_compatible class Service(CreatedUpdatedModel): """ A Service represents a layer-four service (e.g. HTTP or SSH) running on a Device. A Service may optionally be tied @@ -578,5 +588,5 @@ class Service(CreatedUpdatedModel): ordering = ['device', 'protocol', 'port'] unique_together = ['device', 'protocol', 'port'] - def __unicode__(self): + def __str__(self): return u'{} ({}/{})'.format(self.name, self.port, self.get_protocol_display()) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index e379372d5..c2c893e58 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -6,7 +6,7 @@ from django.contrib.messages import constants as messages from django.core.exceptions import ImproperlyConfigured try: - import configuration + from netbox import configuration except ImportError: raise ImproperlyConfigured("Configuration file is not present. Please define netbox/netbox/configuration.py per " "the documentation.") @@ -50,7 +50,7 @@ CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS # Attempt to import LDAP configuration if it has been defined LDAP_IGNORE_CERT_ERRORS = False try: - from ldap_config import * + from netbox.ldap_config import * LDAP_CONFIGURED = True except ImportError: LDAP_CONFIGURED = False diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py index 9d01f1773..bbfdee58d 100644 --- a/netbox/netbox/urls.py +++ b/netbox/netbox/urls.py @@ -2,7 +2,7 @@ from django.conf import settings from django.conf.urls import include, url from django.contrib import admin -from views import home, handle_500, trigger_500 +from netbox.views import home, handle_500, trigger_500 from users.views import login, logout diff --git a/netbox/secrets/models.py b/netbox/secrets/models.py index 930d6e032..a0c3e6f8b 100644 --- a/netbox/secrets/models.py +++ b/netbox/secrets/models.py @@ -8,7 +8,7 @@ from django.contrib.auth.models import Group, User from django.core.exceptions import ValidationError from django.core.urlresolvers import reverse from django.db import models -from django.utils.encoding import force_bytes +from django.utils.encoding import force_bytes, python_2_unicode_compatible from dcim.models import Device from utilities.models import CreatedUpdatedModel @@ -51,6 +51,7 @@ class UserKeyQuerySet(models.QuerySet): raise Exception("Bulk deletion has been disabled.") +@python_2_unicode_compatible class UserKey(CreatedUpdatedModel): """ A UserKey stores a user's personal RSA (public) encryption key, which is used to generate their unique encrypted @@ -76,7 +77,7 @@ class UserKey(CreatedUpdatedModel): self.__initial_public_key = self.public_key self.__initial_master_key_cipher = self.master_key_cipher - def __unicode__(self): + def __str__(self): return self.user.username def clean(self, *args, **kwargs): @@ -170,6 +171,7 @@ class UserKey(CreatedUpdatedModel): self.save() +@python_2_unicode_compatible class SecretRole(models.Model): """ A SecretRole represents an arbitrary functional classification of Secrets. For example, a user might define roles @@ -186,7 +188,7 @@ class SecretRole(models.Model): class Meta: ordering = ['name'] - def __unicode__(self): + def __str__(self): return self.name def get_absolute_url(self): @@ -201,6 +203,7 @@ class SecretRole(models.Model): return user in self.users.all() or user.groups.filter(pk__in=self.groups.all()).exists() +@python_2_unicode_compatible class Secret(CreatedUpdatedModel): """ A Secret stores an AES256-encrypted copy of sensitive data, such as passwords or secret keys. An irreversible @@ -227,7 +230,7 @@ class Secret(CreatedUpdatedModel): self.plaintext = kwargs.pop('plaintext', None) super(Secret, self).__init__(*args, **kwargs) - def __unicode__(self): + def __str__(self): if self.role and self.device: return u'{} for {}'.format(self.role, self.device) return u'Secret' diff --git a/netbox/secrets/tests/__init__.py b/netbox/secrets/tests/__init__.py index b04a14228..e69de29bb 100644 --- a/netbox/secrets/tests/__init__.py +++ b/netbox/secrets/tests/__init__.py @@ -1 +0,0 @@ -from test_models import * diff --git a/netbox/tenancy/models.py b/netbox/tenancy/models.py index 4317af196..df871fc31 100644 --- a/netbox/tenancy/models.py +++ b/netbox/tenancy/models.py @@ -1,12 +1,14 @@ from django.contrib.contenttypes.fields import GenericRelation from django.core.urlresolvers import reverse from django.db import models +from django.utils.encoding import python_2_unicode_compatible from extras.models import CustomFieldModel, CustomFieldValue from utilities.models import CreatedUpdatedModel from utilities.utils import csv_format +@python_2_unicode_compatible class TenantGroup(models.Model): """ An arbitrary collection of Tenants. @@ -17,13 +19,14 @@ class TenantGroup(models.Model): class Meta: ordering = ['name'] - def __unicode__(self): + def __str__(self): return self.name def get_absolute_url(self): return "{}?group={}".format(reverse('tenancy:tenant_list'), self.slug) +@python_2_unicode_compatible class Tenant(CreatedUpdatedModel, CustomFieldModel): """ A Tenant represents an organization served by the NetBox owner. This is typically a customer or an internal @@ -39,7 +42,7 @@ class Tenant(CreatedUpdatedModel, CustomFieldModel): class Meta: ordering = ['group', 'name'] - def __unicode__(self): + def __str__(self): return self.name def get_absolute_url(self): diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index 781c430f3..060e81596 100644 --- a/netbox/tenancy/views.py +++ b/netbox/tenancy/views.py @@ -10,7 +10,7 @@ from utilities.views import ( BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView, ) -from models import Tenant, TenantGroup +from .models import Tenant, TenantGroup from . import filters, forms, tables