Nornir 네트워크 자동화 – 1 (기초편)
1. Nornir란 무엇인가?
Nornir는 파이썬으로 작성된 자동화 프레임워크다. Ansible의 경우 파이썬으로 작성되었지만 프로그래밍을 몰라도 사용하는데 문제는 없지만, Nornir를 사용하려면 파이썬 프로그래밍을 알아야한다.
Nornir는 파이썬 버전 3.6.2 이상에서 동작한다.
2. Nornir 설치
Nornir는 PyPI에 게시되며, 다른 파이썬 패키지들처럼 pip 명령으로 설치가 가능하다. 또한 개발단계에서는 virtual environment를 사용하는것이 좋다.
아래 명령으로 설치한다.
(venv) $ pip install nornir
설치후 파이썬 쉘에서 Nornir 시험해본다.
$ python Python 3.8.5 (default, Jan 27 2021, 15:41:15) [GCC 9.3.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> from nornir import InitNornir >>>
오류가 발생하지 않는다면 잘 설치된 것이다.
3. Nornir 초기화
InitNornir 함수를 이용하여 Nornir를 초기화 한다. 초기화에는 설정파일을 이용할 수도 있고, 파이썬 코드를 사용할 수도 있으며 두가지를 조합하여 사용할 수도 있다.
* 설정파일 작성 – 설정파일은 yaml 포맷으로 작성한다. 여기에서는 설정파일 이름을 config.yaml로 했다.(다른 이름을 사용해도 된다.)
아래와 같이 config.yaml 파일을 작성한다.
$ vi config.yaml --- inventory: plugin: SimpleInventory options: host_file: 'hosts.yaml' group_file: 'groups.yaml' defaults_file: 'defaults.yaml' runner: plugin: threaded options: num_workers: 10
* 설정파일로 Nornir 초기화하기.
아래는 config.yaml 파일로 Nornir를 초기화한후에 worker 개수를 확인하는 파이썬 코드이다.
from nornir import InitNornir nr = InitNornir(config_file='config.yaml') nr.config.runner.options['num_workers']
위 코드를 파이썬 쉘에서 실행하면 10 이 보인다.
* 설정파일 없이 초기화하기(파이썬 코드작성)
nr = InitNornir( runner={ 'plugin': 'threaded', 'options': { 'num_workers': 10, } }, inventory={ 'plugin': 'SimpleInventory', 'options': { 'host_file': 'hosts.yaml', 'group_file': 'groups.yaml', } } ) nr.config.runner.options['num_workers']
파이썬 쉘에서 실행한 결과는 동일하다.
* 위의 두 가지를 조합해서 사용하는 방법
nr = InitNornir( config_file='config.yaml', runner={ 'plugin': 'threaded', 'options': { 'num_workers': 40, } }, ) nr.config.runner.options['num_workers']
위 코드에서는 설정파일의 num_workers는 10이나 InitNornir 파라메터로 숫자를 40으로 변경했으므로, 실행결과는 40 이 된다.
4. 인벤토리
인벤토리는 hosts, groups, defaults로 구성된다. 여기에서는 SimpleInventory 플러그인을 사용한다.
SimpleInventory 플러그인은 세개의 파일(hosts.yaml, groups.yaml, defaults.yaml)에 관련 데이타를 저장한다.
* hosts 파일: hosts 파일은 가장 바깥쪽(왼쪽)의 키가 host 이름과 개체에 연결된 구조로, 아래 코드를 실행하면 host 개체의 스키마를 볼 수 있다.
from nornir.core.inventory import Host, Group import json print(json.dumps(Host.schema(), indent=4))
실행 결과는,
{ "name": "str", "connection_options": { "$connection_type": { "extras": { "$key": "$value" }, "hostname": "str", "port": "int", "username": "str", "password": "str", "platform": "str" } }, "groups": [ "$group_name" ], "data": { "$key": "$value" }, "hostname": "str", "port": "int", "username": "str", "password": "str", "platform": "str" }
위의 스키마를 바탕으로 그림의 리스트를 hosts.yaml 파일을 만들면 아래와 비슷한 구조가 된다.
--- backbone_1: hostname: 172.16.237.2 port: 22 username: admin password: 'passwd1' platform: nxos data: site: HQ device_type: network_device role: l3_switch vendors: cisco backbone_2: hostname: 172.16.237.3 port: 22 username: admin password: 'passwd1' platform: nxos data: site: HQ device_type: network_device role: l3_switch vendors: cisco Library: hostname: 172.16.250.1 port: 23 username: root password: 'passwd2' platform: cisco_ios_telnet data: site: HQ device_type: network_device role: l3_switch vendors: cisco Library_1: hostname: 172.16.251.6 port: 23 username: root password: 'passwd2' platform: cisco_ios_telnet data: site: HQ device_type: network_device role: l2_switch vendors: cisco server_farm: hostname: 172.16.237.5 port: 22 username: root password: 'passwd2' platform: cisco_ios data: site: HQ device_type: network_device role: l2_switch vendors: cisco office: hostname: 172.16.237.6 port: 22 username: admin password: 'passwd3' platform: extreme_exos data: site: HQ device_type: network_device role: l2_switch vendors: extreme head_office: hostname: 172.16.245.1 port: 22 username: admin password: 'passwd3' platform: extreme_exos data: site: WIFI device_type: network_device role: poe_switch vendors: extreme branch_office: hostname: 172.16.245.2 port: 23 username: admin password: 'passwd3' platform: extreme_exos_telnet data: site: WIFI device_type: network_device role: poe_switch vendors: extreme C1_Main: hostname: 172.17.232.246 port: 2511 username: admin password: 'passwd4' platform: extreme_exos_telnet data: site: BRANCH device_type: network_device role: l3_switch vendors: extreme C1_1F_1: hostname: 172.16.243.2 port: 2511 username: admin password: 'passwd4' platform: extreme_exos_telnet data: site: BRANCH device_type: network_device role: l2_switch vendors: extreme CCTV_C1: hostname: 172.17.233.3 port: 23 username: admin password: 'passwd5' platform: extreme_exos_telnet site: CCTV device_type: network_device role: poe_switch vendors: extreme CCTV_M2: hostname: 172.17.233.8 port: 22 username: admin password: 'passwd5' platform: extreme_exos site: CCTV device_type: network_device role: poe_switch vendors: extreme
* groups 파일: groups 파일은 hosts 파일과 동일한 구조를 가진다. 위에서 작성한 hosts파일에서 중복되는 부분을 group으로 만들 수 있다.
backbone_1, backbone_2 항복의 중복되는 부분을 Nexus 그룹으로 만들수 있는데, hosts파일과 group파일은 아래처럽 보일 것이다.
hosts.yaml
--- backbone_1: hostname: 172.16.237.2 groups: - Nexus backbone_2: hostname: 172.16.237.3 groups: - Nexus ...
groups.yaml
--- Nexus: port: 22 username: admin password: 'adminpasswd' platform: nxos data: site: HQ device_type: network_device role: l3_switch vendors: cisco ...
* defaults 파일: defaults파일은 host 파일과 동일한 스키마를 가지나 개별 요소를 나타내는 외부 키가 없다.
--- defaults: data: domain: boxcorea.com username: admin password: 'defaultpasswd'
* 인벤토리 접근하기.
인벤토리는 inventory 속성으로 접근할 수 있다.
from nornir import InitNornir nr = InitNornir(config_file='config.yaml') print(nr.inventory.hosts)
위 코드를 실행하면, 아래와 같은 결과를 볼 수 있다.
{'backbone_1': Host: backbone_1, 'backbone_2': Host: backbone_2, 'Library': Host: Library, 'Library_1': Host: Library_1, 'server_farm': Host: server_farm , 'office': Host: office, 'head_office': Host: head_office, 'branch_office': Host: branch_office, 'C1_Main': Host: C1_Main, 'C1_1F_1': Host: C1_1F_1, 'CC TV_C1': Host: CCTV_C1, 'CCTV_M2': Host: CCTV_M2}
인벤토리는 각 호스트와 그룹에 접근하는데있어 딕셔너리와 유사한 속성을 가진다.
print(nr.inventory.hosts) {'backbone_1': Host: backbone_1, 'backbone_2': Host: backbone_2, 'wifi_backbone': Host: wifi_backbone, 'wifi_switch1': Host: wifi_switch1, 'seafox1': Host: seafox1}
print(nr.inventory.hosts['backbone_1']) Host: backbone_1
hosts와 groups는 모두 아래처럼 keys() 메소드를 사용하여 접근 가능하다.
host = nr.inventory.hosts['backbone_1'] print(host.keys()) dict_keys(['site', 'device_type', 'role', 'vendors'])
* 모델의 상속
groups.yaml 파일에서 Cisco_telnet 그룹은 Cisco 그룹을 상속하여 port 만 다른 그룹을 만들 수 있다.
Cisco: port: 22 username: root password: 'root_passwd' platform: cisco_ios_telnet data: site: HQ device_type: network_device role: l3_switch vendors: cisco Cisco_telnet: groups: - Cisco port: 23
5. 인벤토리 필터
인벤토리에서 원하는 호스트를 골라내는 간단한 방법은 (key, value) 쌍을 이용하는 것이다. 아래와 같은 방법으로 WIFI에 해당하는 호스트를 골라 낼 수 있다.
host = nr.filter(site='WIFI').inventory.hosts.keys() print(host) dict_keys(['head_office', 'branch_office'])
(key, value) 쌍을 여러개 사용하는 것도 가능하며 필터를 여러번 사용하는 것도 가능하다
nr.filter(site='HQ', role='l2_switch').inventory.hosts.keys() dict_keys(['Library_1', 'server_farm', 'office'])
filter를 여러번 사용해도 된다.
nr.filter(site='HQ').filter(role='l2_switch').inventory.hosts.keys() dict_keys(['Library_1', 'server_farm', 'office'])
children_of_group 메소드를 사용할 수도 있다.
nr.inventory.children_of_group('Nexus') {Host: backbone_2, Host: backbone_1}
이 외에 함수를 만들고 filter_func 파라메터를 주어 원하는 호스트를 골라낼 수도 있고,더 많은 경우의 수를 가지고 골라내기위해 filter object F 를 사용하는 방법도 있다.
6. Task
Task는 한 호스트를 위해 어떤 기능을 구현한 재사용 가능한 코드다.
네트워크 장치에서 task는 플러그인 설치를 통해서 실행이 가능하다. task를 이해하기 위해 리눅스 host를 추가하고 만들어본다.
hosts.yaml에 아래 호스트를 추가한다.
seafox1: hostname: 192.168.100.100 username: fox password: 'private' platform: linux data: site: HQ device_type: host role: host
이제, 아래처럼 task 를 작성한다.
$ vi task.py from nornir import InitNornir from nornir_utils.plugins.functions import print_result from nornir.core.task import Task, Result nr = InitNornir() nr = nr.filter(site='HQ', role='host') def say_hello(task: Task) -> Result: return Result( host=task.host, result=f"{task.host.name} says hello world!" ) result = nr.run(task=say_hello) print_result(result)
위에서 작성한 task를 실행하면 아래와 같은 결과를 볼 수 있다.
$ python task.py say_hello*********************************************************************** * seafox1 ** changed : False *************************************************** vvvv say_hello ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO seafox1 says hello world! ^^^^ END say_hello ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
task 함수에 인수를 주어 기능을 확장할 수 있다. 위에서 만든 함수를 아래와 같이 변경하고 실행할때 인수를 준다.
def say_hello(task: Task, text: str) -> Result: return Result( host=task.host, result=f"{task.host.name} says {text}." ) result = nr.run(task=say_hello, text='Good to see you.', name='Task describe' ) print_result(result)
이제, 실행하면 아래와 같은 결과를 볼 수 있다.
$ python task.py Task describe******************************************************************* * seafox1 ** changed : False *************************************************** vvvv Task describe ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO seafox1 says Good to see you.. ^^^^ END Task describe ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
run 함수에 name 인수를 주지 않으면 함수 이름이 기본값이 된다.
7. 그룹화된 Task
네트워크 장치에서 여러 명령을 내려야 한다면 어떻게 해야할까?
netmiko_send_command 는 하나의 명령어만 처리한다.
한번에 여러 가지 명령어를 실행하고 싶다면, task를 그룹화해서 사용할 수 있다.
아래는 cisco ios 장비에서 show hardware, show env all 명령을 실행하는 task다.
import logging from nornir import InitNornir from nornir.core.task import Task, Result from nornir_utils.plugins.functions import print_result from nornir_netmiko.tasks import netmiko_send_command def ios_maintenance_task(task: Task, text: str) -> Result: task.run( name='HW Info.', task=netmiko_send_command, command_string='show hardware' ) task.run( name='Check HW', task=netmiko_send_command, command_string='show env all' ) return Result( host=task.host, result=f'{task.host}: ios device maintenance.{text}', ) if __name__ == '__main__': nr = InitNornir(config_file='config.yaml') nr = nr.filter(site='HQ', role='l2_switch', ) result = nr.run(name='test', task=ios_maintenance_task, text='Maintenance') print_result(result, severity_level=logging.DEBUG)
위에서 작성한 코드를 실행하면 아래와 같은 결과를 볼 수 있다.
test**************************************************************************** * Library_1 ** changed : False ************************************************* vvvv test ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO Library_1: ios device maintenance.Maintenance ---- HW Info. ** changed : False ----------------------------------------------- INFO Cisco IOS Software, C3550 Software (C3550-IPBASE-M), Version 12.2(44)SE6, RELEASE SOFTWARE (fc1) Copyright (c) 1986-2009 by Cisco Systems, Inc. Compiled Mon 09-Mar-09 14:33 by gereddy Image text-base: 0x00003000, data-base: 0x00D32668 ROM: Bootstrap program is C3550 boot loader lib_1F_1 uptime is 11 weeks, 4 days, 22 hours, 9 minutes System returned to ROM by power-on System restarted at 17:39:53 KST Sun Feb 21 2021 System image file is "flash:c3550-ipbase-mz.122-44.SE6.bin" Cisco WS-C3550-48 (PowerPC) processor (revision L0) with 65526K/8192K bytes of memory. Processor board ID CAT0818R0B9 Last reset from warm-reset Running Layer2/3 Switching Image Ethernet-controller 1 has 12 Fast Ethernet/IEEE 802.3 interfaces Ethernet-controller 2 has 12 Fast Ethernet/IEEE 802.3 interfaces Ethernet-controller 3 has 12 Fast Ethernet/IEEE 802.3 interfaces Ethernet-controller 4 has 12 Fast Ethernet/IEEE 802.3 interfaces Ethernet-controller 5 has 1 Gigabit Ethernet/IEEE 802.3 interface Ethernet-controller 6 has 1 Gigabit Ethernet/IEEE 802.3 interface 48 FastEthernet interfaces 2 Gigabit Ethernet interfaces The password-recovery mechanism is enabled. 384K bytes of flash-simulated NVRAM. Base ethernet MAC Address: 00:11:20:39:E5:00 Motherboard assembly number: 73-5701-09 Power supply part number: 34-0967-02 Motherboard serial number: CAT08180H18 Power supply serial number: LIT0810016J Model revision number: L0 Motherboard revision number: A0 Model number: WS-C3550-48-SMI System serial number: CAT0818R0B9 Configuration register is 0x10F ---- Check HW ** changed : False ----------------------------------------------- INFO FAN is OK TEMPERATURE is OK POWER is OK RPS is NOT PRESENT ^^^^ END test ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ * office ** changed : False **************************************************** vvvv test ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO office: ios device maintenance.Maintenance ---- HW Info. ** changed : False ----------------------------------------------- INFO %% Unrecognized command: "show hardware" ---- Check HW ** changed : False ----------------------------------------------- INFO %% Unrecognized command: "show env all" ^^^^ END test ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ * server_farm ** changed : False *********************************************** vvvv test ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO server_farm: ios device maintenance.Maintenance ---- HW Info. ** changed : False ----------------------------------------------- INFO Cisco IOS Software, IOS-XE Software, Catalyst L3 Switch Software (CAT3K_CAA-UNIVERSALK9-M), Version 03.03.04SE RELEASE SOFTWARE (fc3) Technical Support: http://www.cisco.com/techsupport Copyright (c) 1986-2014 by Cisco Systems, Inc. Compiled Fri 29-Aug-14 22:22 by prod_rel_team Cisco IOS-XE software, Copyright (c) 2005-2014 by cisco Systems, Inc. All rights reserved. Certain components of Cisco IOS-XE software are licensed under the GNU General Public License ("GPL") Version 2.0. The software code licensed under GPL Version 2.0 is free software that comes with ABSOLUTELY NO WARRANTY. You can redistribute and/or modify such GPL code under the terms of GPL Version 2.0. (http://www.gnu.org/licenses/gpl-2.0.html) For more details, see the documentation or "License Notice" file accompanying the IOS-XE software, or the applicable URL provided on the flyer accompanying the IOS-XE software. ROM: IOS-XE ROMMON BOOTLDR: CAT3K_CAA Boot Loader (CAT3K_CAA-HBOOT-M) Version 1.2, RELEASE SOFTWARE (P) System_Node uptime is 1 year, 14 weeks, 5 days, 2 hours, 53 minutes Uptime for this control processor is 1 year, 14 weeks, 5 days, 2 hours, 56 minutes System returned to ROM by reload System restarted at 12:59:59 KST Sat Feb 1 2020 System image file is "flash:packages.conf" Last reload reason: reload This product contains cryptographic features and is subject to United States and local country laws governing import, export, transfer and use. Delivery of Cisco cryptographic products does not imply third-party authority to import, export, distribute or use encryption. Importers, exporters, distributors and users are responsible for compliance with U.S. and local country laws. By using this product you agree to comply with applicable laws and regulations. If you are unable to comply with U.S. and local laws, return this product immediately. A summary of U.S. laws governing Cisco cryptographic products may be found at: http://www.cisco.com/wwl/export/crypto/tool/stqrg.html If you require further assistance please contact us by sending email to export@cisco.com. License Level: Ipbase License Type: Permanent Next reload license Level: Ipbase cisco WS-C3650-48TD (MIPS) processor with 4194304K bytes of physical memory. Processor board ID FDO1843E18T 3 Virtual Ethernet interfaces 50 Gigabit Ethernet interfaces 2 Ten Gigabit Ethernet interfaces 2048K bytes of non-volatile configuration memory. 4194304K bytes of physical memory. 250456K bytes of Crash Files at crashinfo:. 1609272K bytes of Flash at flash:. 0K bytes of Dummy USB Flash at usbflash0:. 0K bytes of at webui:. Base Ethernet MAC Address : a0:ec:f9:2e:c9:80 Motherboard Assembly Number : 73-15124-05 Motherboard Serial Number : FDO18430XG4 Model Revision Number : D0 Motherboard Revision Number : A0 Model Number : WS-C3650-48TD System Serial Number : FDO1843E18T Switch Ports Model SW Version SW Image Mode ------ ----- ----- ---------- ---------- ---- * 1 52 WS-C3650-48TD 03.03.04SE cat3k_caa-universalk9 INSTALL Configuration register is 0x102 ---- Check HW ** changed : False ----------------------------------------------- INFO Switch 1 FAN 1 is OK Switch 1 FAN 2 is OK Switch 1 FAN 3 is OK FAN PS-1 is OK FAN PS-2 is NOT PRESENT Switch 1: SYSTEM TEMPERATURE is OK SW PID Serial# Status Sys Pwr PoE Pwr Watts -- ------------------ ---------- --------------- ------- ------- ----- 1A PWR-C2-250WAC LIT18290P1Y OK Good Good 250 1B Not Present ^^^^ END test ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
8. 처리결과
처리결과를 보는 가장 간단한 방법은 nornir-util 패키지의 print_result 함수를 이용하는 것이다. 이것은 위의 코드에서 볼 수 있다.
사용방법은,
from nornir_utils.plugins.functions import print_result ... print_result(result)
일부 호스트의 결과만 보려면,
print_result(result['Library_1'])
자세한 처리 결과를 이용하는 방법은 part-2 부분에서 다를 것이다.
참고문서:
https://nornir.readthedocs.io/en/latest/tutorial/index.html
https://blogs.cisco.com/developer/nornir-python-automation-framework
https://saidvandeklundert.net/2020-12-06-nornir/