Nornir 네트워크 자동화 -2 (활용편)

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) 해 보면, 이므로, 이제 문자열에서 uptime을 찾아 잘라내는 스크립트를 만들면 된다.

이 시점에서 스위치를 점검하기 위한 명령어가 여러 개라는 문제가 발생한다. 보통 시스코 스위치 상태를 점검하려면 몇가지 명령어를 사용해야 하므로, 이 전에 다루었던(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

  1. ㄴㅇㄹㄴㅇㄹ

    1. ???

답글 남기기

Your email address will not be published.