Перейти к содержанию

Пользовательские скрипты

Пользовательское скриптование было введено для предоставления пользователям возможности выполнять пользовательскую логику из интерфейса InfraVision. Пользовательские скрипты позволяют пользователю напрямую и удобно манипулировать данными InfraVision предписанным образом. Они могут использоваться для выполнения множества задач, таких как:

  • Автоматическое заполнение новых устройств и кабелей при подготовке к развёртыванию новой площадки
  • Создание диапазона новых зарезервированных префиксов или IP-адресов
  • Получение данных из внешнего источника и импорт их в InfraVision
  • Обновление объектов с недействительными или неполными данными

Они также могут использоваться как механизм для проверки целостности данных в InfraVision. Авторы скриптов могут определять тесты для проверки объектов на соответствие определённым правилам и условиям. Например, вы можете написать скрипт для проверки того, что:

  • Все коммутаторы верхнего уровня стойки имеют консольное подключение
  • Каждый маршрутизатор имеет loopback-интерфейс с назначенным IP-адресом
  • Каждое описание интерфейса соответствует стандартному формату
  • Каждая площадка имеет определённый минимальный набор VLAN
  • Все IP-адреса имеют родительский префикс

Пользовательские скрипты — это Python-код, который существует за пределами кодовой базы InfraVision, поэтому их можно обновлять и изменять без вмешательства в основную установку InfraVision. И поскольку они полностью пользовательские, нет никаких врождённых ограничений на то, что может делать скрипт.

Опасность

Устанавливайте только доверенные скрипты. Пользовательские скрипты имеют неограниченный доступ для изменения чего-либо в базе данных и по своей природе небезопасны, и должны устанавливаться и запускаться только из доверенных источников. Вы также должны просмотреть и установить разрешения на то, кто может запускать скрипты, если скрипт может изменять какие-либо данные.

Написание пользовательских скриптов

Все пользовательские скрипты должны наследоваться от базового класса extras.scripts.Script. Этот класс предоставляет функциональность, необходимую для генерации форм и логирования активности.

from extras.scripts import Script

class MyScript(Script):
    ...

Скрипты состоят из двух основных компонентов: набора переменных и метода run(). Переменные позволяют вашему скрипту принимать пользовательский ввод через интерфейс InfraVision, но они необязательны: если вашему скрипту не требуется никакой пользовательский ввод, нет необходимости определять какие-либо переменные.

Метод run() — это место, где находится логика выполнения вашего скрипта. (Обратите внимание, что ваш скрипт может иметь столько методов, сколько необходимо: это просто точка вызова для InfraVision.)

class MyScript(Script):
    var1 = StringVar(...)
    var2 = IntegerVar(...)
    var3 = ObjectVar(...)

    def run(self, data, commit):
        ...

Метод run() должен принимать два аргумента:

  • data - Словарь, содержащий все данные переменных, переданные через веб-форму.
  • commit - Логическое значение, указывающее, будут ли изменения базы данных зафиксированы.

Определение переменных скрипта необязательно: вы можете создать скрипт только с методом run(), если пользовательский ввод не нужен.

Любой вывод, сгенерированный скриптом во время его выполнения, будет отображён на вкладке "output" в интерфейсе.

По умолчанию скрипты внутри модуля упорядочены по алфавиту на странице списка скриптов. Чтобы возвращать скрипты в определённом порядке, вы можете определить переменную script_order в конце вашего модуля. Переменная script_order — это кортеж, содержащий каждый класс Script в желаемом порядке. Любые скрипты, опущенные в этом списке, будут перечислены последними.

from extras.scripts import Script

class MyCustomScript(Script):
    ...

class AnotherCustomScript(Script):
    ...

script_order = (MyCustomScript, AnotherCustomScript)

Атрибуты скрипта

Атрибуты скрипта определяются в классе с именем Meta внутри скрипта. Они необязательны, но рекомендуются.

Внимание

Они также определены и используются как свойства базового класса пользовательского скрипта, поэтому не используйте те же имена для переменных и не переопределяйте их в своём пользовательском скрипте.

name

Это удобочитаемое имя вашего скрипта. Если опущено, будет использоваться имя класса.

description

Удобочитаемое описание того, что делает ваш скрипт.

field_order

По умолчанию переменные скрипта будут упорядочены в форме так, как они определены в скрипте. field_order может быть определён как итерируемый объект имён полей для определения порядка, в котором переменные отображаются в группе "Script Data" по умолчанию. Любые поля, не включённые в этот итерируемый объект, будут перечислены последними. Если определён fieldsets, field_order будет игнорироваться. Группа fieldset для "Script Execution Parameters" будет добавлена в конец формы по умолчанию для пользователя.

fieldsets

fieldsets может быть определён как итерируемый объект групп полей и их имён полей для определения порядка, в котором переменные группируются и отображаются. Любые поля, не включённые в этот итерируемый объект, не будут отображаться в форме. Если определён fieldsets, field_order будет игнорироваться. Группа fieldset для "Script Execution Parameters" будет добавлена в конец fieldsets по умолчанию для пользователя.

Ниже приведён пример определения fieldset:

class MyScript(Script):
    class Meta:
        fieldsets = (
            ('First group', ('field1', 'field2', 'field3')),
            ('Second group', ('field4', 'field5')),
        )

commit_default

Флажок для фиксации изменений базы данных при выполнении скрипта установлен по умолчанию. Установите commit_default в False в классе Meta скрипта, чтобы оставить эту опцию неотмеченной по умолчанию.

commit_default = False

scheduling_enabled

По умолчанию скрипт может быть запланирован для выполнения в более позднее время. Установка scheduling_enabled в False отключает эту возможность: будет возможно только немедленное выполнение. (Это также отключает возможность установки интервала повторяющегося выполнения.)

job_timeout

Установите максимально допустимое время выполнения для скрипта. Если не установлено, будет использоваться RQ_DEFAULT_TIMEOUT.

Доступ к данным запроса

Детали текущего HTTP-запроса (того, который выполняется для запуска скрипта) доступны как атрибут экземпляра self.request. Это может использоваться для определения, например, пользователя, выполняющего скрипт, и IP-адреса клиента:

username = self.request.user.username
ip_address = self.request.META.get('HTTP_X_FORWARDED_FOR') or \
    self.request.META.get('REMOTE_ADDR')
self.log_info(f"Running as user {username} (IP: {ip_address})...")

Полный список доступных параметров запроса см. в документации Django.

Чтение данных из файлов

Класс Script предоставляет два удобных метода для чтения данных из файлов:

  • load_yaml
  • load_json

Эти два метода загружают данные в формате YAML или JSON соответственно из файлов в локальном каталоге (т.е. SCRIPTS_ROOT).

Примечание: Эти удобные методы устарели и будут удалены в InfraVision v4.4. Они работают только при запуске скриптов в локальном каталоге, они не будут работать при использовании хранилища, отличного от ScriptFileSystemStorage.

Логирование

Объект Script предоставляет набор удобных функций для записи сообщений с разными уровнями серьёзности:

  • log_debug(message=None, obj=None)
  • log_success(message=None, obj=None)
  • log_info(message=None, obj=None)
  • log_warning(message=None, obj=None)
  • log_failure(message=None, obj=None)

Сообщения журнала возвращаются пользователю при выполнении скрипта. Поддерживается рендеринг Markdown для сообщений журнала. Сообщение может быть опционально связано с конкретным объектом, передав его в качестве второго аргумента методу логирования.

Тестовые методы

Скрипт может определять один или несколько тестовых методов для отчёта об определённых условиях. Все тестовые методы должны иметь имя, начинающееся с test_, и не принимать никаких аргументов, кроме self.

Эти методы обнаруживаются и запускаются автоматически при выполнении скрипта, если его метод run() не был переопределён. (При переопределении run() можно вызвать run_tests() для запуска всех тестовых методов, присутствующих в скрипте.)

Вызов любого из этих методов логирования без сообщения увеличит соответствующий счётчик, но не сгенерирует строку вывода в журнале скрипта.

Информация

Эта функциональность была перенесена из устаревших отчётов в InfraVision v4.0.

Пример

from dcim.choices import DeviceStatusChoices
from dcim.models import ConsolePort, Device, PowerPort
from extras.scripts import Script


class DeviceConnectionsReport(Script):
    description = "Validate the minimum physical connections for each device"

    def test_console_connection(self):

        # Check that every console port for every active device has a connection defined.
        active = DeviceStatusChoices.STATUS_ACTIVE
        for console_port in ConsolePort.objects.prefetch_related('device').filter(device__status=active):
            if not console_port.connected_endpoints:
                self.log_failure(
                    f"No console connection defined for {console_port.name}",
                    console_port.device,
                )
            elif not console_port.connection_status:
                self.log_warning(
                    f"Console connection for {console_port.name} marked as planned",
                    console_port.device,
                )
            else:
                self.log_success("Passed", console_port.device)

    def test_power_connections(self):

        # Check that every active device has at least two connected power supplies.
        for device in Device.objects.filter(status=DeviceStatusChoices.STATUS_ACTIVE):
            connected_ports = 0
            for power_port in PowerPort.objects.filter(device=device):
                if power_port.connected_endpoints:
                    connected_ports += 1
                    if not power_port.path.is_active:
                        self.log_warning(
                            f"Power connection for {power_port.name} marked as planned",
                            device,
                        )
            if connected_ports < 2:
                self.log_failure(
                    f"{connected_ports} connected power supplies found (2 needed)",
                    device,
                )
            else:
                self.log_success("Passed", device)

Логирование изменений

Для генерации правильных данных журнала изменений при редактировании существующего объекта необходимо сделать снимок объекта перед внесением каких-либо изменений.

if obj.pk and hasattr(obj, 'snapshot'):
    obj.snapshot()

obj.property = "New Value"
obj.full_clean()
obj.save()

Обработка ошибок

Иногда что-то идёт не так, и скрипт сталкивается с исключением. Если это происходит и необработанное исключение вызывается пользовательским скриптом, выполнение прерывается и сообщается полная трассировка стека.

Хотя это полезно для отладки, в некоторых ситуациях может потребоваться чисто прервать выполнение пользовательского скрипта (например, из-за недействительных входных данных) и тем самым убедиться, что никакие изменения не выполняются в базе данных. В этом случае скрипт может бросить исключение AbortScript, которое предотвратит отображение трассировки стека, но всё же прервёт выполнение скрипта и сообщит заданное сообщение об ошибке.

from utilities.exceptions import AbortScript

if some_error:
    raise AbortScript("Some meaningful error message")

Справочник по переменным

Параметры по умолчанию

Все переменные пользовательских скриптов поддерживают следующие параметры по умолчанию:

  • default - Значение поля по умолчанию
  • description - Краткое удобочитаемое описание поля
  • label - Имя поля, отображаемое в отрендеренной форме
  • required - Указывает, является ли поле обязательным (все поля обязательны по умолчанию)
  • widget - Класс виджета формы для использования (см. документацию Django)

StringVar

Хранит строку символов (т.е. текст). Параметры включают:

  • min_length - Минимальное количество символов
  • max_length - Максимальное количество символов
  • regex - Регулярное выражение, которому должно соответствовать предоставленное значение

Обратите внимание, что min_length и max_length могут быть установлены на одно и то же число для создания поля фиксированной длины.

TextVar

Произвольный текст любой длины. Отображается как многострочное текстовое поле ввода.

IntegerVar

Хранит числовое целое значение. Параметры включают:

  • min_value - Минимальное значение
  • max_value - Максимальное значение

DecimalVar

Хранит числовое десятичное значение. Параметры включают:

  • min_value - Минимальное значение
  • max_value - Максимальное значение
  • max_digits - Максимальное количество цифр, включая десятичные знаки
  • decimal_places - Количество десятичных знаков

BooleanVar

Флаг истина/ложь. Это поле не имеет параметров, кроме перечисленных выше по умолчанию.

ChoiceVar

Набор вариантов, из которых пользователь может выбрать один.

  • choices - Список кортежей (value, label), представляющих доступные варианты. Например:
CHOICES = (
    ('n', 'North'),
    ('s', 'South'),
    ('e', 'East'),
    ('w', 'West')
)

direction = ChoiceVar(choices=CHOICES)

В приведённом выше примере выбор варианта с меткой "North" отправит значение n.

MultiChoiceVar

Аналогично ChoiceVar, но позволяет выбирать несколько вариантов.

ObjectVar

Конкретный объект в InfraVision. Каждый ObjectVar должен указывать конкретную модель и позволяет пользователю выбрать один из доступных экземпляров. ObjectVar принимает несколько аргументов, перечисленных ниже.

  • model - Класс модели
  • query_params - Словарь параметров запроса для использования при получении доступных опций (необязательно)
  • context - Пользовательский словарь, сопоставляющий контекстные переменные шаблона с полями, используемый при рендеринге элементов <option> в выпадающем меню (необязательно; см. ниже)
  • null_option - Метка, представляющая "нулевой" или пустой выбор (необязательно)
  • selector - Логическое значение, которое, если True, включает расширенный виджет выбора объекта для помощи пользователю в идентификации нужного объекта (необязательно; по умолчанию False)

Чтобы ограничить доступные выборы в списке, можно передать дополнительные параметры запроса как словарь query_params. Например, чтобы показывать только устройства со статусом "active":

device = ObjectVar(
    model=Device,
    query_params={
        'status': 'active'
    }
)

Несколько значений можно указать, назначив список ключу словаря. Также можно ссылаться на значение других полей в форме, добавляя знак доллара ($) к имени переменной.

region = ObjectVar(
    model=Region
)
site = ObjectVar(
    model=Site,
    query_params={
        'region_id': '$region'
    }
)

Контекстные переменные

Пользовательские контекстные переменные могут быть переданы для переопределения имён атрибутов по умолчанию или для отображения дополнительной информации, такой как родительский объект.

Имя По умолчанию Описание
value "id" Атрибут, содержащий значение опции
label "display" Атрибут, используемый как удобочитаемая метка опции
description "description" Атрибут для использования в качестве описания
depth1 "_depth" Атрибут, указывающий глубину объекта в рекурсивной иерархии
disabled -- Атрибут, который, если true, означает, что опция должна быть отключена
parent -- Атрибут, представляющий родительский объект
count1 -- Атрибут, содержащий числовое количество связанных объектов

MultiObjectVar

Аналогично ObjectVar, но позволяет выбирать несколько объектов.

FileVar

Загруженный файл. Обратите внимание, что загруженные файлы находятся в памяти только на время выполнения скрипта: они не будут автоматически сохранены для будущего использования. Скрипт несёт ответственность за запись содержимого файла на диск, где это необходимо.

IPAddressVar

IPv4 или IPv6 адрес без маски. Возвращает объект netaddr.IPAddress.

IPAddressWithMaskVar

IPv4 или IPv6 адрес с маской. Возвращает объект netaddr.IPNetwork, который включает маску.

IPNetworkVar

IPv4 или IPv6 сеть с маской. Возвращает объект netaddr.IPNetwork. Доступны два атрибута для проверки предоставленной маски:

  • min_prefix_length - Минимальная длина маски
  • max_prefix_length - Максимальная длина маски

DateVar

Календарная дата. Возвращает объект datetime.date.

DateTimeVar

Полная дата и время. Возвращает объект datetime.datetime.

Запуск пользовательских скриптов

Примечание

Для запуска пользовательского скрипта пользователю должны быть назначены разрешения для объектов Extras > Script, Extras > Script Module и Core > Managed File. Им также должно быть назначено разрешение extras.run_script. Это достигается путём назначения пользователю (или группе) разрешения на объект Script и указания действия run в "Permissions", как показано ниже.

Добавление действия run к разрешению

Через веб-интерфейс

Пользовательские скрипты можно запускать через веб-интерфейс, перейдя к скрипту, заполнив необходимые данные формы и нажав кнопку "run script". Можно запланировать выполнение скрипта в указанное время в будущем. Запланированный скрипт можно отменить, удалив связанный объект результата задания.

Предзаполнение переменных через параметры URL

Поля формы скрипта могут быть предзаполнены путём добавления параметров запроса к URL скрипта. Каждое имя параметра должно соответствовать имени переменной, определённой в классе скрипта. Предзаполненные значения рассматриваются как начальные значения и могут быть отредактированы перед выполнением. Несколько значений можно предоставить, повторяя один и тот же параметр. Значения запроса должны быть процентно закодированы, где это требуется (например, пробелы как %20).

Примеры:

Для строковых и целочисленных переменных, когда скрипт определяет:

from extras.scripts import Script, StringVar, IntegerVar

class MyScript(Script):
    name = StringVar()
    count = IntegerVar()

следующий URL предзаполнит поля name и count:

https://<netbox>/extras/scripts/<script_id>/?name=Branch42&count=3

Для переменных объектов (ObjectVar) укажите первичный ключ (PK) объекта:

https://<netbox>/extras/scripts/<script_id>/?device=1

Если ID объекта не может быть разрешён или объект не виден запрашивающему пользователю, поле остаётся незаполненным.

Поддерживаемые типы переменных:

Класс переменной Ожидаемый ввод Пример строки запроса
StringVar строка (процентно закодированная) ?name=Branch42
TextVar строка (процентно закодированная) ?notes=Initial%20value
IntegerVar целое число ?count=3
DecimalVar десятичное число ?ratio=0.75
BooleanVar значение → True; пусто → False ?enabled=true (True), ?enabled= (False)
ChoiceVar значение варианта (не метка) ?role=edge
MultiChoiceVar значения вариантов (повторять) ?roles=edge&roles=core
ObjectVar(Device) PK (целое число) ?device=1
MultiObjectVar(Device) PK (повторять) ?devices=1&devices=2
IPAddressVar IP-адрес ?ip=198.51.100.10
IPAddressWithMaskVar IP-адрес с маской ?addr=192.0.2.1/24
IPNetworkVar IP-сеть (префикс) ?network=2001:db8::/64
DateVar дата ГГГГ-ММ-ДД ?date=2025-01-05
DateTimeVar ISO datetime ?when=2025-01-05T14:30:00
FileVar — (не поддерживается)

Примечание

  • Имена параметров выше являются примерами; используйте фактические имена атрибутов переменных, определённые скриптом.
  • Для BooleanVar только пустое значение (?enabled=) снимает флажок; любое другое значение, включая false или 0, устанавливает его.
  • Загрузка файлов (FileVar) не может быть предзаполнена через параметры URL.

Через API

Чтобы запустить скрипт через REST API, выполните POST-запрос к endpoint скрипта, указав данные формы и фиксацию. Например, чтобы запустить скрипт с именем example.MyReport, мы сделаем запрос следующего вида:

curl -X POST \
-H "Authorization: Token $TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json; indent=4" \
http://netbox/api/extras/scripts/example.MyReport/ \
--data '{"data": {"foo": "somevalue", "bar": 123}, "commit": true}'

Опционально schedule_at может быть передан в данных формы со строкой datetime для планирования скрипта на указанную дату и время.

Через CLI

Скрипты можно запускать в CLI, вызывая команду управления:

python3 manage.py runscript [--commit] [--loglevel {debug,info,warning,error,critical}] [--data "<data>"] <module>.<script>

Обязательный аргумент <module>.<script> — это запускаемый скрипт, где <module> — имя файла Python в каталоге scripts без расширения .py, а <script> — имя класса скрипта в <module> для запуска.

Необязательный аргумент --data "<data>" — данные для отправки скрипту

Необязательный аргумент --loglevel — желаемый уровень логирования для вывода в консоль.

Необязательный аргумент --commit зафиксирует любые изменения в скрипте в базе данных.

Пример

Ниже приведён пример скрипта, который создаёт новые объекты для планируемой площадки. Пользователю предлагается три переменные:

  • Имя новой площадки
  • Модель устройства (отфильтрованный список определённых типов устройств)
  • Количество создаваемых коммутаторов доступа

Эти переменные представлены как веб-форма, которую должен заполнить пользователь. После отправки вызывается метод run() скрипта для создания соответствующих объектов.

from django.utils.text import slugify

from dcim.choices import DeviceStatusChoices, SiteStatusChoices
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
from extras.scripts import *


class NewBranchScript(Script):

    class Meta:
        name = "New Branch"
        description = "Provision a new branch site"
        field_order = ['site_name', 'switch_count', 'switch_model']

    site_name = StringVar(
        description="Name of the new site"
    )
    switch_count = IntegerVar(
        description="Number of access switches to create"
    )
    manufacturer = ObjectVar(
        model=Manufacturer,
        required=False
    )
    switch_model = ObjectVar(
        description="Access switch model",
        model=DeviceType,
        query_params={
            'manufacturer_id': '$manufacturer'
        }
    )

    def run(self, data, commit):

        # Create the new site
        site = Site(
            name=data['site_name'],
            slug=slugify(data['site_name']),
            status=SiteStatusChoices.STATUS_PLANNED
        )
        site.full_clean()
        site.save()
        self.log_success(f"Created new site: {site}")

        # Create access switches
        switch_role = DeviceRole.objects.get(name='Access Switch')
        for i in range(1, data['switch_count'] + 1):
            switch = Device(
                device_type=data['switch_model'],
                name=f'{site.slug}-switch{i}',
                site=site,
                status=DeviceStatusChoices.STATUS_PLANNED,
                role=switch_role
            )
            switch.full_clean()
            switch.save()
            self.log_success(f"Created new switch: {switch}")

        # Generate a CSV table of new devices
        output = [
            'name,make,model'
        ]
        for switch in Device.objects.filter(site=site):
            attrs = [
                switch.name,
                switch.device_type.manufacturer.name,
                switch.device_type.model
            ]
            output.append(','.join(attrs))

        return '\n'.join(output)

  1. Значение этого атрибута должно быть положительным целым числом