Tóm tắt: Nornir là framework Python (không phải DSL như Ansible): đọc inventory → nr.filter(...) chọn host → nr.run(task=...) chạy song song bằng thread → nhận AggregatedResult (dict host → list Result). Kết nối thiết bị nằm ở plugin riêng: nornir_netmiko, nornir_napalm, nornir_scrapli. In kết quả bằng print_result của nornir_utils.
Luồng hoạt động
flowchart LR A["config.yaml"] --> B["InitNornir()"] B --> C["SimpleInventory: hosts/groups/defaults"] C --> D["nr.filter(F(...))"] D --> E["nr.run(task)"] E -->|"thread pool"| F[("Router 1..N")] F --> G["AggregatedResult"] G --> H["print_result / failed_hosts"]
Cấu hình
Cài đặt
python3 -m pip install nornir nornir-utils nornir-netmiko nornir-napalmInventory
inventory:
plugin: SimpleInventory
options:
host_file: inventory/hosts.yaml
group_file: inventory/groups.yaml
defaults_file: inventory/defaults.yaml
runner:
plugin: threaded
options:
num_workers: 20
logging:
log_file: nornir.logpe1-hni:
hostname: 198.51.100.11
groups: [iosxr]
data: { site: hni, role: pe }
mx1-hcm:
hostname: 198.51.100.21
groups: [junos]
data: { site: hcm, role: pe }iosxr:
platform: iosxr # napalm driver name
connection_options:
netmiko:
platform: cisco_xr # netmiko device_type
junos:
platform: junos
connection_options:
netmiko:
platform: juniper_junos
napalm:
extras:
optional_args: { port: 830 }platform ở host/group được plugin dùng làm tên driver. Netmiko và NAPALM đặt tên khác nhau (cisco_xr vs iosxr), nên ghi đè trong connection_options.<plugin>.platform.
Không ghi mật khẩu vào defaults.yaml, nạp từ biến môi trường:
import os
from nornir import InitNornir
from nornir.core.filter import F
from nornir_netmiko.tasks import netmiko_send_command
from nornir_utils.plugins.functions import print_result
nr = InitNornir(config_file="config.yaml")
nr.inventory.defaults.username = os.environ["NET_USER"]
nr.inventory.defaults.password = os.environ["NET_PASS"]
pe_hni = nr.filter(F(role="pe") & F(site="hni"))
res = pe_hni.run(task=netmiko_send_command, command_string="show version")
print_result(res)Filter
| Mục đích | Cú pháp |
|---|---|
| Theo field/data đơn giản | nr.filter(site="hni", role="pe") |
| Thuộc group | nr.filter(F(groups__contains="iosxr")) |
| AND / OR / NOT | F(a=1) & F(b=2), F(a=1) | F(a=2), ~F(role="pe") |
| Key lồng trong data (dict) | F(mgmt__vrf="MGMT") (data mgmt: {vrf: MGMT}) |
| Hàm tuỳ ý | nr.filter(filter_func=lambda h: h.name.startswith("pe")) |
| Xem host sau filter | print(list(pe_hni.inventory.hosts)) |
Task tự viết, gom nhiều bước
from nornir.core.task import Task, Result
from nornir_netmiko.tasks import netmiko_send_command
from nornir_napalm.plugins.tasks import napalm_get, napalm_configure
def check_bgp(task: Task) -> Result:
# Sub-task: kết quả con nằm trong MultiResult của host
r = task.run(task=napalm_get, getters=["bgp_neighbors"])
peers = r.result["bgp_neighbors"]["global"]["peers"]
down = [ip for ip, p in peers.items() if not p["is_up"]]
return Result(host=task.host, result=down, failed=bool(down))
def push_desc(task: Task, dry_run: bool = True) -> Result:
cfg = task.host["desc_cfg"] # lấy từ data của host
r = task.run(task=napalm_configure, configuration=cfg, dry_run=dry_run)
return Result(host=task.host, result=r.result, changed=r.changed, diff=r.diff)res = nr.run(task=check_bgp, name="BGP down?")
res = nr.filter(site="hni").run(task=push_desc, dry_run=True) # chỉ xem diffXử lý kết quả
| Mục đích | Cú pháp |
|---|---|
| Có host nào lỗi không | res.failed |
| Danh sách host lỗi | res.failed_hosts (dict host → MultiResult) |
| Kết quả task chính của 1 host | res["pe1-hni"][0].result |
| Kết quả sub-task thứ 2 | res["pe1-hni"][1].result |
| Exception gốc | res["pe1-hni"][0].exception |
| Diff (napalm_configure) | res["pe1-hni"][0].diff |
| In gọn, chỉ host lỗi | print_result(res.failed_hosts) hoặc lặp res.items() |
| Chạy lại trên host lỗi | nr.run(task=..., on_good=False, on_failed=True) |
| Xoá danh sách host lỗi đã ghi nhận | nr.data.reset_failed_hosts() |
for host, multi in res.items():
status = "FAIL" if multi.failed else "OK"
print(f"{host:12} {status:4} {multi[0].result}")Host đã lỗi ở một nr.run sẽ bị bỏ qua ở các lần run sau (mặc định on_failed=False). Muốn chạy tiếp trên mọi host thì reset hoặc truyền on_failed=True.
Ví dụ đẩy config bằng netmiko và commit
from nornir_netmiko.tasks import netmiko_send_config, netmiko_commit
def xr_desc(task: Task) -> Result:
task.run(task=netmiko_send_config,
config_commands=["interface GigabitEthernet0/0/0/1",
"description to-pe2-hcm"],
exit_config_mode=False)
task.run(task=netmiko_commit, comment="CHG-1234")
return Result(host=task.host, result="committed", changed=True)
nr.filter(F(groups__contains="iosxr")).run(task=xr_desc)Muốn diff/commit confirmed chuẩn hơn thì dùng napalm_configure (Junos hỗ trợ revert_in), xem NAPALM cheatsheet.
Lỗi thường gặp
| Triệu chứng | Nguyên nhân | Cách xử lý |
|---|---|---|
PluginNotRegistered |
Chưa cài plugin (nornir-utils, nornir-netmiko...) hoặc sai tên plugin inventory/runner |
pip list | grep nornir, kiểm tra plugin: trong config |
Netmiko báo Unsupported 'device_type' |
Dùng platform: iosxr cho netmiko |
Đặt connection_options.netmiko.platform: cisco_xr |
Host không chạy ở lần run thứ hai |
Host nằm trong failed_hosts từ lần trước |
nr.data.reset_failed_hosts() hoặc on_failed=True |
| Kết quả rỗng, không lỗi | Filter không khớp host nào | In list(nr_f.inventory.hosts) trước khi run |
| Treo khi chạy nhiều host | num_workers lớn, AAA/TACACS giới hạn session |
Giảm num_workers (10-20) |
KeyError khi task.host["x"] |
Thiếu key trong data host/group/defaults | Dùng task.host.get("x") hoặc bổ sung inventory |
Tham khảo
- Nornir documentation
- nornir_netmiko
- nornir_napalm
- So sánh Netmiko và Scrapli: Netmiko và Scrapli