Pull to refresh

Comments 2

хороший пример, спасибо, но я не вижу идет ли проверка есть ли уже доступ? например поступила заявка открыть доступ от сервера с1 до сервера с2. есть ли проверка что уже такой доступ существует? у вас есть доступ пользователей к Nautobot и они сами должны проверять все существующие доступы до того как создать заявку? а если их тысячи? и объектные группы внутри других объектных групп?

Мы сознательно отказались от использования в этих политиках вложенных object-group. Реализовать их при таком подходе вроде бы не составляет труда. Но задача, точнее объект автоматизации, был не сложным и решили не городить многофункциональный комбайн :-)

Доступов далеко не тысячи, поэтому их ручная проверка (даже если пользователю лень смотреть в Nautobot и заявка дойдет до сетевого отдела) занимает минимум времени. Но я соглашусь, мысль очень здравая, обязательно в проверку добавлю поиск дублей. Спасибо

Sign up to leave a comment.

Articles

\nIP4_NETWORK_REGEX = r'^(?:[0-9]{1,3}\\.){3}[0-9]{1,3} (255)\\.(0|128|192|224|240|248|252|254|255)\\.(0|128|192|224|240|248|252|254|255)\\.(0|128|192|224|240|248|252|254|255)$ '\nIP4_RANGE_REGEX = r'^(?:[0-9]{1,3}\\.){3}[0-9]{1,3} (?:[0-9]{1,3}\\.){3}[0-9]{1,3}
Pull to refresh

Comments 2

хороший пример, спасибо, но я не вижу идет ли проверка есть ли уже доступ? например поступила заявка открыть доступ от сервера с1 до сервера с2. есть ли проверка что уже такой доступ существует? у вас есть доступ пользователей к Nautobot и они сами должны проверять все существующие доступы до того как создать заявку? а если их тысячи? и объектные группы внутри других объектных групп?

Мы сознательно отказались от использования в этих политиках вложенных object-group. Реализовать их при таком подходе вроде бы не составляет труда. Но задача, точнее объект автоматизации, был не сложным и решили не городить многофункциональный комбайн :-)

Доступов далеко не тысячи, поэтому их ручная проверка (даже если пользователю лень смотреть в Nautobot и заявка дойдет до сетевого отдела) занимает минимум времени. Но я соглашусь, мысль очень здравая, обязательно в проверку добавлю поиск дублей. Спасибо

Sign up to leave a comment.

Articles

\nPORTS_REGEX = r'^(range [0-9]{2,5} [0-9]{2,5})|(eq|lt|gt) [0-9]{1,5}
Pull to refresh

Comments 2

хороший пример, спасибо, но я не вижу идет ли проверка есть ли уже доступ? например поступила заявка открыть доступ от сервера с1 до сервера с2. есть ли проверка что уже такой доступ существует? у вас есть доступ пользователей к Nautobot и они сами должны проверять все существующие доступы до того как создать заявку? а если их тысячи? и объектные группы внутри других объектных групп?

Мы сознательно отказались от использования в этих политиках вложенных object-group. Реализовать их при таком подходе вроде бы не составляет труда. Но задача, точнее объект автоматизации, был не сложным и решили не городить многофункциональный комбайн :-)

Доступов далеко не тысячи, поэтому их ручная проверка (даже если пользователю лень смотреть в Nautobot и заявка дойдет до сетевого отдела) занимает минимум времени. Но я соглашусь, мысль очень здравая, обязательно в проверку добавлю поиск дублей. Спасибо

Sign up to leave a comment.

Articles

\n\nmetadata_scheme = {\n Optional('filter'): {\n Optional('location_types'): [str],\n Optional('locations'): [str],\n Optional('hosts'): [str],\n },\n 'weight': int,\n 'description': str,\n 'is_active': bool,\n 'schema': Or('network_groups',\n 'service_groups',\n 'rules',\n 'policy'\n ),\n}\n\nscheme_dict['network_groups'] = {\n '_metadata': metadata_scheme,\n 'network_groups': {\n str: {\n Optional('description'): str,\n Optional('extend'): bool,\n Optional('fqdn'): str,\n Optional('networks'): [Regex(IP4_NETWORK_REGEX)],\n Optional('hosts'): [Regex(IP4_REGEX)],\n Optional('ranges'): [Regex(IP4_RANGE_REGEX)],\n Optional('groups'): [str],\n },\n }\n}\n\nscheme_dict['service_groups'] = {\n '_metadata': metadata_scheme,\n 'service_groups': {\n str: {\n Optional('extend'): bool,\n Optional('tcp'): [Regex(PORTS_REGEX)],\n Optional('udp'): [Regex(PORTS_REGEX)],\n },\n }\n}\n\nscheme_dict['rules'] = {\n '_metadata': metadata_scheme,\n 'rules': {\n str: {\n Optional('extend'): bool,\n Optional('lines'): [{\n Optional('sequence'): int,\n 'action': Or('permit', 'deny'),\n 'source': str,\n Optional('source_type'): str,\n 'destination': str,\n Optional('destination_type'): str,\n 'services': {\n Optional('protocol'): Or('ip',\n 'icmp',\n 'tcp',\n 'udp'),\n Optional('ports'): [int],\n Optional('group'): str,\n },\n Optional('reason'): str,\n Optional('expiry'): Or(datetime.date,\n datetime.datetime),\n }]\n },\n }\n}\n\nscheme_dict['policy'] = {\n '_metadata': metadata_scheme,\n 'name': Or('RM_NAT', 'ACL_NAT'),\n 'type': Or('route-map', 'acl'),\n Optional('lines'): {\n int: {\n 'rule': str,\n 'action': Or('permit', 'deny'),\n }\n }\n}\u003C\u002Fcode\u003E\u003Cdiv class=\"code-explainer\"\u003E\u003Ca href=\"https:\u002F\u002Fsourcecraft.dev\u002F\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"\u003E\u003Cimg style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\u002F\u003E\u003C\u002Fa\u003E\u003C\u002Fdiv\u003E\u003C\u002Fpre\u003E\u003Cp\u003E\u003C\u002Fp\u003E\u003C\u002Fdiv\u003E\u003C\u002Fdetails\u003E\u003Ch3\u003EПодготовка списка затронутых устройств \u003C\u002Fh3\u003E\u003Cp\u003EТак как нам необходимо при каждом изменении политик отправлять новые конфигурации на конкретные маршрутизаторы, необходимо определить перечень тех устройств, на которые эти изменения могли повлиять. Для этого сперва получим список измененных файлов. У нас есть хэш текущего коммита, воспользуемся им, чтобы сформировать этот список и сохранить его на диск:\u003C\u002Fp\u003E\u003Cpre\u003E\u003Ccode class=\"python\"\u003E# code\u002Fget_changes.py\n\nimport sys\nfrom git import Repo\nfrom pathlib import Path\n\n# Создадим необходимые каталоги\nPath(Path.cwd() \u002F 'artifacts').mkdir(parents=True, exist_ok=True)\nARTIFACTS_PATH = Path.cwd() \u002F 'artifacts'\n\n# хэш коммита передается первым параметром\nCOMMIT_HASH = sys.argv[1]\n\n# подготовим список измененных файлов\nrepo = Repo.init(Path.cwd())\nchanged_files = ''\nfor filename in repo.commit(COMMIT_HASH).stats.files:\n if filename.split('\u002F')[0] == 'intended_state' and filename.split('.')[-1] == 'yaml':\n changed_files += f'{filename}\\n'\n\nif not changed_files:\n print('State не изменился! Завершаем работу')\n exit(1)\nwith open(f'{ARTIFACTS_PATH}\u002Fchanged_files.txt', 'w') as file:\n # сохраним список файлов в артефакты\n file.write(changed_files)\u003C\u002Fcode\u003E\u003Cdiv class=\"code-explainer\"\u003E\u003Ca href=\"https:\u002F\u002Fsourcecraft.dev\u002F\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"\u003E\u003Cimg style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\u002F\u003E\u003C\u002Fa\u003E\u003C\u002Fdiv\u003E\u003C\u002Fpre\u003E\u003Cp\u003EТеперь, используя API Nautobot, мы можем запросить актуальный список маршрутизаторов и ограничить его только теми локациями, которые относятся к файлам, измененным в текущем коммите. В качестве API будем использовать GraphQL, а результаты сохраним в файле \u003Cem\u003Einventory.yaml\u003C\u002Fem\u003E\u003C\u002Fp\u003E\u003Cpre\u003E\u003Ccode class=\"python\"\u003E# code\u002Fprepare_inventory.py\n# часть кода опущена, так как сейчас не важна\n\naffected_items = {\n 'location_types': set(),\n 'locations': set(),\n 'hosts': set(),\n}\naffected_all = False\nfor filename in changed_files:\n with open(filename.strip(), 'r') as file:\n yaml_data = yaml.safe_load(file)\n if 'filter' not in yaml_data['_metadata']:\n # изменение касается всех без исключения\n affected_all = True\n # дальше что-то искать нет смысла\n break\n for filter_name, filter_data in yaml_data['_metadata']['filter'].items():\n for item in filter_data:\n affected_items[filter_name].add(item)\n\n# вытащим из Nautobot список устройств\nnautobot_health = requests.get(f'https:\u002F\u002F{NAUTOBOT_URL}\u002Fhealth', verify=False)\nif nautobot_health.status_code != 200:\n print(\"Ошибка! Nautobot недоступен\")\n exit(1)\n\nquery = '''\nquery {\n devices (role: ['Router'], status: 'Active') {\n name\n primary_ip4 {\n address\n }\n location {\n name\n location_type {\n name\n }\n }\n rel_device_soft {\n version\n device_platform {\n name\n }\n } \n tags {\n name\n }\n }\n}\n'''\nnb = pynautobot.api(\n url = f'https:\u002F\u002F{NAUTOBOT_URL}',\n token = NAUTOBOT_TOKEN,\n threading=True\n)\ngraphql_response = nb.graphql.query(query=query)\ndevices = graphql_response.json['data']['devices']\naffected_devices = {}\naffected_devices_for_test = {}\n\n# список поддерживаемых ОС\nANSIBLE_NETWORK_OS = {\n 'Cisco IOS': 'ios',\n 'Mikrotik RouterOS': 'routeros',\n}\n\nfor device in devices:\n if not device['primary_ip4']:\n # пропустим устройства без IP адреса\n continue\n if (not device['rel_device_soft'] or \n device['rel_device_soft']['platform']['name'] not in ANSIBLE_NETWORK_OS\n ):\n # пропустим неизвестные и неподдерживаемые ОС\n continue \n if (\n affected_all or\n device['location']['location_type']['name'] in affected_items['location_types'] or\n device['location']['name'] in affected_items['locations'] or\n device['name'] in affected_items['hosts']\n ):\n affected_devices[device['name']] = {\n 'address': device['primary_ip4']['address'].split('\u002F')[0],\n 'location': device['location']['name'],\n 'location_type': device['location']['location_type']['name'],\n 'platform': device['rel_device_soft']['platform']['name'],\n 'software_version': device['rel_device_soft']['version']\n }\n\n# подготовим inventory для ansible\ninventory = {\n 'all': {\n 'hosts': {}\n }\n}\nfor host, data in affected_devices.items():\n inventory['all']['hosts'][host] = {\n 'ansible_host': data['address'],\n 'location': data['location'],\n 'location_type': data['location_type'],\n 'software_version': data.get('software_version'),\n 'ansible_connection': 'network_cli',\n 'ansible_network_os': ANSIBLE_NETWORK_OS[data['platform']],\n }\n\nwith open(f'{INVENTORY_PATH}\u002Finventory.yaml', 'w') as file:\n # сохраним инвентори в артефакты\n yaml.dump(inventory, file, allow_unicode=True)\u003C\u002Fcode\u003E\u003Cdiv class=\"code-explainer\"\u003E\u003Ca href=\"https:\u002F\u002Fsourcecraft.dev\u002F\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"\u003E\u003Cimg style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\u002F\u003E\u003C\u002Fa\u003E\u003C\u002Fdiv\u003E\u003C\u002Fpre\u003E\u003Cp\u003EОбратите внимание: в данные каждого хоста мы записываем версию ОС — это пригодится нам в будущем\u003C\u002Fp\u003E\u003Ch3\u003EСборка политик доступа\u003C\u002Fh3\u003E\u003Cp\u003EКак правило, политика доступа для каждого маршрутизатора собирается из нескольких файлов: это могут быть общие политики, политики площадки или политика для самого устройства. Для объединения политик в единое целое будем учитывать веса, указанные в разделе \u003Cem\u003E_metadata\u003C\u002Fem\u003E, и не забываем про директиву \u003Cem\u003Eextend\u003C\u002Fem\u003E. Вот так, например, выглядит фрагмент скрипта \u003Cem\u003Ecode\u002Fprepare_configs.py\u003C\u002Fem\u003E, отвечающий за объединение правил (rules):\u003C\u002Fp\u003E\u003Cpre\u003E\u003Ccode class=\"python\"\u003E# code\u002Fprepare_configs.py (не весь)\n\n# вытащим из артефактов список файлов\nall_files = []\nfor root, dirs, files in os.walk('intended_state', topdown = False):\n for name in files:\n all_files.append(os.path.join(root, name))\n\n# сформируем словарь политик\nintended_schema = defaultdict(dict)\nfor filename in all_files:\n with open(filename.strip(), 'r') as file:\n yaml_data = yaml.safe_load(file)\n if yaml_data['_metadata'].get('is_active'):\n intended_schema[yaml_data['_metadata']['schema']][filename.strip()] = yaml_data\n\n# соберем правила доступа для каждого хоста\nif 'rules' in intended_schema:\n for host, host_data in inventory['all']['hosts'].items():\n location = host_data['location']\n\n # отсортируем правила по весам\n weighted_dict = {}\n for current_state in intended_schema['rules'].values():\n if (\n host_data[‘location_type’] in current_state['_metadata'].get('location_types',[]) or\n host in current_state['_metadata'].get('hosts',[]) or\n host_data['location'] in current_state['_metadata'].get('locations',[])\n ):\n weighted_dict[current_state['_metadata']['weight']] = copy.deepcopy(current_state)\n\n # выберем правила с самым большим весом для этого хоста\n rendered_state = {'rules': {}}\n for weight, current_state in sorted(weighted_dict.items()):\n if weight > rendered_state.get('weight', 0):\n for rule_name, rule_data in current_state['rules'].items():\n if rule_data.get('extend') and rule_name in rendered_state['rules']:\n rendered_state['rules'][rule_name]['lines'].extend(rule_data.get('lines',[]))\n else:\n rendered_state['rules'][rule_name] = rule_data\u003C\u002Fcode\u003E\u003Cdiv class=\"code-explainer\"\u003E\u003Ca href=\"https:\u002F\u002Fsourcecraft.dev\u002F\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"\u003E\u003Cimg style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\u002F\u003E\u003C\u002Fa\u003E\u003C\u002Fdiv\u003E\u003C\u002Fpre\u003E\u003Cp\u003EВ результате для каждого маршрутизатора мы получили словарь с итоговой политикой доступа, включающей правила, описания сервисов и хостов. На этом работа с vendor‑agnostic данными завершена, и пришла пора преобразовать их в команды конфигурации для дальнейшей отправки на устройства.\u003C\u002Fp\u003E\u003Ch3\u003EГенерация целевой конфигурации и команд для отправки\u003C\u002Fh3\u003E\u003Cp\u003EСогласно разработанному dataflow (см. предыдущую часть), нам предстоит сперва подготовить полную целевую конфигурацию, затем сравнить ее с текущей и для каждого маршрутизатора вычислить набор команд, необходимых для приведения конфигурации на устройстве к ожидаемому состоянию.\u003C\u002Fp\u003E\u003Cp\u003EДля нас проще всего было превратить словарь с данными в кусок конфигурации с помощью Python и Jinja2. В процессе разработки мы столкнулись с одним нюансом, который нельзя не отметить. В Cisco IOS начиная с 17 версии в конфигурации ACL сохраняется sequence. А до 16 — не сохраняется. Поэтому при генерации конфигурации мы учитываем версию ПО, установленного на маршрутизатор. Именно для этого мы и сохранили версию в данных хоста в файле inventory.\u003C\u002Fp\u003E\u003Cpre\u003E\u003Ccode class=\"python\"\u003E# code\u002Fprepare_configs.py (не весь)\n\n# sequence зависит от версии ПО, начиная с 17 версии IOS - sequence обязательна\n# по умолчанию шаг=10\ndefault_step = 10\nneed_sequence = host_data['software_version'] and host_data['software_version'].split('.')[0] == '17'\n\nfor rule in rendered_state['rules']:\n step = default_step\n for line in rendered_state['rules'][rule]['lines']:\n if need_sequence and not line.get('sequence'):\n # установим sequence, ведь он необходим, а его нет\n line['sequence'] = step\n step += default_step\n if not need_sequence and line.get('sequence'):\n # удалим sequence из исходных данных\n line.pop('sequence', None)\n \n# целевой конфиг\ntemplate = env.get_template('rules.j2')\nhost_intended_config = template.render({\n 'rules': rendered_state['rules'],\n})\n\n# сохраним целевой конфиг в артефакты для последующего анализа\nwith open(f'{INTENDED_CONFIG_PATH}\u002F{host}_rules.conf', 'w') as file:\n file.write(host_intended_config)\u003C\u002Fcode\u003E\u003Cdiv class=\"code-explainer\"\u003E\u003Ca href=\"https:\u002F\u002Fsourcecraft.dev\u002F\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"\u003E\u003Cimg style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\u002F\u003E\u003C\u002Fa\u003E\u003C\u002Fdiv\u003E\u003C\u002Fpre\u003E\u003Cp\u003EСледующий шаг — сравнение ожидаемой и фактической конфигураций. Напомню, актуальный running‑config каждого сетевого устройства у нас хранится в репозитории на корпоративном Gitlab, так что вытащить его не составляет никакого труда. Для сравнения конфигураций мы используем модуль Compliance, который входит в замечательнейшую библиотеку \u003Ca href=\"https:\u002F\u002Fnetutils.readthedocs.io\u002Fen\u002Flatest\u002F\" rel=\"noopener noreferrer nofollow\"\u003ENetutils \u003C\u002Fa\u003E от network.toCode. На вход подаем название вендора (это может быть Cisco, Huawei и даже Mikrotik), секцию, в которой будем искать различия, и две конфигурации — фактическую и целевую. На выходе получаем два набора команд, лишние и отсутствующие. Вот так легко и элегантно две конфигурации ACL превращаются в список готовых команд для маршрутизаторов Cisco:\u003C\u002Fp\u003E\u003Cpre\u003E\u003Ccode class=\"python\"\u003Efrom netutils.config.compliance import compliance\nfeatures = [\n {\n 'name': 'ACL_TEST_NAT',\n 'ordered': True,\n 'section': ['ip access-list extended ACL_TEST_NAT']\n },\n ]\nhost_intended_config = '''\nip access-list extended ACL_TEST_NAT\n permit icmp any host 8.8.8.8 \n'''\nhost_actual_config = '''\nip access-list extended ACL_TEST_NAT\n permit tcp any host 8.8.8.8 eq 53\n permit udp any host 8.8.8.8 eq 53\n'''\n\nnetwork_os = 'cisco_ios'\ndiff = compliance(features, host_actual_config, host_intended_config, network_os, 'string')\n\n# output pprint(diff[features[0][name]])\n# \n# {'actual': 'ip access-list extended ACL_TEST_NAT\\n'\n# ' permit tcp any host 8.8.8.8 eq 53\\n'\n# ' permit udp any host 8.8.8.8 eq 53',\n# 'cannot_parse': True,\n# 'compliant': False,\n# 'extra': 'ip access-list extended ACL_TEST_NAT\\n'\n# ' permit tcp any host 8.8.8.8 eq 53\\n'\n# ' permit udp any host 8.8.8.8 eq 53',\n# 'intended': 'ip access-list extended ACL_TEST_NAT\\n'\n# ' permit icmp any host 8.8.8.8',\n# 'missing': 'ip access-list extended ACL_TEST_NAT\\n'\n# ' permit icmp any host 8.8.8.8',\n# 'ordered_compliant': False,\n# 'unordered_compliant': False}\u003C\u002Fcode\u003E\u003Cdiv class=\"code-explainer\"\u003E\u003Ca href=\"https:\u002F\u002Fsourcecraft.dev\u002F\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"\u003E\u003Cimg style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\u002F\u003E\u003C\u002Fa\u003E\u003C\u002Fdiv\u003E\u003C\u002Fpre\u003E\u003Cp\u003EЦелевую конфигурацию (\u003Cem\u003Ehost_intended_config\u003C\u002Fem\u003E) мы получили, выполнив рендер в соответствующий шаблон Jinja, а текущую (\u003Cem\u003Ehost_actual_config\u003C\u002Fem\u003E) загрузили из файла бэкапа. Осталось только сохранить в файл команды. Лишние (extra) команды добавим тоже, но уже с ключевым словом \u003Cem\u003Eno \u003C\u002Fem\u003Eв начале строки:\u003C\u002Fp\u003E\u003Cdetails class=\"spoiler\"\u003E\u003Csummary\u003Ecode\u002Fprepare_configs.py (пример для Cisco)\u003C\u002Fsummary\u003E\u003Cdiv class=\"spoiler__content\"\u003E\u003Cpre\u003E\u003Ccode class=\"python\"\u003E# code\u002Fprepare_configs.py (не весь)\n\nfrom netutils.config.compliance import compliance\n\n# вытащим актуальный конфиг\nhost_actual_config = ''\nwith open(f'{CURRENT_STATE_PATH}\u002F{site}\u002F{host}.conf', 'r') as file:\n host_actual_config = file.read()\n\n# список поддерживаемых ОС\nCOMLIANCE_NETWORK_OS = 'cisco_ios'\nhost_diff_config = ''\nfeatures = []\n\n# список ACL для сравнения\nfor rule_name, rule_data in rendered_state['rules'].items():\n features.append({\n 'name': f'ip access-list extended {rule_name}',\n 'ordered': True,\n 'section': [f'ip access-list extended {rule_name}']\n })\nhost_compliance = compliance(\n features,\n host_actual_config,\n host_intended_config,\n COMLIANCE_NETWORK_OS,\n 'string'\n)\n\n# для каждого ACL проверим результаты сравнения\nfor feature in features:\n feature_compliance = host_compliance[feature['name']]\n if not feature_compliance['compliant']:\n # есть различия в конфигурациях\n if feature_compliance['extra']:\n # лишние строки в конфигурации, добавим в начало no\n for line in feature_compliance['extra'].splitlines():\n if line[:1] == ' ':\n host_diff_config += f' no {line[1:].strip()}\\n'\n else:\n host_diff_config += line.strip() + '\\n'\n if feature_compliance['missing']:\n # недостающие строки в конфигурации\n host_diff_config += feature_compliance['missing'] + '\\n'\n\n# сохраним команды в файл для последующего деплоя на устройство\nif host_diff_config:\n with open(f'{DIFF_CONFIG_PATH}\u002F{host}_rules.conf', 'w') as file:\n file.write(host_diff_config)\u003C\u002Fcode\u003E\u003Cdiv class=\"code-explainer\"\u003E\u003Ca href=\"https:\u002F\u002Fsourcecraft.dev\u002F\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"\u003E\u003Cimg style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\u002F\u003E\u003C\u002Fa\u003E\u003C\u002Fdiv\u003E\u003C\u002Fpre\u003E\u003Cp\u003E\u003C\u002Fp\u003E\u003C\u002Fdiv\u003E\u003C\u002Fdetails\u003E\u003Cp\u003EПоскольку мы уверены, что бэкапы содержат актуальные конфигурации маршрутизаторов, такой подход обеспечивает идемпотентность. Повторный запуск приведенного кода не сформирует diff, и на устройство не будут отправлены никакие команды.\u003C\u002Fp\u003E\u003Ch3\u003EДоставка конфигураций\u003C\u002Fh3\u003E\u003Cp\u003EНу вот, самая сложная часть работы была сделана, оставалось только отправить готовый набор команд на нужные устройства. Это мы уже умели делать с помощью Ansible, потребовалось лишь слегка переработать существующий плейбук. В результате работы скрипта \u003Cem\u003Ecode\u002Fprepare_configs.py\u003C\u002Fem\u003E сформировались файлы с командами для каждого маршрутизатора, на который необходимо внести изменения. Плейбук должен проверить наличие соответствующего файла и отправить его на устройство.\u003C\u002Fp\u003E\u003Cdetails class=\"spoiler\"\u003E\u003Csummary\u003Eansible\u002Fplaybook.yml (пример для Cisco)\u003C\u002Fsummary\u003E\u003Cdiv class=\"spoiler__content\"\u003E\u003Cpre\u003E\u003Ccode class=\"yaml\"\u003E---\n- name: Обновление правил NAT\n hosts: all\n gather_facts: false\n vars:\n artifacts_path: artifacts\u002Fconfigs\u002Fdiff\n\n tasks:\n\n - name: Проверка наличия service object group для хоста\n stat:\n path: \"{{ artifacts_path }}\u002F{{ inventory_hostname }}_service_groups.conf\"\n register: host_specific_service_og_config\n\n - name: Проверка наличия network object group для хоста\n stat:\n path: \"{{ artifacts_path }}\u002F{{ inventory_hostname }}_network_groups.conf\"\n register: host_specific_network_og_config\n\n - name: Проверка наличия rules (ACL) для хоста\n stat:\n path: \"{{ artifacts_path }}\u002F{{ inventory_hostname }}_rules.conf\"\n register: host_specific_rules_config\n\n - name: Проверка наличия policy для хоста\n stat:\n path: \"{{ artifacts_path }}\u002F{{ inventory_hostname }}_policy.conf\"\n register: host_specific_policy_config\n\n - name: Обновление service object group \n ios_config:\n src: \"..\u002F{{ artifacts_path }}\u002F{{ inventory_hostname }}_service_groups.conf\"\n when: host_specific_service_og_config.stat.exists\n\n - name: Обновление network object group \n ios_config:\n src: \"..\u002F{{ artifacts_path }}\u002F{{ inventory_hostname }}_network_groups.conf\"\n when: host_specific_network_og_config.stat.exists\n\n - name: Обновление rules (ACL) \n ios_config:\n src: \"..\u002F{{ artifacts_path }}\u002F{{ inventory_hostname }}_rules.conf\"\n when: host_specific_rules_config.stat.exists\n\n - name: Обновление policy (route-map) \n ios_config:\n src: \"..\u002F{{ artifacts_path }}\u002F{{ inventory_hostname }}_policy.conf\"\n when: host_specific_policy_config.stat.exists\n\n - name: Сохранение конфигурации \n ios_config:\n save_when: modified\n\u003C\u002Fcode\u003E\u003Cdiv class=\"code-explainer\"\u003E\u003Ca href=\"https:\u002F\u002Fsourcecraft.dev\u002F\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"\u003E\u003Cimg style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\u002F\u003E\u003C\u002Fa\u003E\u003C\u002Fdiv\u003E\u003C\u002Fpre\u003E\u003Cp\u003E\u003C\u002Fp\u003E\u003C\u002Fdiv\u003E\u003C\u002Fdetails\u003E\u003Ch3\u003EСобираем все воедино - Gitlab CI\u002FCD\u003C\u002Fh3\u003E\u003Cp\u003EК этому моменту мы:\u003C\u002Fp\u003E\u003Col\u003E\u003Cli\u003E\u003Cp\u003Eподготовили политики доступа в формализованном виде,\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003Eпроверили правильность политик доступа,\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003Eсгенерировали целевую конфигурацию для каждого хоста,\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003Eсравнили актуальную и текущую конфигурации и сформировали набор команд для приведения состояния маршрутизатора к нужному виду,\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003Eподготовили список хостов, на которые будем отправлять изменения,\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003Eнаписали Ansible playbook, который отправляет команды на сетевые устройства.\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003C\u002Fol\u003E\u003Cp\u003EТеперь все эти этапы надо было объединить в законченное решение, чтобы при каждом изменении политик в нашем репозитории все скрипты автоматически запускались по порядку. Для этого пришлось погрузиться в DevOps.\u003C\u002Fp\u003E\u003Cfigure class=\"full-width \"\u003E\u003Cimg src=\"https:\u002F\u002Fhabrastorage.org\u002Fr\u002Fw1560\u002Fgetpro\u002Fhabr\u002Fupload_files\u002F4f8\u002F96d\u002Ff34\u002F4f896df34d680e04926308078d504110.jpeg\" width=\"792\" height=\"747\" sizes=\"(max-width: 780px) 100vw, 50vw\" srcset=\"https:\u002F\u002Fhabrastorage.org\u002Fr\u002Fw780\u002Fgetpro\u002Fhabr\u002Fupload_files\u002F4f8\u002F96d\u002Ff34\u002F4f896df34d680e04926308078d504110.jpeg 780w, https:\u002F\u002Fhabrastorage.org\u002Fr\u002Fw1560\u002Fgetpro\u002Fhabr\u002Fupload_files\u002F4f8\u002F96d\u002Ff34\u002F4f896df34d680e04926308078d504110.jpeg 781w\" loading=\"lazy\" decode=\"async\"\u002F\u003E\u003C\u002Ffigure\u003E\u003Cp\u003EНе буду рассказывать про настройку Gitlab и установку gitlab‑runners, это заслуживает отдельной статьи, и не одной. Если не раскрывать всю магию DevOps, то с точки сетевого инженера пайплайн выглядит следующим образом:\u003C\u002Fp\u003E\u003Cul\u003E\u003Cli\u003E\u003Cp\u003Eвносим изменения в YAML (коммитим в ветку test или master),\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003Eзапускается проверка всех файлов с политиками,\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003Eформируется список хостов, которых касаются внесенные изменения,\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003Eдля каждого хоста создаются файлы с командами для отправки на устройство,\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003Eкоманды отправляются на целевые маршрутизаторы,\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003Eинициируется обновление бэкапов конфигураций маршрутизаторов в Gitlab.\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Cp\u003EЭтап отправки набора команд на устройства запускается вручную, чтобы была возможность посмотреть и проверить файлы с наборами команд. Но если изменения в репозиторий были инициированы доверенным скриптом, то сформированные команды отправляются на маршрутизаторы автоматически. \u003C\u002Fp\u003E\u003Cp\u003EЗа все это отвечает стандартный механизм Gitlab CI\u002FCD, который настраивается в файле.gitlab‑ci.yml\u003C\u002Fp\u003E\u003Cdetails class=\"spoiler\"\u003E\u003Csummary\u003E.gitlab-ci.yml\u003C\u002Fsummary\u003E\u003Cdiv class=\"spoiler__content\"\u003E\u003Cpre\u003E\u003Ccode class=\"yaml\"\u003Estages:\n - lint\n - prepare\n - deploy\n\n# ШАБЛОНЫ\n.default-stage-template:\n image: network\u002Fansible:2.13-22.04\n tags:\n - network-runner\n\n.prepare-stage-template:\n extends:\n - .default-stage-template\n script:\n - python3 .\u002Fcode\u002Fget_changes.py $CI_COMMIT_SHORT_SHA\n - python3 .\u002Fcode\u002Fprepare_inventory.py\n - python3 .\u002Fcode\u002Fprepare_configs.py\n artifacts:\n paths:\n - artifacts\u002F\n expire_in: 1 week\n\n.deploy-stage-template:\n extends:\n - .default-stage-template\n script:\n - cp ansible\u002Fansible.cfg \u002Fetc\u002Fansible\u002F\n - ansible-playbook ansible\u002Fplaybook.yaml\n after_script:\n - python3 .\u002Fcode\u002Fupdate_current_state.py\n\n# LINT – проверка файлов состояния\nyaml-lint:\n # проверка yaml-файлов\n # выполняется при коммите в master или test, если меняется intended state\n stage: lint\n extends:\n - .default-stage-template\n script:\n - python3 .\u002Fcode\u002Flint_state.py\n rules:\n - if: ($CI_PIPELINE_SOURCE == \"push\" && ($CI_COMMIT_BRANCH == \"test\" || $CI_COMMIT_BRANCH == \"master\")) || $CI_PIPELINE_SOURCE == \"web\"\n changes:\n - intended_state\u002F**\u002F*.yaml\n\n# PREPARE – подготовка inventory и конфигураций\nprepare-artifacts-test:\n # подготовка конфигов и inventory-файла для тестовых роутеров\n # выполняется только при коммите в ветку test, если меняется intended state\n stage: prepare\n extends:\n - .prepare-artifacts-template\n environment:\n name: test\n rules:\n - if: $CI_PIPELINE_SOURCE == \"push\" && $CI_COMMIT_BRANCH == \"test\"\n changes:\n - intended_state\u002F**\u002F*.yaml\n\nprepare-artifacts:\n # подготовка конфигов и inventory-файла для роутеров, у которых изменился state\n # выполняется при коммите в master, если меняется intended state\n stage: prepare\n extends:\n - .prepare-artifacts-template\n environment:\n name: prod\n rules:\n - if: $CI_PIPELINE_SOURCE == \"push\" && $CI_COMMIT_BRANCH == \"master\"\n changes:\n - intended_state\u002F**\u002F*.yaml\n\nprepare-artifacts-all:\n # подготовка конфигов и inventory-файла для всех роутеров\n # выполняется при запуске pipeline через веб-интерфейс\n stage: prepare\n extends:\n - .prepare-artifacts-template\n environment:\n name: manual\n rules:\n - if: $CI_PIPELINE_SOURCE == \"web\"\n\n# DEPLOY - отправка команд на роутеры\ndeploy-test:\n # отправка команд на тестовые роутеры\n # выполняется только при коммите в ветку test\n # запуск вручную\n stage: deploy\n extends:\n - .deploy-stage-template\n environment:\n name: test\n before_script:\n - mkdir \u002Fansible\u002Finventory\n - cp artifacts\u002Finventory_for_test\u002Finventory.yaml \u002Fansible\u002Finventory\u002F\n rules:\n - if: ($CI_PIPELINE_SOURCE == \"push\" && $CI_COMMIT_BRANCH == \"test\") || $CI_PIPELINE_SOURCE == \"web\"\n changes:\n - intended_state\u002F**\u002F*.yaml\n when: manual\n\nmanual-deploy-prod:\n # отправка команд на роутеры, у которых изменился state\n # выполняется только при коммите в ветку master\n # запуск вручную\n stage: deploy\n environment:\n name: prod\n extends:\n - .deploy-stage-template\n before_script:\n - mkdir \u002Fansible\u002Finventory\n - cp artifacts\u002Finventory\u002Finventory.yaml \u002Fansible\u002Finventory\u002F\n rules:\n - if: ($CI_PIPELINE_SOURCE == \"push\" && $CI_COMMIT_BRANCH == \"master\" && $CI_COMMIT_AUTHOR != \"nautobot”) || $CI_PIPELINE_SOURCE == \"web\"\n changes:\n - intended_state\u002F**\u002F*.yaml\n when: manual\n\nauto-deploy-prod-from-nautobot:\n # отправка команд на тестовые роутеры\n # выполняется только при коммите в ветку master\n # автором коммита может быть только nautobot\n # запуск автоматический\n stage: deploy\n environment:\n name: prod\n extends:\n - .deploy-stage-template\n before_script:\n - mkdir \u002Fansible\u002Finventory\n - cp artifacts\u002Finventory\u002Finventory.yaml \u002Fansible\u002Finventory\u002F\n rules:\n - if: $CI_PIPELINE_SOURCE == \"push\" && $CI_COMMIT_BRANCH == \"master\" && $CI_COMMIT_AUTHOR == \"nautobot\"\n changes:\n - intended_state\u002F**\u002F*.yaml\u003C\u002Fcode\u003E\u003Cdiv class=\"code-explainer\"\u003E\u003Ca href=\"https:\u002F\u002Fsourcecraft.dev\u002F\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"\u003E\u003Cimg style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\u002F\u003E\u003C\u002Fa\u003E\u003C\u002Fdiv\u003E\u003C\u002Fpre\u003E\u003Cp\u003E\u003C\u002Fp\u003E\u003C\u002Fdiv\u003E\u003C\u002Fdetails\u003E\u003Cp\u003EНастроены три этапа:\u003C\u002Fp\u003E\u003Cul\u003E\u003Cli\u003E\u003Cp\u003Elint (проверка данных)\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003Eprepare (подготовка артефактов — список хостов, конфигурации)\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003Edeploy (доставка конфигураций)\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Cp\u003Eи два окружения:\u003C\u002Fp\u003E\u003Cul\u003E\u003Cli\u003E\u003Cp\u003Eprod (доставка на маршрутизаторы, которых касаются изменения в текущем коммите)\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003Etest (аналогично prod, но доставка только на тестовые маршрутизаторы)\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Cp\u003EПосле выполнения этапа deploy запускается скрипт \u003Cem\u003Eupdate_current_state.py\u003C\u002Fem\u003E, основное назначение которого — инициировать обновление бэкапов текущих конфигураций всех маршрутизаторов, на которые была отправлена новая конфигурация.\u003C\u002Fp\u003E\u003Ch3\u003EАвтоматическое внесение изменений\u003C\u002Fh3\u003E\u003Cp\u003EОчень часто к нам приходят с задачей открыть доступ к определенному сайту по его доменному имени. К сожалению, Cisco IOS позволяет использовать в ACL и object‑group только IP‑адреса. Да, конечно, при создании ACL можно указать hostname, но оно немедленно будет преобразовано в IP‑адрес, который и попадет в конфигурацию. \u003C\u002Fp\u003E\u003Cp\u003EТо же самое касается и заявок на открытие временного доступа. Вроде и есть в Cisco IOS функционал time‑based ACL, но для таких запросов он подходит не очень. Надо каждый раз создавать новый time‑range, а потом его удалять.\u003C\u002Fp\u003E\u003Cp\u003EТеперь же, имея репозиторий с формализованными политиками доступа и возможность автоматического деплоя при изменении данных, мы легко можем следить за изменениями в DNS или за сроком действия правил.\u003C\u002Fp\u003E\u003Cp\u003EПо уже сложившейся традиции, мы разработали плагин для Nautobot, в состав которого вошел Job, запускаемый встроенным планировщиком. Job клонирует репозиторий, после чего парсит правила и описания сетей. Если в описании хоста присутствует поле \u003Cem\u003Efqdn\u003C\u002Fem\u003E, Job асинхронно обращается к серверам DNS для проверки актуальности IP‑адресов. Правила с истекшим сроком действия (\u003Cem\u003Eexpiry) \u003C\u002Fem\u003Eудаляются из политик. Все изменения сохраняются обратно в файлы и коммитятся и пушатся на сервер. Ну а дальше все происходит по уже описанной схеме.\u003C\u002Fp\u003E\u003Cp\u003ENautobot позволяет легко реализовать планировщик, просмотр логов работы скрипта, уведомление по электронной почте об изменениях в политике доступа. Приятным бонусом разработанного плагина стала возможность сделать удобное представление политик доступа в табличном виде.\u003C\u002Fp\u003E\u003Ch3\u003EБонус. Покажем политики доступа пользователям\u003C\u002Fh3\u003E\u003Cp\u003EМожно было, конечно, дать доступ пользователям к репозиторию. Но искать нужную информацию во множестве YAML‑файлов — задачка не из легких. Гораздо проще найти площадку в Nautobot и увидеть все правила, которые к ней привязаны. К счастью, Nautobot — открытая система и позволяет добавлять подобные страницы к любому объекту учета.\u003C\u002Fp\u003E\u003Cp\u003EТак у нас можно вывести правила для конкретной площадки:\u003C\u002Fp\u003E\u003Cfigure class=\"full-width \"\u003E\u003Cimg src=\"https:\u002F\u002Fhabrastorage.org\u002Fr\u002Fw1560\u002Fgetpro\u002Fhabr\u002Fupload_files\u002F0f6\u002F674\u002F855\u002F0f6674855bde69b779d6ae6165339196.jpg\" width=\"1253\" height=\"462\" sizes=\"(max-width: 780px) 100vw, 50vw\" srcset=\"https:\u002F\u002Fhabrastorage.org\u002Fr\u002Fw780\u002Fgetpro\u002Fhabr\u002Fupload_files\u002F0f6\u002F674\u002F855\u002F0f6674855bde69b779d6ae6165339196.jpg 780w, https:\u002F\u002Fhabrastorage.org\u002Fr\u002Fw1560\u002Fgetpro\u002Fhabr\u002Fupload_files\u002F0f6\u002F674\u002F855\u002F0f6674855bde69b779d6ae6165339196.jpg 781w\" loading=\"lazy\" decode=\"async\"\u002F\u003E\u003C\u002Ffigure\u003E\u003Cp\u003EНа странице IP‑адреса можно посмотреть правила, в которые он попадает:\u003C\u002Fp\u003E\u003Cfigure class=\"full-width \"\u003E\u003Cimg src=\"https:\u002F\u002Fhabrastorage.org\u002Fr\u002Fw1560\u002Fgetpro\u002Fhabr\u002Fupload_files\u002Fdf8\u002Fae8\u002F612\u002Fdf8ae861280e1520758e33e61f173e1e.jpg\" width=\"1318\" height=\"552\" sizes=\"(max-width: 780px) 100vw, 50vw\" srcset=\"https:\u002F\u002Fhabrastorage.org\u002Fr\u002Fw780\u002Fgetpro\u002Fhabr\u002Fupload_files\u002Fdf8\u002Fae8\u002F612\u002Fdf8ae861280e1520758e33e61f173e1e.jpg 780w, https:\u002F\u002Fhabrastorage.org\u002Fr\u002Fw1560\u002Fgetpro\u002Fhabr\u002Fupload_files\u002Fdf8\u002Fae8\u002F612\u002Fdf8ae861280e1520758e33e61f173e1e.jpg 781w\" loading=\"lazy\" decode=\"async\"\u002F\u003E\u003C\u002Ffigure\u003E\u003Ch3\u003EВыводы, которые каждый может сделать сам\u003C\u002Fh3\u003E\u003Cp\u003EДаже простые рутинные операции лучше автоматизировать. Это сильно снижает вероятность человеческой ошибки и ускоряет время выполнения заявок. Чем же хорош подход «Network as code»? Работать с текстовыми данными очень удобно; система контроля версий предоставляет удобный функционал по версионированию, логированию, авторизации; использование единого источника истины позволяет переиспользовать данные, например, для систем ИБ. Рассмотренное решение легко можно адаптировать для автоматизации управления политиками QoS или правилами zone‑based firewall.\u003C\u002Fp\u003E\u003Cp\u003E\u003C\u002Fp\u003E\u003C\u002Fdiv\u003E","tags":[{"titleHtml":"netdevops"},{"titleHtml":"автоматизация"},{"titleHtml":"infrastructure-as-a-code"}],"metadata":{"stylesUrls":[],"scriptUrls":[],"shareImageUrl":"https:\u002F\u002Fhabrastorage.org\u002Fgetpro\u002Fhabr\u002Fupload_files\u002F536\u002Fdb9\u002F38a\u002F536db938abb74c51005cb13381cf466e.jpg","shareImageWidth":1200,"shareImageHeight":630,"vkShareImageUrl":"https:\u002F\u002Fhabrastorage.org\u002Fgetpro\u002Fhabr\u002Fupload_files\u002F536\u002Fdb9\u002F38a\u002F536db938abb74c51005cb13381cf466e.jpg","schemaJsonLd":"{\"@context\":\"http:\\\u002F\\\u002Fschema.org\",\"@type\":\"Article\",\"mainEntityOfPage\":{\"@type\":\"WebPage\",\"@id\":\"https:\\\u002F\\\u002Fhabr.com\\\u002Fru\\\u002Farticles\\\u002F893804\\\u002F\"},\"headline\":\"Управляем сетевыми политиками доступа в стиле «Network as Code». Часть 2\",\"datePublished\":\"2025-03-24T16:06:46+03:00\",\"dateModified\":\"2025-03-24T16:06:46+03:00\",\"author\":{\"@type\":\"Person\",\"name\":\"Макс Ионцев\"},\"publisher\":{\"@type\":\"Organization\",\"name\":\"Habr\",\"logo\":{\"@type\":\"ImageObject\",\"url\":\"https:\\\u002F\\\u002Fhabrastorage.org\\\u002Fwebt\\\u002Fa_\\\u002Flk\\\u002F9m\\\u002Fa_lk9mjkccjox-zccjrpfolmkmq.png\"}},\"description\":\"И снова привет, Хабр! Продолжаю рассказывать, как мы с коллегами решали небольшую задачу по автоматизации управления списками доступов на пограничных маршрутизат...\",\"url\":\"https:\\\u002F\\\u002Fhabr.com\\\u002Fru\\\u002Farticles\\\u002F893804\\\u002F#post-content-body\",\"about\":[\"h_network_technologies\",\"h_devops\",\"h_it-infrastructure\",\"h_sys_admin\",\"f_admin\"],\"image\":[\"https:\\\u002F\\\u002Fhabr.com\\\u002Fshare\\\u002Fpublication\\\u002F893804\\\u002F852c898815245802386e56a9d616e975\\\u002F\",\"https:\\\u002F\\\u002Fhabrastorage.org\\\u002Fgetpro\\\u002Fhabr\\\u002Fupload_files\\\u002F536\\\u002Fdb9\\\u002F38a\\\u002F536db938abb74c51005cb13381cf466e.jpg\",\"https:\\\u002F\\\u002Fhabrastorage.org\\\u002Fgetpro\\\u002Fhabr\\\u002Fupload_files\\\u002F4f8\\\u002F96d\\\u002Ff34\\\u002F4f896df34d680e04926308078d504110.jpeg\",\"https:\\\u002F\\\u002Fhabrastorage.org\\\u002Fgetpro\\\u002Fhabr\\\u002Fupload_files\\\u002F0f6\\\u002F674\\\u002F855\\\u002F0f6674855bde69b779d6ae6165339196.jpg\",\"https:\\\u002F\\\u002Fhabrastorage.org\\\u002Fgetpro\\\u002Fhabr\\\u002Fupload_files\\\u002Fdf8\\\u002Fae8\\\u002F612\\\u002Fdf8ae861280e1520758e33e61f173e1e.jpg\"]}","metaDescription":"И снова привет, Хабр! Продолжаю рассказывать, как мы с коллегами решали небольшую задачу по автоматизации управления списками доступов на пограничных маршрутизаторах....","mainImageUrl":null,"amp":true,"customTrackerLinks":[]},"polls":[],"commentsEnabled":{"status":true,"reason":null},"rulesRemindEnabled":false,"votesEnabled":true,"status":"published","plannedPublishTime":null,"checked":null,"hasPinnedComments":false,"format":"case","banner":null,"multiwidget":null,"multiwidgetUuid":null,"readingTime":18,"complexity":"medium","isEditorial":false,"flowNew":null,"linkedPostTranslation":null,"hasRegionalRestrictions":false}},"articlesIds":{},"isLoading":false,"pagesCount":{},"route":{},"reasonsList":null,"postReasonsList":null,"view":"cards","oldEditorForm":null,"lastVisitedRoute":{},"ssrCommentsArticleIds":[],"viewedPosts":[],"myFeedFilter":{"complexity":"all","score":"all","types":["articles","posts","news"]},"myFeedIsApplyFilters":false,"myFeedIsForce":false},"me":{"user":null,"uuid":null,"ppgDemanded":false,"karmaResetInfo":{"canReincarnate":null,"wasReincarnated":null,"currentScore":null},"notes":null,"userUpdates":{"feeds":{"newCount":null},"conversationUnreadCount":0,"trackerUnreadCount":0},"features":null},"flowsMenu":{"flowsMenuData":{"whatsNew":{"newAuthors":{"order":7,"items":[{"alias":"Maden4ic","fullname":"Maden4ic","avatarUrl":null},{"alias":"slavacpp","fullname":"0xC0DE1E55","avatarUrl":null},{"alias":"Gim6626","fullname":"Дмитрий Винокуров","avatarUrl":"\u002F\u002Fhabrastorage.org\u002Fgetpro\u002Fhabr\u002Favatars\u002F27a\u002Fe3f\u002Fda7\u002F27ae3fda77c3828724b6c0568d17ee16.jpg"}]},"updates":{"order":8,"items":[{"title":"Changelog","url":"https:\u002F\u002Fhabr.com\u002Fen\u002Fdocs\u002Fchangelog\u002F2025\u002F","imageUrl":null},{"title":"Habr Code of Authors","url":"https:\u002F\u002Fhabr.com\u002Fen\u002Fdocs\u002Fauthors\u002Fcodex\u002F","imageUrl":null},{"title":"How it works","url":"https:\u002F\u002Fhabr.com\u002Fen\u002Fdocs\u002Fhelp\u002Frules\u002F","imageUrl":null},{"title":"Corporate blogs","url":"https:\u002F\u002Fhabr.com\u002Fen\u002Fdocs\u002Fcompanies\u002Fcorpblogs\u002F","imageUrl":null}]},"socialNetwork":{"order":1},"thematicChannels":{"order":3},"ourActivities":{"order":4},"partnersActivities":{"order":5},"banner":{"order":6},"ourBlogs":{"order":2}},"flows":{"backend":{"technologies":{"order":1,"items":[{"alias":"go","title":"Go","imageUrl":"\u002F\u002Fhabrastorage.org\u002Fgetpro\u002Fhabr\u002Fhub\u002Fa10\u002Fc5c\u002F626\u002Fa10c5c62685d9a7d2964993daf6958c3.png"}]},"topics":{"order":2,"items":[]},"topPosts":{"order":3,"items":[],"period":"day"},"topAuthors":{"order":4,"items":[{"alias":"xeovo","fullname":"xeovo","avatarUrl":"\u002F\u002Fhabrastorage.org\u002Fgetpro\u002Fhabr\u002Favatars\u002Fe83\u002Fde8\u002Ff9e\u002Fe83de8f9ed182d9287ca9900d263c2f9.png"}]},"topCompanies":{"order":5,"items":[{"alias":"xeovo","title":"Xeovo VPN","imageUrl":"\u002F\u002Fhabrastorage.org\u002Fgetpro\u002Fhabr\u002Fcompany\u002F864\u002F431\u002F196\u002F864431196b8a78de320f5707f6d8ff33.png"}]},"banner":{"order":6}},"frontend":{"technologies":{"order":1,"items":[{"alias":"javascript","title":"JavaScript","imageUrl":"\u002F\u002Fhabrastorage.org\u002Fgetpro\u002Fhabr\u002Fhub\u002F2b3\u002F99b\u002F964\u002F2b399b964d456f3ad1bfddc0346b60d4.png"},{"alias":"reactjs","title":"ReactJS","imageUrl":"\u002F\u002Fhabrastorage.org\u002Fgetpro\u002Fhabr\u002Fhub\u002Fe77\u002F27d\u002F25b\u002Fe7727d25b433a67f0e69acc74d2ba785.png"},{"alias":"nestjs","title":"NestJS","imageUrl":"\u002F\u002Fhabrastorage.org\u002Fgetpro\u002Fhabr\u002Fhub\u002F820\u002Ff03\u002F364\u002F820f03364cccdb926fd93cfe2438f962.png"}]},"topics":{"order":2,"items":[]},"topPosts":{"order":3,"items":[],"period":"day"},"topAuthors":{"order":4,"items":[{"alias":"aymericzip","fullname":"Aymeric PINEAU","avatarUrl":null}]},"topCompanies":{"order":5,"items":[]},"banner":{"order":6}},"hardware_and_gadgets":{"technologies":{"order":1,"items":[]},"topics":{"order":2,"items":[]},"topPosts":{"order":3,"items":[{"id":"1033642","isCorporative":false,"lang":"en","titleHtml":"Appeal to keyboard makers: Please Stop Adding FN Buttons","postType":"article","hubs":[{"id":"21898","alias":"hardware","type":"collective","title":"Computer hardware","titleHtml":"Computer hardware","isProfiled":false},{"id":"17189","alias":"itstandarts","type":"collective","title":"IT Standards","titleHtml":"IT Standards","isProfiled":true},{"id":"21954","alias":"Peripheral","type":"collective","title":"Periphery","titleHtml":"Periphery","isProfiled":false}],"author":{"alias":"Murz","fullname":"Alexey Murz Korepov","avatarUrl":"\u002F\u002Fhabrastorage.org\u002Fgetpro\u002Fhabr\u002Favatars\u002F527\u002Fcd0\u002F606\u002F527cd060631ef496fb52ce16199efca0.jpg"}}],"period":"day"},"topAuthors":{"order":4,"items":[]},"topCompanies":{"order":5,"items":[]},"banner":{"order":6}},"mobile_development":{"technologies":{"order":1,"items":[{"alias":"dart","title":"Dart","imageUrl":"\u002F\u002Fhabrastorage.org\u002Fgetpro\u002Fhabr\u002Fhub\u002F41e\u002F671\u002F2dd\u002F41e6712dd7d298077553efff8562bd73.png"},{"alias":"flutter","title":"Flutter","imageUrl":"\u002F\u002Fhabrastorage.org\u002Fgetpro\u002Fhabr\u002Fhub\u002F63b\u002F728\u002Fb76\u002F63b728b76ec18862a5454a684509940b.png"},{"alias":"cpp","title":"C++","imageUrl":"\u002F\u002Fhabrastorage.org\u002Fgetpro\u002Fhabr\u002Fhub\u002F89a\u002Fc44\u002F09e\u002F89ac4409ea406d835a82383fa53fcda7.png"},{"alias":"qt_software","title":"Qt","imageUrl":"\u002F\u002Fhabrastorage.org\u002Fgetpro\u002Fhabr\u002Fhub\u002Fa69\u002F436\u002F043\u002Fa69436043016a35349d05068d584b316.png"}]},"topics":{"order":2,"items":[]},"topPosts":{"order":3,"items":[],"period":"day"},"topAuthors":{"order":4,"items":[{"alias":"Paulik8","fullname":"Paulik8","avatarUrl":null},{"alias":"slavacpp","fullname":"0xC0DE1E55","avatarUrl":null}]},"topCompanies":{"order":5,"items":[]},"banner":{"order":6}},"industrial_engineering":{"technologies":{"order":1,"items":[]},"topics":{"order":2,"items":[]},"topPosts":{"order":3,"items":[],"period":"day"},"topAuthors":{"order":4,"items":[{"alias":"rsashka","fullname":"Александр Рябиков","avatarUrl":"\u002F\u002Fhabrastorage.org\u002Fgetpro\u002Fhabr\u002Favatars\u002Fad5\u002Fd5f\u002Feb2\u002Fad5d5feb2fbf547d4d74f65d041cb2c7.jpg"}]},"topCompanies":{"order":5,"items":[]},"banner":{"order":6}}},"events":{"items":[]}},"isMenuVisible":false},"banner":{"isArticleStickyPanelVisible":false,"isArticleStickyPanelAtTheBottom":false,"bannerContainer":null,"isHeaderBannerInView":true,"isSponsorBlockPresent":false,"isHeaderBannerVisible":true,"isPBrandingVisible":false},"refs":{"flowsRefs":[{"id":"2","title":"Бэкенд","alias":"backend"},{"id":"4","title":"Фронтенд","alias":"frontend"},{"id":"6","title":"Мобильная разработка","alias":"mobile_development"},{"id":"8","title":"Системное администрирование","alias":"admin"},{"id":"10","title":"Информационная безопасность","alias":"information_security"},{"id":"12","title":"AI и ML","alias":"ai_and_ml"},{"id":"14","title":"Промышленная инженерия","alias":"industrial_engineering"},{"id":"16","title":"Геймдев","alias":"gamedev"},{"id":"18","title":"Тестирование","alias":"quality_assurance"},{"id":"20","title":"Техническая поддержка","alias":"support"},{"id":"22","title":"Системный и бизнес-анализ","alias":"analytics"},{"id":"24","title":"Дизайн","alias":"design"},{"id":"26","title":"Менеджмент","alias":"management"},{"id":"28","title":"Топ-менеджмент","alias":"top_management"},{"id":"30","title":"Маркетинг и контент","alias":"marketing"},{"id":"34","title":"Продажи","alias":"sales"},{"id":"36","title":"HR","alias":"human_resources"},{"id":"38","title":"Бэк-офис","alias":"back_office"},{"id":"40","title":"Зерокодинг","alias":"zero-code_development"},{"id":"42","title":"Железо и гаджеты","alias":"hardware_and_gadgets"},{"id":"44","title":"DIY","alias":"diy"},{"id":"46","title":"Здоровье","alias":"healthcare"},{"id":"48","title":"Научпоп","alias":"popsci"},{"id":"50","title":"Другое","alias":"other"}]},"promoData":{"isLoading":false,"hasLoaded":false,"featurer":null,"megaposts":null,"promoLinks":null,"promoPosts":null,"sticker":null,"stories":null,"activities":[],"partnerActivities":[],"isPromoDataAvailable":{"featurer":true,"promoPosts":true,"promoLinks":false,"megaposts":false}},"fixedBanner":{"isLoading":false,"viewEventsSent":[],"imagesLoaded":{}},"companies":{"companyRefs":{"__ALIAS_STORE__":true},"companyIds":{},"companyTopIds":[],"companyRouteStatistics":{},"pagesCount":{},"companyProfiles":{"__ALIAS_STORE__":true},"companiesCategories":[],"companiesCategoriesTotalCount":0,"companiesWidgets":{"__ALIAS_STORE__":true},"companiesWorkers":{"__ALIAS_STORE__":true},"companiesFans":{"__ALIAS_STORE__":true},"multiwidgets":{"__ALIAS_STORE__":true},"route":{},"isLoading":false,"companyWorkersLoading":false,"companyFansLoading":false,"multiwidgetLoading":false,"vacancies":{},"companiesGalleries":{"__ALIAS_STORE__":true},"companiesBanners":{"__ALIAS_STORE__":true},"companiesLandingVacancies":{"__ALIAS_STORE__":true},"companiesTechnologies":{"__ALIAS_STORE__":true},"workplaceInfo":null},"ssr":{"error":null,"isDataLoaded":true,"isDataLoading":false},"hubs":{"hubRefs":{"__ALIAS_STORE__":true},"hubIds":{},"hubRouteStats":{},"pagesCount":{},"isLoading":false,"counters":{"__ALIAS_STORE__":true},"route":{"name":"","params":{},"query":{}}},"adblock":{"hasAcceptableAdsFilter":false,"hasAdblock":false},"pullRefresh":{"shouldRefresh":false},"viewport":{"prevScrollY":{},"scrollY":0,"width":0,"pageContentHeight":0},"feedStatistics":{"feedsStatistics":{}},"pageArticleComments":{"lastViewedComment":0,"postId":null,"lastCommentTimestamp":1742838374,"moderated":[],"moderatedIds":[],"commentRoute":"","idempotenceKey":""},"comments":{"articleComments":{"893804":{"refs":{"28083608":{"id":"28083608","parentId":null,"level":0,"timePublished":"2025-03-24T16:57:56+00:00","timeChanged":null,"isSuspended":false,"status":"published","score":0,"votesCount":0,"message":"\u003Cp\u003Eхороший пример, спасибо, но я не вижу идет ли проверка есть ли уже доступ? например поступила заявка открыть доступ от сервера с1 до сервера с2. есть ли проверка что уже такой доступ существует? у вас есть доступ пользователей к Nautobot и они сами должны проверять все существующие доступы до того как создать заявку? а если их тысячи? и объектные группы внутри других объектных групп?\u003C\u002Fp\u003E","editorVersion":2,"author":{"id":"3382387","alias":"CCNPengineer","fullname":null,"avatarUrl":null,"speciality":null,"deleted":false},"isAuthor":false,"isPostAuthor":false,"isNew":false,"isFavorite":false,"isCanEdit":false,"timeEditAllowedTill":null,"children":["28083834"],"vote":null,"votePlus":null,"voteMinus":null,"isPinned":false},"28083834":{"id":"28083834","parentId":"28083608","level":1,"timePublished":"2025-03-24T17:46:14+00:00","timeChanged":null,"isSuspended":false,"status":"published","score":1,"votesCount":1,"message":"\u003Cp\u003EМы сознательно отказались от использования в этих политиках вложенных object-group. Реализовать их при таком подходе вроде бы не составляет труда. Но задача, точнее объект автоматизации, был не сложным и решили не городить многофункциональный комбайн :-)\u003C\u002Fp\u003E\u003Cp\u003EДоступов далеко не тысячи, поэтому их ручная проверка (даже если пользователю лень смотреть в Nautobot и заявка дойдет до сетевого отдела) занимает минимум времени. Но я соглашусь, мысль очень здравая, обязательно в проверку добавлю поиск дублей. Спасибо\u003C\u002Fp\u003E","editorVersion":2,"author":{"id":"360999","alias":"iontzev","fullname":"Макс Ионцев","avatarUrl":"\u002F\u002Fhabrastorage.org\u002Fgetpro\u002Fhabr\u002Favatars\u002F478\u002F9e6\u002Fc5a\u002F4789e6c5a632edc325e980ee25290397.jpg","speciality":"Network Team Lead","deleted":false},"isAuthor":false,"isPostAuthor":false,"isNew":false,"isFavorite":false,"isCanEdit":false,"timeEditAllowedTill":null,"children":[],"vote":null,"votePlus":null,"voteMinus":null,"isPinned":false}},"root":["28083608"],"cacheKey":"db04f1b4-ddd6-47e3-971c-f0a0ebc162b6"}},"articlePinnedComments":{"893804":{"refs":{},"commentIds":[]}},"searchCommentsResults":null,"pagesCount":null,"commentAccess":{},"scrollParents":{}},"publicationStats":{"statsInfo":{},"statsFunnels":{},"statsGraph":{},"defaultSuggest":{},"suggest":{},"timeTracker":{},"isUserActive":true,"otherPublicationStats":{}}};(function(){var s=document.currentScript||document.scripts[document.scripts.length-1];s.parentNode.removeChild(s);}());
Pull to refresh

Comments 2

хороший пример, спасибо, но я не вижу идет ли проверка есть ли уже доступ? например поступила заявка открыть доступ от сервера с1 до сервера с2. есть ли проверка что уже такой доступ существует? у вас есть доступ пользователей к Nautobot и они сами должны проверять все существующие доступы до того как создать заявку? а если их тысячи? и объектные группы внутри других объектных групп?

Мы сознательно отказались от использования в этих политиках вложенных object-group. Реализовать их при таком подходе вроде бы не составляет труда. Но задача, точнее объект автоматизации, был не сложным и решили не городить многофункциональный комбайн :-)

Доступов далеко не тысячи, поэтому их ручная проверка (даже если пользователю лень смотреть в Nautobot и заявка дойдет до сетевого отдела) занимает минимум времени. Но я соглашусь, мысль очень здравая, обязательно в проверку добавлю поиск дублей. Спасибо

Sign up to leave a comment.

Articles