1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Merge branch 'develop' into develop-2.4

This commit is contained in:
Jeremy Stretch
2018-07-02 17:01:14 -04:00
23 changed files with 112 additions and 44 deletions

View File

@ -21,6 +21,7 @@
[ ] Feature request <!-- An enhancement of existing functionality --> [ ] Feature request <!-- An enhancement of existing functionality -->
[ ] Bug report <!-- Unexpected or erroneous behavior --> [ ] Bug report <!-- Unexpected or erroneous behavior -->
[ ] Documentation <!-- A modification to the documentation --> [ ] Documentation <!-- A modification to the documentation -->
[ ] Housekeeping <!-- Changes pertaining to the codebase itself -->
<!-- <!--
Please describe the environment in which you are running NetBox. (Be sure Please describe the environment in which you are running NetBox. (Be sure
@ -31,7 +32,7 @@
--> -->
### Environment ### Environment
* Python version: <!-- Example: 3.5.4 --> * Python version: <!-- Example: 3.5.4 -->
* NetBox version: <!-- Example: 2.1.3 --> * NetBox version: <!-- Example: 2.3.5 -->
<!-- <!--
BUG REPORTS must include: BUG REPORTS must include:

View File

@ -9,7 +9,7 @@ python:
- "3.5" - "3.5"
install: install:
- pip install -r requirements.txt - pip install -r requirements.txt
- pip install pep8 - pip install pycodestyle
before_script: before_script:
- psql --version - psql --version
- psql -U postgres -c 'SELECT version();' - psql -U postgres -c 'SELECT version();'

View File

@ -19,6 +19,7 @@ from . import serializers
class CircuitsFieldChoicesViewSet(FieldChoicesViewSet): class CircuitsFieldChoicesViewSet(FieldChoicesViewSet):
fields = ( fields = (
(Circuit, ['status']),
(CircuitTermination, ['term_side']), (CircuitTermination, ['term_side']),
) )

View File

@ -36,11 +36,12 @@ class DCIMFieldChoicesViewSet(FieldChoicesViewSet):
fields = ( fields = (
(Device, ['face', 'status']), (Device, ['face', 'status']),
(ConsolePort, ['connection_status']), (ConsolePort, ['connection_status']),
(Interface, ['form_factor']), (Interface, ['form_factor', 'mode']),
(InterfaceConnection, ['connection_status']), (InterfaceConnection, ['connection_status']),
(InterfaceTemplate, ['form_factor']), (InterfaceTemplate, ['form_factor']),
(PowerPort, ['connection_status']), (PowerPort, ['connection_status']),
(Rack, ['type', 'width']), (Rack, ['type', 'width']),
(Site, ['status']),
) )

View File

@ -35,7 +35,7 @@ from .models import (
RackRole, Region, Site, VirtualChassis RackRole, Region, Site, VirtualChassis
) )
DEVICE_BY_PK_RE = '{\d+\}' DEVICE_BY_PK_RE = r'{\d+\}'
INTERFACE_MODE_HELP_TEXT = """ INTERFACE_MODE_HELP_TEXT = """
Access: One untagged VLAN<br /> Access: One untagged VLAN<br />

View File

@ -1352,6 +1352,12 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
'face': "Must specify rack face when defining rack position.", 'face': "Must specify rack face when defining rack position.",
}) })
# Prevent 0U devices from being assigned to a specific position
if self.position and self.device_type.u_height == 0:
raise ValidationError({
'position': "A U0 device type ({}) cannot be assigned to a rack position.".format(self.device_type)
})
if self.rack: if self.rack:
try: try:
@ -1612,8 +1618,8 @@ class ConsoleServerPortManager(models.Manager):
def get_queryset(self): def get_queryset(self):
# Pad any trailing digits to effect natural sorting # Pad any trailing digits to effect natural sorting
return super(ConsoleServerPortManager, self).get_queryset().extra(select={ return super(ConsoleServerPortManager, self).get_queryset().extra(select={
'name_padded': "CONCAT(REGEXP_REPLACE(dcim_consoleserverport.name, '\d+$', ''), " 'name_padded': r"CONCAT(REGEXP_REPLACE(dcim_consoleserverport.name, '\d+$', ''), "
"LPAD(SUBSTRING(dcim_consoleserverport.name FROM '\d+$'), 8, '0'))", r"LPAD(SUBSTRING(dcim_consoleserverport.name FROM '\d+$'), 8, '0'))",
}).order_by('device', 'name_padded') }).order_by('device', 'name_padded')
@ -1720,8 +1726,8 @@ class PowerOutletManager(models.Manager):
def get_queryset(self): def get_queryset(self):
# Pad any trailing digits to effect natural sorting # Pad any trailing digits to effect natural sorting
return super(PowerOutletManager, self).get_queryset().extra(select={ return super(PowerOutletManager, self).get_queryset().extra(select={
'name_padded': "CONCAT(REGEXP_REPLACE(dcim_poweroutlet.name, '\d+$', ''), " 'name_padded': r"CONCAT(REGEXP_REPLACE(dcim_poweroutlet.name, '\d+$', ''), "
"LPAD(SUBSTRING(dcim_poweroutlet.name FROM '\d+$'), 8, '0'))", r"LPAD(SUBSTRING(dcim_poweroutlet.name FROM '\d+$'), 8, '0'))",
}).order_by('device', 'name_padded') }).order_by('device', 'name_padded')

View File

@ -7,9 +7,9 @@ from tenancy.tables import COL_TENANT
from utilities.tables import BaseTable, BooleanColumn, ToggleColumn from utilities.tables import BaseTable, BooleanColumn, ToggleColumn
from .models import ( from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, InventoryItem, Manufacturer, Platform, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, InventoryItem,
PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, Region, Site, Manufacturer, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
VirtualChassis, RackReservation, Region, Site, VirtualChassis,
) )
REGION_LINK = """ REGION_LINK = """
@ -621,7 +621,7 @@ class InterfaceConnectionTable(BaseTable):
interface_b = tables.Column(verbose_name='Interface B') interface_b = tables.Column(verbose_name='Interface B')
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Interface model = InterfaceConnection
fields = ('device_a', 'interface_a', 'device_b', 'interface_b') fields = ('device_a', 'interface_a', 'device_b', 'interface_b')

View File

@ -102,7 +102,7 @@ class TopologyMapViewSet(ModelViewSet):
try: try:
data = tmap.render(img_format=img_format) data = tmap.render(img_format=img_format)
except: except Exception:
return HttpResponse( return HttpResponse(
"There was an error generating the requested graph. Ensure that the GraphViz executables have been " "There was an error generating the requested graph. Ensure that the GraphViz executables have been "
"installed correctly." "installed correctly."

View File

@ -5,6 +5,7 @@ from collections import OrderedDict
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.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from mptt.forms import TreeNodeMultipleChoiceField from mptt.forms import TreeNodeMultipleChoiceField
from taggit.models import Tag from taggit.models import Tag
@ -64,7 +65,14 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
choices = [(cfc.pk, cfc) for cfc in cf.choices.all()] choices = [(cfc.pk, cfc) for cfc in cf.choices.all()]
if not cf.required or bulk_edit or filterable_only: if not cf.required or bulk_edit or filterable_only:
choices = [(None, '---------')] + choices choices = [(None, '---------')] + choices
field = forms.TypedChoiceField(choices=choices, coerce=int, required=cf.required) # Check for a default choice
default_choice = None
if initial:
try:
default_choice = cf.choices.get(value=initial).pk
except ObjectDoesNotExist:
pass
field = forms.TypedChoiceField(choices=choices, coerce=int, required=cf.required, initial=default_choice)
# URL # URL
elif cf.type == CF_TYPE_URL: elif cf.type == CF_TYPE_URL:

View File

@ -16,7 +16,7 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='customfield', model_name='customfield',
name='default', name='default',
field=models.CharField(blank=True, help_text='Default value for the field. Use "true" or "false" for booleans. N/A for selection fields.', max_length=100), field=models.CharField(blank=True, help_text='Default value for the field. Use "true" or "false" for booleans.', max_length=100),
), ),
migrations.AlterField( migrations.AlterField(
model_name='customfield', model_name='customfield',

View File

@ -19,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 = re.match('^PostgreSQL (\d+\.\d+(\.\d+)?)', row[0]).group(1) pg_version = re.match(r'^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

@ -172,8 +172,7 @@ class CustomField(models.Model):
default = models.CharField( default = models.CharField(
max_length=100, max_length=100,
blank=True, blank=True,
help_text='Default value for the field. Use "true" or "false" for ' help_text='Default value for the field. Use "true" or "false" for booleans.'
'booleans. N/A for selection fields.'
) )
weight = models.PositiveSmallIntegerField( weight = models.PositiveSmallIntegerField(
default=100, default=100,

View File

@ -163,8 +163,8 @@ class IOSSSH(SSHClient):
sh_ver = self._send('show version').split('\r\n') sh_ver = self._send('show version').split('\r\n')
return { return {
'serial': parse(sh_ver, 'Processor board ID ([^\s]+)'), 'serial': parse(sh_ver, r'Processor board ID ([^\s]+)'),
'description': parse(sh_ver, 'cisco ([^\s]+)') 'description': parse(sh_ver, r'cisco ([^\s]+)')
} }
def items(chassis_serial=None): def items(chassis_serial=None):
@ -172,9 +172,9 @@ class IOSSSH(SSHClient):
for i in cmd: for i in cmd:
i_fmt = i.replace('\r\n', ' ') i_fmt = i.replace('\r\n', ' ')
try: try:
m_name = re.search('NAME: "([^"]+)"', i_fmt).group(1) m_name = re.search(r'NAME: "([^"]+)"', i_fmt).group(1)
m_pid = re.search('PID: ([^\s]+)', i_fmt).group(1) m_pid = re.search(r'PID: ([^\s]+)', i_fmt).group(1)
m_serial = re.search('SN: ([^\s]+)', i_fmt).group(1) m_serial = re.search(r'SN: ([^\s]+)', i_fmt).group(1)
# Omit built-in items and those with no PID # Omit built-in items and those with no PID
if m_serial != chassis_serial and m_pid.lower() != 'unspecified': if m_serial != chassis_serial and m_pid.lower() != 'unspecified':
yield { yield {
@ -208,7 +208,7 @@ class OpengearSSH(SSHClient):
try: try:
stdin, stdout, stderr = self.ssh.exec_command("showserial") stdin, stdout, stderr = self.ssh.exec_command("showserial")
serial = stdout.readlines()[0].strip() serial = stdout.readlines()[0].strip()
except: except Exception:
raise RuntimeError("Failed to glean chassis serial from device.") raise RuntimeError("Failed to glean chassis serial from device.")
# Older models don't provide serial info # Older models don't provide serial info
if serial == "No serial number information available": if serial == "No serial number information available":
@ -217,7 +217,7 @@ class OpengearSSH(SSHClient):
try: try:
stdin, stdout, stderr = self.ssh.exec_command("config -g config.system.model") stdin, stdout, stderr = self.ssh.exec_command("config -g config.system.model")
description = stdout.readlines()[0].split(' ', 1)[1].strip() description = stdout.readlines()[0].split(' ', 1)[1].strip()
except: except Exception:
raise RuntimeError("Failed to glean chassis description from device.") raise RuntimeError("Failed to glean chassis description from device.")
return { return {

View File

@ -95,7 +95,31 @@ class PrefixViewSet(CustomFieldModelViewSet):
requested_prefixes = request.data if isinstance(request.data, list) else [request.data] requested_prefixes = request.data if isinstance(request.data, list) else [request.data]
# Allocate prefixes to the requested objects based on availability within the parent # Allocate prefixes to the requested objects based on availability within the parent
for requested_prefix in requested_prefixes: for i, requested_prefix in enumerate(requested_prefixes):
# Validate requested prefix size
error_msg = None
if 'prefix_length' not in requested_prefix:
error_msg = "Item {}: prefix_length field missing".format(i)
elif not isinstance(requested_prefix['prefix_length'], int):
error_msg = "Item {}: Invalid prefix length ({})".format(
i, requested_prefix['prefix_length']
)
elif prefix.family == 4 and requested_prefix['prefix_length'] > 32:
error_msg = "Item {}: Invalid prefix length ({}) for IPv4".format(
i, requested_prefix['prefix_length']
)
elif prefix.family == 6 and requested_prefix['prefix_length'] > 128:
error_msg = "Item {}: Invalid prefix length ({}) for IPv6".format(
i, requested_prefix['prefix_length']
)
if error_msg:
return Response(
{
"detail": error_msg
},
status=status.HTTP_400_BAD_REQUEST
)
# Find the first available prefix equal to or larger than the requested size # Find the first available prefix equal to or larger than the requested size
for available_prefix in available_prefixes.iter_cidrs(): for available_prefix in available_prefixes.iter_cidrs():
@ -157,8 +181,8 @@ class PrefixViewSet(CustomFieldModelViewSet):
requested_ips = request.data if isinstance(request.data, list) else [request.data] requested_ips = request.data if isinstance(request.data, list) else [request.data]
# Determine if the requested number of IPs is available # Determine if the requested number of IPs is available
available_ips = list(prefix.get_available_ips()) available_ips = prefix.get_available_ips()
if len(available_ips) < len(requested_ips): if available_ips.size < len(requested_ips):
return Response( return Response(
{ {
"detail": "An insufficient number of IP addresses are available within the prefix {} ({} " "detail": "An insufficient number of IP addresses are available within the prefix {} ({} "
@ -168,8 +192,9 @@ class PrefixViewSet(CustomFieldModelViewSet):
) )
# Assign addresses from the list of available IPs and copy VRF assignment from the parent prefix # Assign addresses from the list of available IPs and copy VRF assignment from the parent prefix
available_ips = iter(available_ips)
for requested_ip in requested_ips: for requested_ip in requested_ips:
requested_ip['address'] = available_ips.pop(0) requested_ip['address'] = next(available_ips)
requested_ip['vrf'] = prefix.vrf.pk if prefix.vrf else None requested_ip['vrf'] = prefix.vrf.pk if prefix.vrf else None
# Initialize the serializer with a list or a single object depending on what was requested # Initialize the serializer with a list or a single object depending on what was requested

View File

@ -1,6 +1,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import django_filters import django_filters
from django.core.exceptions import ValidationError
from django.db.models import Q from django.db.models import Q
import netaddr import netaddr
from netaddr.core import AddrFormatError from netaddr.core import AddrFormatError
@ -242,6 +243,10 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
method='search_by_parent', method='search_by_parent',
label='Parent prefix', label='Parent prefix',
) )
address = django_filters.CharFilter(
method='filter_address',
label='Address',
)
mask_length = django_filters.NumberFilter( mask_length = django_filters.NumberFilter(
method='filter_mask_length', method='filter_mask_length',
label='Mask length', label='Mask length',
@ -325,6 +330,17 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
except (AddrFormatError, ValueError): except (AddrFormatError, ValueError):
return queryset.none() return queryset.none()
def filter_address(self, queryset, name, value):
if not value.strip():
return queryset
try:
# Match address and subnet mask
if '/' in value:
return queryset.filter(address=value)
return queryset.filter(address__net_host=value)
except ValidationError:
return queryset.none()
def filter_mask_length(self, queryset, name, value): def filter_mask_length(self, queryset, name, value):
if not value: if not value:
return queryset return queryset

View File

@ -294,7 +294,15 @@ SWAGGER_SETTINGS = {
'utilities.custom_inspectors.NullablePaginatorInspector', 'utilities.custom_inspectors.NullablePaginatorInspector',
'drf_yasg.inspectors.DjangoRestResponsePagination', 'drf_yasg.inspectors.DjangoRestResponsePagination',
'drf_yasg.inspectors.CoreAPICompatInspector', 'drf_yasg.inspectors.CoreAPICompatInspector',
] ],
'SECURITY_DEFINITIONS': {
'Bearer': {
'type': 'apiKey',
'name': 'Authorization',
'in': 'header',
}
},
'VALIDATOR_URL': None,
} }
@ -307,5 +315,5 @@ INTERNAL_IPS = (
try: try:
HOSTNAME = socket.gethostname() HOSTNAME = socket.gethostname()
except: except Exception:
HOSTNAME = 'localhost' HOSTNAME = 'localhost'

View File

@ -52,9 +52,9 @@ _patterns = [
url(r'^api/secrets/', include('secrets.api.urls')), url(r'^api/secrets/', include('secrets.api.urls')),
url(r'^api/tenancy/', include('tenancy.api.urls')), url(r'^api/tenancy/', include('tenancy.api.urls')),
url(r'^api/virtualization/', include('virtualization.api.urls')), url(r'^api/virtualization/', include('virtualization.api.urls')),
url(r'^api/docs/$', schema_view.with_ui('swagger', cache_timeout=None), name='api_docs'), url(r'^api/docs/$', schema_view.with_ui('swagger'), name='api_docs'),
url(r'^api/redoc/$', schema_view.with_ui('redoc', cache_timeout=None), name='api_redocs'), url(r'^api/redoc/$', schema_view.with_ui('redoc'), name='api_redocs'),
url(r'^api/swagger(?P<format>.json|.yaml)$', schema_view.without_ui(cache_timeout=None), name='schema_swagger'), url(r'^api/swagger(?P<format>.json|.yaml)$', schema_view.without_ui(), name='schema_swagger'),
# 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}),

View File

@ -27,7 +27,7 @@ def validate_rsa_key(key, is_secret=True):
raise forms.ValidationError("This looks like a private key. Please provide your public RSA key.") raise forms.ValidationError("This looks like a private key. Please provide your public RSA key.")
try: try:
PKCS1_OAEP.new(key) PKCS1_OAEP.new(key)
except: except Exception:
raise forms.ValidationError("Error validating RSA key. Please ensure that your key supports PKCS#1 OAEP.") raise forms.ValidationError("Error validating RSA key. Please ensure that your key supports PKCS#1 OAEP.")

View File

@ -105,7 +105,7 @@ class UserKey(models.Model):
raise ValidationError({ raise ValidationError({
'public_key': "Invalid RSA key format." 'public_key': "Invalid RSA key format."
}) })
except: except Exception:
raise ValidationError("Something went wrong while trying to save your key. Please ensure that you're " raise ValidationError("Something went wrong while trying to save your key. Please ensure that you're "
"uploading a valid RSA public key in PEM format (no SSH/PGP).") "uploading a valid RSA public key in PEM format (no SSH/PGP).")

View File

@ -26,7 +26,7 @@
<li class="occupied h{{ u.device.device_type.u_height }}u"{% ifequal u.device.face face_id %} style="background-color: #{{ u.device.device_role.color }}"{% endifequal %}> <li class="occupied h{{ u.device.device_type.u_height }}u"{% ifequal u.device.face face_id %} style="background-color: #{{ u.device.device_role.color }}"{% endifequal %}>
{% ifequal u.device.face face_id %} {% ifequal u.device.face face_id %}
<a href="{% url 'dcim:device' pk=u.device.pk %}" data-toggle="popover" data-trigger="hover" data-container="body" data-html="true" <a href="{% url 'dcim:device' pk=u.device.pk %}" data-toggle="popover" data-trigger="hover" data-container="body" data-html="true"
data-content="{{ u.device.device_role }}<br />{{ u.device.device_type.full_name }} ({{ u.device.device_type.u_height }}U){% if u.device.asset_tag %}<br />{{ u.device.asset_tag }}{% endif %}"> data-content="{{ u.device.device_role }}<br />{{ u.device.device_type.full_name }} ({{ u.device.device_type.u_height }}U){% if u.device.asset_tag %}<br />{{ u.device.asset_tag }}{% endif %}{% if u.device.serial %}<br />{{ u.device.serial }}{% endif %}">
{{ u.device.name|default:u.device.device_role }} {{ u.device.name|default:u.device.device_role }}
{% if u.device.devicebay_count %} {% if u.device.devicebay_count %}
({{ u.device.get_children.count }}/{{ u.device.devicebay_count }}) ({{ u.device.get_children.count }}/{{ u.device.devicebay_count }})

View File

@ -38,10 +38,10 @@ COLOR_CHOICES = (
('607d8b', 'Dark grey'), ('607d8b', 'Dark grey'),
('111111', 'Black'), ('111111', 'Black'),
) )
NUMERIC_EXPANSION_PATTERN = '\[((?:\d+[?:,-])+\d+)\]' NUMERIC_EXPANSION_PATTERN = r'\[((?:\d+[?:,-])+\d+)\]'
ALPHANUMERIC_EXPANSION_PATTERN = '\[((?:[a-zA-Z0-9]+[?:,-])+[a-zA-Z0-9]+)\]' ALPHANUMERIC_EXPANSION_PATTERN = r'\[((?:[a-zA-Z0-9]+[?:,-])+[a-zA-Z0-9]+)\]'
IP4_EXPANSION_PATTERN = '\[((?:[0-9]{1,3}[?:,-])+[0-9]{1,3})\]' IP4_EXPANSION_PATTERN = r'\[((?:[0-9]{1,3}[?:,-])+[0-9]{1,3})\]'
IP6_EXPANSION_PATTERN = '\[((?:[0-9a-f]{1,4}[?:,-])+[0-9a-f]{1,4})\]' IP6_EXPANSION_PATTERN = r'\[((?:[0-9a-f]{1,4}[?:,-])+[0-9a-f]{1,4})\]'
def parse_numeric_range(string, base=10): def parse_numeric_range(string, base=10):
@ -407,7 +407,7 @@ class FlexibleModelChoiceField(forms.ModelChoiceField):
try: try:
if not self.to_field_name: if not self.to_field_name:
key = 'pk' key = 'pk'
elif re.match('^\{\d+\}$', value): elif re.match(r'^\{\d+\}$', value):
key = 'pk' key = 'pk'
value = value.strip('{}') value = value.strip('{}')
else: else:

View File

@ -15,7 +15,7 @@ class EnhancedURLValidator(URLValidator):
A fake URL list which "contains" all scheme names abiding by the syntax defined in RFC 3986 section 3.1 A fake URL list which "contains" all scheme names abiding by the syntax defined in RFC 3986 section 3.1
""" """
def __contains__(self, item): def __contains__(self, item):
if not item or not re.match('^[a-z][0-9a-z+\-.]*$', item.lower()): if not item or not re.match(r'^[a-z][0-9a-z+\-.]*$', item.lower()):
return False return False
return True return True

View File

@ -23,8 +23,11 @@ fi
# Check all python source files for PEP 8 compliance, but explicitly # Check all python source files for PEP 8 compliance, but explicitly
# ignore: # ignore:
# - W504: line break after binary operator
# - E501: line greater than 80 characters in length # - E501: line greater than 80 characters in length
pep8 --ignore=E501 netbox/ pycodestyle \
--ignore=W504,E501 \
netbox/
RC=$? RC=$?
if [[ $RC != 0 ]]; then if [[ $RC != 0 ]]; then
echo -e "\n$(info) one or more PEP 8 errors detected, failing build." echo -e "\n$(info) one or more PEP 8 errors detected, failing build."