Nornir 네트워크 자동화 – 2 활용편
목표: Nornir를 이용하여 시스코 스위치 점검하는 파이썬 스크립트를 작성
사람이 장비점검을 한다고 하면 보통 아래 과정을 거쳐야한다.
1. 스위치의 목록 확인(ID, 비밀번호, 접속방법 등등)
2. 장비 접속, 보통 순서대로 접속하지만, 터미널 프로그램에 등록해서 한번에 여러대 접속이 가능하다.
3. 장비별로 명령을 내린다. 이것 역시 순서대로 하거나 여러 장비에 동시 가능.
4. 명령의 실행 결과를 확인하고 체크리스트를 작성한다.(이부분에서 사람은 동시에 하기 어려움)
이것을 자동화한다면,
1. 장비 목록에서 hosts.yaml 작성. 또는 파이썬 딕셔너리 타입의 hosts 데이타를 작성.
2. Nornir netmiko_task로 장비에 명령을 내리고 실행 결과를 받음.
3. 실행결과에서 원하는 결과를 추출하고 체크리스트를 작성한다.
인벤토리는 기본적으로 hosts 정보만으로 Nornir를 초기화하고 실행할 수 있다. 따라서 이전 글에서 살펴본 hosts의 스키마를 기본으로 아래 항목으로 excel 파일을 작성하고, 명령을 실행한 후 결과를 excel 파일에 저장하도록 스크립트를 작성해 보도록 한다.
* 장비리스트
장비리스트는 위 그림과 같은 항목으로 엑셀 파일을 작성한다. 시트의 이름은 hosts 로 한다.
각 항목은, id, hostname, username, password, platform, port, vendors, role, site 로 구성되며, 다음 yaml 포맷처럼 변환할 것이다.
3: data: site: lan device_type: l3_switch vendors: cisco hostname: 192.168.0.2 password: user_password platform: cisco_ios_telnet port: 23 username: user_id 4: data: site: phone device_type: l2_switch vendors: cisco hostname: 192.168.0.5 password: user_password platform: cisco_ios port: 22 username: user_id
id는 중복되지 않도록 하면되는데, 여기서는 가장 단순하게 숫자를 사용한다.
엑셀파일을 다루기 위해서 openpyxl 패키지를 설치한다.
(venv) CiscoPM_new>pip install openpyxl
* 호스트 파일 읽기
cisco_pm.py 파일을 만들고, 엑셀 파일에서 데이타를 읽어 딕셔너리 데이타를 만드는 get_hosts_file 함수를 만든다.
from pprint import pprint from openpyxl import load_workbook def get_hosts_file(excel_file): sheet = 'hosts' book = load_workbook(excel_file) hosts = dict() for row in sheet.rows: key = row[0].value if key: hosts[key] = { 'hostname': row[1].value.strip(), 'username': row[2].value.strip(), 'password': row[3].value.strip(), 'platform': row[4].value.strip(), 'port': row[5].value, 'data': { 'vendors': row[6].value.strip(), 'role': row[7].value.strip(), 'site': row[8].value.strip(), }, } # remove first row of excel sheet. if hosts['id']: del hosts['id'] return hosts if __name__ == '__main__': hosts = get_hosts_file('cisco.xlsx') pprint(hosts)
이것을 실행하면 아래와 같은 딕셔너리 타입의 데이타를 볼 수 있다. pprint
... 3: {'data': {'role': 'l', 'site': 'lan', 'vendors': 'cisco'}, 'hostname': '192.168.0.2', 'password': 'user_password', 'platform': 'cisco_ios_telnet', 'port': 23, 'username': 'user_id'}, 4: {'data': {'role': 'l2_switch', 'site': 'phone', 'vendors': 'cisco'}, 'hostname': '192.168.0.5', 'password': 'user_password', 'platform': 'cisco_ios', 'port': 22, 'username': 'user_id'}} ...
이것을 yaml 포맷의 파일로 저장하여 SimpleInventory 플러그인을 사용하는 방법도 있고, 딕셔너리를 그대로 사용할 수 있는 DictInventory 플러그인을 사용해도 된다.
SimpleInventory를 사용하기 위해 위의 데이타를 yaml 파일로 저장하도록 한다. yaml 파일 작성을 위해서, PyYAML 패키지를 설치한다.
(venv) CiscoPM_new>pip install pyyaml
* hosts.yaml 파일 생성
딕셔너리 데이타를 yaml 파일로 저장하기 위한 hosts_to_yaml 함수를 작성한다. 이 함수가 실행되면 hosts 데이타가 hosts.yaml 파일로 저장된다.
import yaml import json ... def hosts_to_yaml(hosts): json_data = json.dumps(hosts) yaml_data = yaml.load(json_data, Loader=yaml.SafeLoader) f = open('hosts.yaml', 'w') f.write(yaml.dump(yaml_data, default_flow_style=False)) f.close() if __name__ == '__main__': hosts = get_hosts_file('cisco.xlsx') hosts_to_yaml(hosts)
현재 디렉토리에 hosts.yaml 파일이 만들어 졌으며, 이는 위에서 본 yaml 형식과 동일할 것이다.
* InitNornir
이제, nornir를 사용하기위해 패키지를 설치한다.
(venv) CiscoPM_new>pip install nornir
cisco 스위치를 점검하기위한 ios_pm 함수를 작성한다. 먼저 첫번째 단계로 위에서 만든 hosts.yaml 로 nornir를 초기화하는 코드를 작성한다.
from nornir import InitNornir def ios_pm(): nr = InitNornir( runner={ 'plugin': 'threaded', 'options': { 'num_workers': 30, }, }, inventory={ 'plugin': 'SimpleInventory', 'options': { 'host_file': 'hosts.yaml', } } ) print(nr.inventory.hosts) if __name__ == '__main__': hosts = get_hosts_file('cisco.xlsx') hosts_to_yaml(hosts) ios_pm()
task를 작성하기전에 초기화가 잘 되는지 확인해 본다. 결과가 아래처럼 나오면 코드가 잘 작성된 것이다.
{'1': Host: 1, '10': Host: 10, '100': Host: 100, '101': Host: 101, '102': Host: 102, '103': Host: 103, '104': Host: 104, '105': Host: 105, '106': Host: 106, '107': Host: 107, '108': Host: 108, '109': Host: 109, '11': Host: 11, '110': Host: 110, '111': Host: 111, '112': Host: 112, '113': Host: 113, '114': Host: 114, '115': Host : 115, '116': Host: 116, '117': Host: 117, '118': Host: 118, '12': Host: 12, '13': Host: 13, '14': Host: 14, '15': Host: 15, '16': Host: 16, '17': Host: 17, '18': H ost: 18, '19': Host: 19, ... }
* nornir-netmiko
Nornir를 이용해 네트워크 스위치를 점검하기위해서 필요한 명령을 실행하는데, 이것은 netmiko 플러그인을 사용하면 된다. netmiko 플러그인은 아래 명령으로 설치할 수 있다.
(venv) CiscoPM_new>pip install nornir-netmiko
netmiko_send_command는 nornir task로 스위치에 하나의 명령을 내리고 결과를 받을 수 있다. 사용법은 매우 간단하다.
위에서 작성한 ios_pm 함수에 아래 코드를 추가한다.
from nornir_netmiko import netmiko_send_command ... data = nr.run( task=netmiko_send_command, command_string = 'show hardware', name='HW_info' ) print(data)
이 코드는 초기화된 nornir 호스트들에 show hardware 명령을 실행하고 결과를 받아온다. 코드를 실행하면 아래와 같은 결과가 보인다.
AggregatedResult (HW_info): {'1': MultiResult: [Result: "HW_info"], '10': MultiResult: [Result: "HW_info"], '100': MultiResult: [Result: "HW_info"], '101': MultiRes ult: [Result: "HW_info"], '102': MultiResult: [Result: "HW_info"] ...}
결과는 딕셔너리와 유사한 object에 저장이 되는데, 이것을 단순히 화면으로 확인하려면, print_result를 이용하면된다. print(data)를 print_result(data)로 바꾸고 실행해본다.
하지만, print_result를 사용하려면 nornir-utils패키지를 설치해야한다.
(venv) CiscoPM_new>pip install nornir-utils
이제 아래 내용을 추가해 둔다.
from nornir_utils.plugins.functions import print_result ... data = nr.run( task=netmiko_send_command, command_string = 'show hardware', name='HW_info' ) print_data(data)
실행 결과는 아래와 비슷할 것이다.
... * 99 ** changed : False ******************************************************** vvvv HW_info ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv 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 Switch uptime is 16 weeks, 4 days, 21 hours, 15 minutes System returned to ROM by power-on System restarted at 17:52:20 KST Sun Feb 21 2021 System image file is "flash:c3550-ipbase-mz.122-44.SE6.bin" Cisco WS-C3550-48 (PowerPC) processor (revision N0) with 65526K/8192K bytes of memory. Processor board ID CAT0824Y2ML 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:5C:E6:90:00 Motherboard assembly number: 73-5701-10 Power supply part number: 34-0967-01 Motherboard serial number: CAT08230S4E Power supply serial number: DTH08225PRG Model revision number: N0 Motherboard revision number: A0 Model number: WS-C3550-48-SMI System serial number: CAT0824Y2ML Configuration register is 0x10F ^^^^ END HW_info ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ...
이 실행 결과에서 원하는 부분이 uptime 이라면 결과를 parsing 하면 되는데, 문제는 결과가 단순한 파이썬 데이타 타입이 아니라는 것이다. 이 오브젝트는 딕셔너리와 비슷한 구조라고 했다. 그러면 어떤 구조인지 확인해 보자. 결과를 단순하게 보기 위해서, 하나의 호스트만 필터하여 실행하도록 하자.
코드를 아래와 같이 수정해서 엑셀파일의 id 4번인 스위치에서만 실행되도록 하자. 꼭 4번이 아니어도 된다.(그냥 찍었음.)
nr = nr.filter(name='4') data = nr.run( task=netmiko_send_command, command_string='show hardware', name='HW_info' ) print(data.keys()) print(data['4'])
실행결과는,
dict_keys(['4']) MultiResult: [Result: "HW_info"]
결과를 보면, 원하는 문자열(Switch uptime is 16 weeks, 4 days, 21 hours, 15 minutes)이 포함된 결과는 MultiResult 오브젝트에 있다.
아래 코드를 추가하자.
print(data['4'].result)
실행결과는?
dict_keys(['4']) MultiResult: [Result: "HW_info"] Cisco IOS Software, C3560 Software (C3560-IPBASEK9-M), Version 12.2(55)SE1, RELEASE SOFTWARE (fc1) Technical Support: http://www.cisco.com/techsupport .... SWITCH uptime is 16 weeks, 4 days, 22 hours, 57 minutes ... Switch Ports Model SW Version SW Image ------ ----- ----- ---------- ---------- * 1 52 WS-C3560-48TS 12.2(55)SE1 C3560-IPBASEK9-M Configuration register is 0xF
원하는 결과는 data[‘4’].result 가 가지고 있음을 확인 할 수 있다. 이 결과에서 type(data[‘4’].result) 해 보면,
이 시점에서 스위치를 점검하기 위한 명령어가 여러 개라는 문제가 발생한다. 보통 시스코 스위치 상태를 점검하려면 몇가지 명령어를 사용해야 하므로, 이 전에 다루었던(Nornir 네트워크 자동화 -1) 그룹화된 task를 사용하여 task를 수행해야한다.
이제, 이 시점에서 어떤 항목을 점검할지 정해 두도록 하자.
보고서를 작성할 항목은, 장비 IP 주소, 호스트네임, 모델명, OS버전, UP Time, CPU 사용율, 메모리 사용율, 파워서플라이 상태, FAN상태, 온도로 한다.
먼저, 장비 IP 주소는 hosts.yaml 파일의 IP 주소를 사용하면 되고(장비에서 읽어와도 되나 고려사항이 많으므로 간단히 인벤토리 정보를 이용하도록 하자), 나머지는 아래 명령들을 실행해야한다.
show hardware – 모델명, OS 버전, uptime.
show env all 또는 show env – 파워서플라이, 온도, Fan 상태,
show config | inc hostname – 설정된 호스트네임
show processes cpu – CPU 사용율
show processes mem – 메모리 사용율
위의 경우처럼 적어도 5개의 명령을 실행해야 한다. netmiko_send_command는 하나의 명령밖에 실행하지 못하므로, 위의 명령을 실행하는 그룹화된 task를 작성한다.
그룹화된 task를 만들기 전에, 인벤토리의 hostname(ip 주소)을 반환하는 task를 작성한다. 함수 이름은 device_ip로 한다.
아래 함수는 Task 타입의 인수 task를 받아서 Result 타입의 결과를 반환하는 함수인데, 단순히 인벤토리의 hostname을 반환하는 함수이다.
def device_ip(task: Task) -> Result: return Result( host=task.host, result=f'{task.host.hostname}' )
* Grouped Task 작성
이제 netmiko_send_command로 위의 명령어를 모두 실행하는 task를 작성한다. 이 함수의 이름은 ios_group_task 로 하자.
def ios_group_task(task: Task) -> Result: task.run( task=netmiko_send_command, command_string='show hardware', name='HW_info', ) task.run( task=netmiko_send_command, command_string='show env all', name='HW_check1', ) task.run( task=netmiko_send_command, command_string='show env', name='HW_check2', ) task.run( task=netmiko_send_command, command_string='show processes cpu', name='CPU_info', ) task.run( task=netmiko_send_command, command_string='show processes mem', name='MEM_info', ) task.run( task=netmiko_send_command, command_string='show run | inc hostname', name='Hostname', ) task.run(task=device_ip, name='device_ip') return Result(host=task.host)
이제, 이 task를 실행하기 위해 ios_pm 함수를 수정하자.
def ios_pm(): nr = InitNornir( runner={ 'plugin': 'threaded', 'options': { 'num_workers': 30, }, }, inventory={ 'plugin': 'SimpleInventory', 'options': { 'host_file': 'hosts.yaml', } } ) data = nr.run(task=ios_group_task) print_result(data)
이제 스크립트를 실행하면, 각 호스트별로 각각의 명령어가 실행된 결과를 볼 수 있다. 아래는 실행결과의 일부분이다.
... * 99 ** changed : False ******************************************************** vvvv ios_group_task ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO ---- HW_info ** changed : False ------------------------------------------------ INFO ... System serial number: CAT0824Y2ML Configuration register is 0x10F ---- HW_check1 ** changed : False ---------------------------------------------- INFO FAN is OK TEMPERATURE is OK POWER is OK RPS is NOT PRESENT ---- HW_check2 ** changed : False ---------------------------------------------- INFO % Incomplete command. ---- CPU_info ** changed : False ----------------------------------------------- INFO CPU utilization for five seconds: 1%/0%; one minute: 1%; five minutes: 1% PID Runtime(ms) Invoked uSecs 5Sec 1Min 5Min TTY Process ... 144 25532 10001 2552 0.00% 0.00% 0.00% 0 SNMP Traps 145 1504 10189961 0 0.00% 0.00% 0.00% 0 NTP ---- MEM_info ** changed : False ----------------------------------------------- INFO Processor Pool Total: 45068808 Used: 15808192 Free: 29260616 I/O Pool Total: 8388608 Used: 2981948 Free: 5406660 PID TTY Allocated Freed Holding Getbufs Retbufs Process 0 0 25955936 7149340 15639272 0 0 *Init* ... 144 0 116109520 116098892 23588 0 0 SNMP Traps 145 0 548 180 7292 0 0 NTP 18787688 Total ---- Hostname ** changed : False ----------------------------------------------- INFO hostname SWITCH ---- device_ip ** changed : False ---------------------------------------------- INFO 192.168.0.99 ^^^^ END ios_group_task ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
이제 남은것은 실행 결과로부터 원하는 부분을 추출하고, 엑셀파일에 결과를 예쁘게 출력하는 것이다. 그렇게 하기위해서는 결과를 좀 더 살펴볼 필요가 있다.
다시 코드를 수정하자.
nr = nr.filter(name='4') data = nr.run(task=ios_group_task) print(data['4'])
역시 하나의 결과만 살펴보기위해서다.
MultiResult: [Result: "ios_group_task", Result: "HW_info", Result: "HW_check1", Result: "HW_check2", Result: "CPU_info", Result: "MEM_info", Result: "Hostname", Result: "device_ip"]
이전과 다르게 결과가 좀 더 복잡해 보인다. 코드를 여러줄 써서 각각의 result를 확인해 보자.
nr = nr.filter(name='4') data = nr.run(task=ios_group_task) for i in range(0, 8): print(i) print(data['4'][i].result)
이것을 실행하면,
0 None 1 ... BOOTLDR: C3560 Boot Loader (C3560-HBOOT-M) Version 12.2(44)SE5, RELEASE SOFTWARE (fc1) SWITCH uptime is 16 weeks, 4 days, 23 hours, 55 minutes System returned to ROM by power-on System restarted at 17:00:35 KST Sun Feb 21 2021 ... Configuration register is 0xF 2 FAN is OK TEMPERATURE is OK SW PID Serial# Status Sys Pwr PoE Pwr Watts -- ------------------ ---------- --------------- ------- ------- ----- 1 Built-in Good SW Status RPS Name RPS Serial# RPS Port# -- ------------- ---------------- ----------- --------- 1 Not Present <> 3 % Incomplete command. 4 CPU utilization for five seconds: 5%/0%; one minute: 7%; five minutes: 7% PID Runtime(ms) Invoked uSecs 5Sec 1Min 5Min TTY Process ... 5 Processor Pool Total: 78260728 Used: 18877908 Free: 59382820 I/O Pool Total: 8388608 Used: 3597840 Free: 4790768 Driver te Pool Total: 1048576 Used: 40 Free: 1048536 PID TTY Allocated Freed Holding Getbufs Retbufs Process ... 22474232 Total 6 hostname SWITCH
이제, 장비가 많아지면 결과 추출이 좀 더 복잡해 진다.
다음 편에서는 결과를 파싱하여 보고서를 완성해 보도록 한다.
2 comments
ㄴㅇㄹㄴㅇㄹ
Author
???