def restore(self, request, pk): # Get the ConfigRevision being restored candidate_config = get_object_or_404(ConfigRevision, pk=pk) if request.method == 'POST': candidate_config.activate() self.message_user(request, f"Restored configuration revision #{pk}") return redirect(reverse('admin:extras_configrevision_changelist')) # Get the current ConfigRevision config_version = get_config().version current_config = ConfigRevision.objects.filter( pk=config_version).first() params = [] for param in PARAMS: params.append( (param.name, current_config.data.get(param.name, None), candidate_config.data.get(param.name, None))) context = self.admin_site.each_context(request) context.update({ 'object': candidate_config, 'params': params, }) return TemplateResponse(request, 'admin/extras/configrevision/restore.html', context)
def post(self, request): logger = logging.getLogger('netbox.auth.login') form = LoginForm(request, data=request.POST) if form.is_valid(): logger.debug("Login form validation was successful") # If maintenance mode is enabled, assume the database is read-only, and disable updating the user's # last_login time upon authentication. if get_config().MAINTENANCE_MODE: logger.warning("Maintenance mode enabled: disabling update of most recent login time") user_logged_in.disconnect(update_last_login, dispatch_uid='update_last_login') # Authenticate user auth_login(request, form.get_user()) logger.info(f"User {request.user} successfully authenticated") messages.info(request, "Logged in as {}.".format(request.user)) return self.redirect_to_next(request, logger) else: logger.debug("Login form validation failed") return render(request, self.template_name, { 'form': form, 'auth_backends': load_backends(settings.AUTHENTICATION_BACKENDS), })
def get(self, request, pk): parent = self.get_parent(request, pk) config = get_config() PAGINATE_COUNT = config.PAGINATE_COUNT MAX_PAGE_SIZE = config.MAX_PAGE_SIZE try: limit = int(request.query_params.get('limit', PAGINATE_COUNT)) except ValueError: limit = PAGINATE_COUNT if MAX_PAGE_SIZE: limit = min(limit, MAX_PAGE_SIZE) # Calculate available IPs within the parent ip_list = [] for index, ip in enumerate(parent.get_available_ips(), start=1): ip_list.append(ip) if index == limit: break serializer = serializers.AvailableIPSerializer(ip_list, many=True, context={ 'request': request, 'parent': parent, 'vrf': parent.vrf, }) return Response(serializer.data)
def dispatch(self, request, *args, **kwargs): config = get_config() # Enforce GRAPHQL_ENABLED if not config.GRAPHQL_ENABLED: return HttpResponseNotFound("The GraphQL API is not enabled.") # Attempt to authenticate the user using a DRF token, if provided if not request.user.is_authenticated: authenticator = TokenAuthentication() try: auth_info = authenticator.authenticate(request) if auth_info is not None: request.user = auth_info[0] # User object except AuthenticationFailed as exc: return HttpResponseForbidden(exc.detail) # Enforce LOGIN_REQUIRED if settings.LOGIN_REQUIRED and not request.user.is_authenticated: # If this is a human user, send a redirect to the login page if self.request_wants_html(request): return redirect_to_login(reverse('graphql')) return HttpResponseForbidden("No credentials provided.") return super().dispatch(request, *args, **kwargs)
def get_elevation_svg( self, face=DeviceFaceChoices.FACE_FRONT, user=None, unit_width=None, unit_height=None, legend_width=RACK_ELEVATION_LEGEND_WIDTH_DEFAULT, include_images=True, base_url=None ): """ Return an SVG of the rack elevation :param face: Enum of [front, rear] representing the desired side of the rack elevation to render :param user: User instance to be used for evaluating device view permissions. If None, all devices will be included. :param unit_width: Width in pixels for the rendered drawing :param unit_height: Height of each rack unit for the rendered drawing. Note this is not the total height of the elevation :param legend_width: Width of the unit legend, in pixels :param include_images: Embed front/rear device images where available :param base_url: Base URL for links and images. If none, URLs will be relative. """ elevation = RackElevationSVG(self, user=user, include_images=include_images, base_url=base_url) if unit_width is None or unit_height is None: config = get_config() unit_width = unit_width or config.RACK_ELEVATION_DEFAULT_UNIT_WIDTH unit_height = unit_height or config.RACK_ELEVATION_DEFAULT_UNIT_HEIGHT return elevation.render(face, unit_width, unit_height, legend_width)
def get_paginate_count(request): """ Determine the desired length of a page, using the following in order: 1. per_page URL query parameter 2. Saved user preference 3. PAGINATE_COUNT global setting. Return the lesser of the calculated value and MAX_PAGE_SIZE. """ config = get_config() def _max_allowed(page_size): if config.MAX_PAGE_SIZE: return min(page_size, config.MAX_PAGE_SIZE) return page_size if 'per_page' in request.GET: try: per_page = int(request.GET.get('per_page')) if request.user.is_authenticated: request.user.config.set('pagination.per_page', per_page, commit=True) return _max_allowed(per_page) except ValueError: pass if request.user.is_authenticated: per_page = request.user.config.get('pagination.per_page', config.PAGINATE_COUNT) return _max_allowed(per_page) return _max_allowed(config.PAGINATE_COUNT)
def render_markdown(value): """ Render text as Markdown """ schemes = '|'.join(get_config().ALLOWED_URL_SCHEMES) # Strip HTML tags value = strip_tags(value) # Sanitize Markdown links pattern = fr'\[([^\]]+)\]\((?!({schemes})).*:(.+)\)' value = re.sub(pattern, '[\\1](\\3)', value, flags=re.IGNORECASE) # Sanitize Markdown reference links pattern = fr'\[(.+)\]:\s*(?!({schemes}))\w*:(.+)' value = re.sub(pattern, '[\\1]: \\3', value, flags=re.IGNORECASE) # Render Markdown html = markdown( value, extensions=['fenced_code', 'tables', StrikethroughExtension()]) # If the string is not empty wrap it in rendered-markdown to style tables if html: html = f'<div class="rendered-markdown">{html}</div>' return mark_safe(html)
def __init__(self, object_list, per_page, orphans=None, **kwargs): # Determine the page size try: per_page = int(per_page) if per_page < 1: per_page = get_config().PAGINATE_COUNT except ValueError: per_page = get_config().PAGINATE_COUNT # Set orphans count based on page size if orphans is None and per_page <= 50: orphans = 5 elif orphans is None: orphans = 10 super().__init__(object_list, per_page, orphans=orphans, **kwargs)
def test_config_init_empty(self): cache.clear() config = get_config() self.assertEqual(config.config, {}) self.assertEqual(config.version, None) clear_config()
def primary_ip(self): if get_config().PREFER_IPV4 and self.primary_ip4: return self.primary_ip4 elif self.primary_ip6: return self.primary_ip6 elif self.primary_ip4: return self.primary_ip4 else: return None
def test_default_page_size(self): response = self.client.get(self.url, format='json', **self.header) page_size = get_config().PAGINATE_COUNT self.assertLess(page_size, 100, "Default page size not sufficient for data set") self.assertHttpStatus(response, status.HTTP_200_OK) self.assertEqual(response.data['count'], 100) self.assertTrue(response.data['next'].endswith(f'?limit={page_size}&offset={page_size}')) self.assertIsNone(response.data['previous']) self.assertEqual(len(response.data['results']), page_size)
def settings_and_registry(request): """ Expose Django settings and NetBox registry stores in the template context. Example: {{ settings.DEBUG }} """ return { 'settings': django_settings, 'config': get_config(), 'registry': registry, 'preferences': request.user.config if request.user.is_authenticated else {}, }
def test_config_init_from_db(self): CONFIG_DATA = {'BANNER_TOP': 'A'} cache.clear() # Create a config but don't load it into the cache configrevision = ConfigRevision.objects.create(data=CONFIG_DATA) config = get_config() self.assertEqual(config.config, CONFIG_DATA) self.assertEqual(config.version, configrevision.pk) clear_config()
def test_settings_override(self): CONFIG_DATA = {'BANNER_TOP': 'A'} cache.clear() # Create a config and load it into the cache configrevision = ConfigRevision.objects.create(data=CONFIG_DATA) configrevision.activate() config = get_config() self.assertEqual(config.BANNER_TOP, 'Z') self.assertEqual(config.version, configrevision.pk) clear_config()
def clean(self): super().clean() if self.address: # /0 masks are not acceptable if self.address.prefixlen == 0: raise ValidationError( {'address': "Cannot create IP address with /0 mask."}) # Enforce unique IP space (if applicable) if (self.vrf is None and get_config().ENFORCE_GLOBAL_UNIQUE) or ( self.vrf and self.vrf.enforce_unique): duplicate_ips = self.get_duplicates() if duplicate_ips and ( self.role not in IPADDRESS_ROLES_NONUNIQUE or any(dip.role not in IPADDRESS_ROLES_NONUNIQUE for dip in duplicate_ips)): raise ValidationError({ 'address': "Duplicate IP address found in {}: {}".format( "VRF {}".format(self.vrf) if self.vrf else "global table", duplicate_ips.first(), ) }) # Check for primary IP assignment that doesn't match the assigned device/VM if self.pk: for cls, attr in ((Device, 'device'), (VirtualMachine, 'virtual_machine')): parent = cls.objects.filter( Q(primary_ip4=self) | Q(primary_ip6=self)).first() if parent and getattr(self.assigned_object, attr, None) != parent: # Check for a NAT relationship if not self.nat_inside or getattr( self.nat_inside.assigned_object, attr, None) != parent: raise ValidationError({ 'interface': f"IP address is primary for {cls._meta.model_name} {parent} but " f"not assigned to it!" }) # Validate IP status selection if self.status == IPAddressStatusChoices.STATUS_SLAAC and self.family != 6: raise ValidationError( {'status': "Only IPv6 addresses can be assigned SLAAC status"})
def get_limit(self, request): if self.limit_query_param: try: limit = int(request.query_params[self.limit_query_param]) if limit < 0: raise ValueError() # Enforce maximum page size, if defined MAX_PAGE_SIZE = get_config().MAX_PAGE_SIZE if MAX_PAGE_SIZE: return MAX_PAGE_SIZE if limit == 0 else min(limit, MAX_PAGE_SIZE) return limit except (KeyError, ValueError): pass return self.default_limit
def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Append current parameter values to form field help texts and check for static configurations config = get_config() for param in PARAMS: value = getattr(config, param.name) is_static = hasattr(settings, param.name) if value: help_text = self.fields[param.name].help_text if help_text: help_text += '<br />' # Line break help_text += f'Current value: <strong>{value}</strong>' if is_static: help_text += ' (defined statically)' elif value == param.default: help_text += ' (default)' self.fields[param.name].help_text = help_text if is_static: self.fields[param.name].disabled = True
def clean(self): super().clean() if self.prefix: # /0 masks are not acceptable if self.prefix.prefixlen == 0: raise ValidationError( {'prefix': "Cannot create prefix with /0 mask."}) # Enforce unique IP space (if applicable) if (self.vrf is None and get_config().ENFORCE_GLOBAL_UNIQUE) or ( self.vrf and self.vrf.enforce_unique): duplicate_prefixes = self.get_duplicates() if duplicate_prefixes: raise ValidationError({ 'prefix': "Duplicate prefix found in {}: {}".format( "VRF {}".format(self.vrf) if self.vrf else "global table", duplicate_prefixes.first(), ) })
def napalm(self, request, pk): """ Execute a NAPALM method on a Device """ device = get_object_or_404(self.queryset, pk=pk) if not device.primary_ip: raise ServiceUnavailable( "This device does not have a primary IP address configured.") if device.platform is None: raise ServiceUnavailable( "No platform is configured for this device.") if not device.platform.napalm_driver: raise ServiceUnavailable( f"No NAPALM driver is configured for this device's platform: {device.platform}." ) # Check for primary IP address from NetBox object if device.primary_ip: host = str(device.primary_ip.address.ip) else: # Raise exception for no IP address and no Name if device.name does not exist if not device.name: raise ServiceUnavailable( "This device does not have a primary IP address or device name to lookup configured." ) try: # Attempt to complete a DNS name resolution if no primary_ip is set host = socket.gethostbyname(device.name) except socket.gaierror: # Name lookup failure raise ServiceUnavailable( f"Name lookup failure, unable to resolve IP address for {device.name}. Please set Primary IP or " f"setup name resolution.") # Check that NAPALM is installed try: import napalm from napalm.base.exceptions import ModuleImportError except ModuleNotFoundError as e: if getattr(e, 'name') == 'napalm': raise ServiceUnavailable( "NAPALM is not installed. Please see the documentation for instructions." ) raise e # Validate the configured driver try: driver = napalm.get_network_driver(device.platform.napalm_driver) except ModuleImportError: raise ServiceUnavailable( "NAPALM driver for platform {} not found: {}.".format( device.platform, device.platform.napalm_driver)) # Verify user permission if not request.user.has_perm('dcim.napalm_read_device'): return HttpResponseForbidden() napalm_methods = request.GET.getlist('method') response = OrderedDict([(m, None) for m in napalm_methods]) config = get_config() username = config.NAPALM_USERNAME password = config.NAPALM_PASSWORD timeout = config.NAPALM_TIMEOUT optional_args = config.NAPALM_ARGS.copy() if device.platform.napalm_args is not None: optional_args.update(device.platform.napalm_args) # Update NAPALM parameters according to the request headers for header in request.headers: if header[:9].lower() != 'x-napalm-': continue key = header[9:] if key.lower() == 'username': username = request.headers[header] elif key.lower() == 'password': password = request.headers[header] elif key: optional_args[key.lower()] = request.headers[header] # Connect to the device d = driver(hostname=host, username=username, password=password, timeout=timeout, optional_args=optional_args) try: d.open() except Exception as e: raise ServiceUnavailable( "Error connecting to the device at {}: {}".format(host, e)) # Validate and execute each specified NAPALM method for method in napalm_methods: if not hasattr(driver, method): response[method] = {'error': 'Unknown NAPALM method'} continue if not method.startswith('get_'): response[method] = { 'error': 'Only get_* NAPALM methods are supported' } continue try: response[method] = getattr(d, method)() except NotImplementedError: response[method] = { 'error': 'Method {} not implemented for NAPALM driver {}'.format( method, driver) } except Exception as e: response[method] = { 'error': 'Method {} failed: {}'.format(method, e) } d.close() return Response(response)
def __call__(self, value): if self.schemes is None: # We can't load the allowed schemes until the configuration has been initialized self.schemes = get_config().ALLOWED_URL_SCHEMES return super().__call__(value)
def __init__(self): self.default_limit = get_config().PAGINATE_COUNT