Tự động tạo và điền các thành phần cấu hình thiết bị mạng bằng Nornir

Tự động tạo và điền các thành phần cấu hình thiết bị mạng bằng Nornir

Này Habr!

Gần đây có một bài viết xuất hiện ở đây Mikrotik và Linux. Thường xuyên và tự động hóa nơi một vấn đề tương tự đã được giải quyết bằng cách sử dụng phương tiện hóa thạch. Và mặc dù nhiệm vụ này hoàn toàn điển hình nhưng không có gì tương tự ở Habré. Tôi dám tặng chiếc xe đạp của mình cho cộng đồng CNTT đáng kính.

Đây không phải là chiếc xe đạp đầu tiên thực hiện nhiệm vụ như vậy. Tùy chọn đầu tiên đã được triển khai cách đây vài năm vào năm ansible phiên bản 1.x.x. Xe đạp ít được sử dụng nên thường xuyên bị rỉ sét. Theo nghĩa là bản thân nhiệm vụ không phát sinh thường xuyên khi các phiên bản được cập nhật ansible. Và mỗi khi bạn cần lái xe, xích lại rơi ra hoặc bánh xe bị rơi ra. Tuy nhiên, phần đầu tiên tạo config luôn hoạt động rất rõ ràng, thật may mắn jinja2 Động cơ đã được thiết lập từ lâu. Nhưng phần thứ hai - tung ra cấu hình - thường mang đến những điều bất ngờ. Và vì tôi phải triển khai cấu hình từ xa cho nửa trăm thiết bị, một số thiết bị ở cách xa hàng nghìn km nên việc sử dụng công cụ này hơi nhàm chán.

Ở đây tôi phải thừa nhận rằng sự không chắc chắn của tôi rất có thể nằm ở việc tôi chưa quen với ansiblehơn những thiếu sót của nó. Và nhân tiện, đây là một điểm quan trọng. ansible là một lĩnh vực kiến ​​thức hoàn toàn riêng biệt, có DSL (Ngôn ngữ cụ thể miền) riêng, phải được duy trì ở mức độ tin cậy. Chà, khoảnh khắc đó ansible Nó đang phát triển khá nhanh và không có sự quan tâm đặc biệt đến khả năng tương thích ngược nên nó không tạo thêm sự tự tin.

Vì vậy, cách đây không lâu, phiên bản thứ hai của xe đạp đã được triển khai. Lần này trên mãng xà, hay đúng hơn là trên một khung được viết bằng mãng xà và vì mãng xà được gọi là Nornir

Vì thế - Nornir là một microframework được viết bằng mãng xà và vì mãng xà và được thiết kế để tự động hóa. Tương tự như trường hợp với ansible, để giải quyết vấn đề ở đây, cần phải chuẩn bị dữ liệu có thẩm quyền, tức là. kiểm kê các máy chủ và các tham số của chúng, nhưng các tập lệnh được viết không phải bằng một DSL riêng biệt mà bằng p[i|i]ton không cũ lắm nhưng rất tốt.

Hãy xem nó đang làm gì bằng ví dụ trực tiếp sau đây.

Tôi có mạng lưới chi nhánh với vài chục văn phòng khắp cả nước. Mỗi văn phòng có một bộ định tuyến WAN kết cuối một số kênh liên lạc từ các nhà khai thác khác nhau. Giao thức định tuyến là BGP. Bộ định tuyến WAN có hai loại: Cisco ISG hoặc Juniper SRX.

Bây giờ, nhiệm vụ: bạn cần định cấu hình mạng con chuyên dụng cho Giám sát video trên một cổng riêng trên tất cả các bộ định tuyến WAN của mạng nhánh - quảng cáo mạng con này trong BGP - định cấu hình giới hạn tốc độ của cổng chuyên dụng.

Đầu tiên, chúng ta cần chuẩn bị một số mẫu, trên cơ sở đó sẽ tạo cấu hình riêng cho Cisco và Juniper. Cũng cần chuẩn bị dữ liệu cho từng điểm và thông số kết nối, tức là. thu thập cùng một khoảng không quảng cáo

Mẫu sẵn sàng cho Cisco:

$ cat templates/ios/base.j2 
class-map match-all VIDEO_SURV
 match access-group 111

policy-map VIDEO_SURV
 class VIDEO_SURV
    police 1500000 conform-action transmit  exceed-action drop

interface {{ host.task_data.ifname }}
  description VIDEOSURV
  ip address 10.10.{{ host.task_data.ipsuffix }}.254 255.255.255.0
  service-policy input VIDEO_SURV

router bgp {{ host.task_data.asn }}
  network 10.40.{{ host.task_data.ipsuffix }}.0 mask 255.255.255.0

access-list 11 permit 10.10.{{ host.task_data.ipsuffix }}.0 0.0.0.255
access-list 111 permit ip 10.10.{{ host.task_data.ipsuffix }}.0 0.0.0.255 any

Mẫu cho Juniper:

$ cat templates/junos/base.j2 
set interfaces {{ host.task_data.ifname }} unit 0 description "Video surveillance"
set interfaces {{ host.task_data.ifname }} unit 0 family inet filter input limit-in
set interfaces {{ host.task_data.ifname }} unit 0 family inet address 10.10.{{ host.task_data.ipsuffix }}.254/24
set policy-options policy-statement export2bgp term 1 from route-filter 10.10.{{ host.task_data.ipsuffix }}.0/24 exact
set security zones security-zone WAN interfaces {{ host.task_data.ifname }}
set firewall policer policer-1m if-exceeding bandwidth-limit 1m
set firewall policer policer-1m if-exceeding burst-size-limit 187k
set firewall policer policer-1m then discard
set firewall policer policer-1.5m if-exceeding bandwidth-limit 1500000
set firewall policer policer-1.5m if-exceeding burst-size-limit 280k
set firewall policer policer-1.5m then discard
set firewall filter limit-in term 1 then policer policer-1.5m
set firewall filter limit-in term 1 then count limiter

Tất nhiên, các mẫu không tự nhiên xuất hiện. Về cơ bản, đây là những khác biệt giữa các cấu hình hoạt động trước đây và sau khi giải quyết tác vụ trên hai bộ định tuyến cụ thể của các kiểu máy khác nhau.

Từ các mẫu của chúng tôi, chúng tôi thấy rằng để giải quyết vấn đề, chúng tôi chỉ cần hai tham số cho Juniper và 3 tham số cho Cisco. họ đây rồi:

  • nếu tên
  • hậu tố ip
  • mông

Bây giờ chúng ta cần đặt các tham số này cho từng thiết bị, tức là. Làm điều tương tự hàng tồn kho.

hàng tồn kho Chúng tôi sẽ tuân thủ nghiêm ngặt các tài liệu Đang khởi tạo Nornir

nghĩa là, hãy tạo bộ khung tập tin tương tự:

.
├── config.yaml
├── inventory
│   ├── defaults.yaml
│   ├── groups.yaml
│   └── hosts.yaml

File config.yaml là file cấu hình nornir tiêu chuẩn

$ cat config.yaml 
---
core:
    num_workers: 10

inventory:
    plugin: nornir.plugins.inventory.simple.SimpleInventory
    options:
        host_file: "inventory/hosts.yaml"
        group_file: "inventory/groups.yaml"
        defaults_file: "inventory/defaults.yaml"

Chúng tôi sẽ chỉ ra các tham số chính trong tập tin máy chủ.yaml, nhóm (trong trường hợp của tôi đây là thông tin đăng nhập/mật khẩu) trong nhóm.yamlvà trong mặc định.yaml Chúng tôi sẽ không chỉ ra bất cứ điều gì, nhưng bạn cần nhập ba điểm trừ vào đó - cho biết rằng đó là khoai mỡ mặc dù tập tin trống.

Đây là giao diện của Host.yaml:

---
srx-test:
    hostname: srx-test
    groups: 
        - juniper
    data:
        task_data:
            ifname: fe-0/0/2
            ipsuffix: 111

cisco-test:
    hostname: cisco-test
    groups: 
        - cisco
    data:
        task_data:
            ifname: GigabitEthernet0/1/1
            ipsuffix: 222
            asn: 65111

Và đây là nhóm.yaml:

---
cisco:
    platform: ios
    username: admin1
    password: cisco1

juniper:
    platform: junos
    username: admin2
    password: juniper2

Đây là những gì đã xảy ra hàng tồn kho cho nhiệm vụ của chúng tôi. Trong quá trình khởi tạo, các tham số từ tệp kiểm kê được ánh xạ tới mô hình đối tượng Phần tử tồn kho.

Bên dưới spoiler là sơ đồ của mô hình InventoryElement

print(json.dumps(InventoryElement.schema(), indent=4))
{
    "title": "InventoryElement",
    "type": "object",
    "properties": {
        "hostname": {
            "title": "Hostname",
            "type": "string"
        },
        "port": {
            "title": "Port",
            "type": "integer"
        },
        "username": {
            "title": "Username",
            "type": "string"
        },
        "password": {
            "title": "Password",
            "type": "string"
        },
        "platform": {
            "title": "Platform",
            "type": "string"
        },
        "groups": {
            "title": "Groups",
            "default": [],
            "type": "array",
            "items": {
                "type": "string"
            }
        },
        "data": {
            "title": "Data",
            "default": {},
            "type": "object"
        },
        "connection_options": {
            "title": "Connection_Options",
            "default": {},
            "type": "object",
            "additionalProperties": {
                "$ref": "#/definitions/ConnectionOptions"
            }
        }
    },
    "definitions": {
        "ConnectionOptions": {
            "title": "ConnectionOptions",
            "type": "object",
            "properties": {
                "hostname": {
                    "title": "Hostname",
                    "type": "string"
                },
                "port": {
                    "title": "Port",
                    "type": "integer"
                },
                "username": {
                    "title": "Username",
                    "type": "string"
                },
                "password": {
                    "title": "Password",
                    "type": "string"
                },
                "platform": {
                    "title": "Platform",
                    "type": "string"
                },
                "extras": {
                    "title": "Extras",
                    "type": "object"
                }
            }
        }
    }
}

Mô hình này có thể trông hơi khó hiểu, đặc biệt là lúc đầu. Để tìm ra điều đó, chế độ tương tác trong con trăn.

 $ ipython3
Python 3.6.9 (default, Nov  7 2019, 10:44:02) 
Type 'copyright', 'credits' or 'license' for more information
IPython 7.1.1 -- An enhanced Interactive Python. Type '?' for help.

In [1]: from nornir import InitNornir                                                                           

In [2]: nr = InitNornir(config_file="config.yaml", dry_run=True)                                                

In [3]: nr.inventory.hosts                                                                                      
Out[3]: 
{'srx-test': Host: srx-test, 'cisco-test': Host: cisco-test}

In [4]: nr.inventory.hosts['srx-test'].data                                                                                    
Out[4]: {'task_data': {'ifname': 'fe-0/0/2', 'ipsuffix': 111}}

In [5]: nr.inventory.hosts['srx-test']['task_data']                                                     
Out[5]: {'ifname': 'fe-0/0/2', 'ipsuffix': 111}

In [6]: nr.inventory.hosts['srx-test'].platform                                                                                
Out[6]: 'junos'

Và cuối cùng, hãy chuyển sang phần kịch bản. Tôi không có gì đặc biệt để tự hào ở đây. Tôi vừa lấy một ví dụ làm sẵn từ hướng dẫn và sử dụng nó gần như không thay đổi. Kịch bản làm việc hoàn thiện trông như thế này:

from nornir import InitNornir
from nornir.plugins.tasks import networking, text
from nornir.plugins.functions.text import print_title, print_result

def config_and_deploy(task):
    # Transform inventory data to configuration via a template file
    r = task.run(task=text.template_file,
                 name="Base Configuration",
                 template="base.j2",
                 path=f"templates/{task.host.platform}")

    # Save the compiled configuration into a host variable
    task.host["config"] = r.result

    # Save the compiled configuration into a file
    with open(f"configs/{task.host.hostname}", "w") as f:
        f.write(r.result)

    # Deploy that configuration to the device using NAPALM
    task.run(task=networking.napalm_configure,
             name="Loading Configuration on the device",
             replace=False,
             configuration=task.host["config"])

nr = InitNornir(config_file="config.yaml", dry_run=True) # set dry_run=False, cross your fingers and run again

# run tasks
result = nr.run(task=config_and_deploy)
print_result(result)

Chú ý đến tham số dry_run=Đúng khởi tạo đối tượng trong dòng nr.
Ở đây giống như ở ansible một lần chạy thử đã được triển khai trong đó kết nối với bộ định tuyến được thực hiện, một cấu hình sửa đổi mới được chuẩn bị, sau đó được thiết bị xác thực (nhưng điều này không chắc chắn; nó phụ thuộc vào sự hỗ trợ của thiết bị và việc triển khai trình điều khiển trong NAPALM) , nhưng cấu hình mới không được áp dụng trực tiếp. Để sử dụng trong chiến đấu, bạn phải loại bỏ tham số Dry_run hoặc thay đổi giá trị của nó thành Sai.

Khi tập lệnh được thực thi, Nornir xuất nhật ký chi tiết ra bảng điều khiển.

Bên dưới spoiler là kết quả của quá trình chạy chiến đấu trên hai bộ định tuyến thử nghiệm:

config_and_deploy***************************************************************
* cisco-test ** changed : True *******************************************
vvvv config_and_deploy ** changed : True vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
---- Base Configuration ** changed : True ------------------------------------- INFO
class-map match-all VIDEO_SURV
 match access-group 111

policy-map VIDEO_SURV
 class VIDEO_SURV
    police 1500000 conform-action transmit  exceed-action drop

interface GigabitEthernet0/1/1
  description VIDEOSURV
  ip address 10.10.222.254 255.255.255.0
  service-policy input VIDEO_SURV

router bgp 65001
  network 10.10.222.0 mask 255.255.255.0

access-list 11 permit 10.10.222.0 0.0.0.255
access-list 111 permit ip 10.10.222.0 0.0.0.255 any
---- Loading Configuration on the device ** changed : True --------------------- INFO
+class-map match-all VIDEO_SURV
+ match access-group 111
+policy-map VIDEO_SURV
+ class VIDEO_SURV
+interface GigabitEthernet0/1/1
+  description VIDEOSURV
+  ip address 10.10.222.254 255.255.255.0
+  service-policy input VIDEO_SURV
+router bgp 65001
+  network 10.10.222.0 mask 255.255.255.0
+access-list 11 permit 10.10.222.0 0.0.0.255
+access-list 111 permit ip 10.10.222.0 0.0.0.255 any
^^^^ END config_and_deploy ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* srx-test ** changed : True *******************************************
vvvv config_and_deploy ** changed : True vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
---- Base Configuration ** changed : True ------------------------------------- INFO
set interfaces fe-0/0/2 unit 0 description "Video surveillance"
set interfaces fe-0/0/2 unit 0 family inet filter input limit-in
set interfaces fe-0/0/2 unit 0 family inet address 10.10.111.254/24
set policy-options policy-statement export2bgp term 1 from route-filter 10.10.111.0/24 exact
set security zones security-zone WAN interfaces fe-0/0/2
set firewall policer policer-1m if-exceeding bandwidth-limit 1m
set firewall policer policer-1m if-exceeding burst-size-limit 187k
set firewall policer policer-1m then discard
set firewall policer policer-1.5m if-exceeding bandwidth-limit 1500000
set firewall policer policer-1.5m if-exceeding burst-size-limit 280k
set firewall policer policer-1.5m then discard
set firewall filter limit-in term 1 then policer policer-1.5m
set firewall filter limit-in term 1 then count limiter
---- Loading Configuration on the device ** changed : True --------------------- INFO
[edit interfaces]
+   fe-0/0/2 {
+       unit 0 {
+           description "Video surveillance";
+           family inet {
+               filter {
+                   input limit-in;
+               }
+               address 10.10.111.254/24;
+           }
+       }
+   }
[edit]
+  policy-options {
+      policy-statement export2bgp {
+          term 1 {
+              from {
+                  route-filter 10.10.111.0/24 exact;
+              }
+          }
+      }
+  }
[edit security zones]
     security-zone test-vpn { ... }
+    security-zone WAN {
+        interfaces {
+            fe-0/0/2.0;
+        }
+    }
[edit]
+  firewall {
+      policer policer-1m {
+          if-exceeding {
+              bandwidth-limit 1m;
+              burst-size-limit 187k;
+          }
+          then discard;
+      }
+      policer policer-1.5m {
+          if-exceeding {
+              bandwidth-limit 1500000;
+              burst-size-limit 280k;
+          }
+          then discard;
+      }
+      filter limit-in {
+          term 1 {
+              then {
+                  policer policer-1.5m;
+                  count limiter;
+              }
+          }
+      }
+  }
^^^^ END config_and_deploy ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Ẩn mật khẩu trong ansible_vault

Ở đầu bài viết tôi đã hơi quá nhiệt tình ansible, nhưng nó không đến nỗi tệ đến thế. Tôi thực sự thích họ kho như, được thiết kế để che giấu thông tin nhạy cảm khỏi tầm mắt. Và có lẽ nhiều người đã nhận thấy rằng chúng tôi có tất cả thông tin đăng nhập/mật khẩu cho tất cả các bộ định tuyến chiến đấu ở dạng mở trong một tệp nhóm.yaml. Tất nhiên là nó không đẹp. Hãy bảo vệ dữ liệu này với kho.

Hãy chuyển các tham số từgroup.yaml sang creds.yaml và mã hóa nó bằng AES256 bằng mật khẩu 20 chữ số:

$ cd inventory
$ cat creds.yaml
---
cisco:
    username: admin1
    password: cisco1

juniper:
    username: admin2
    password: juniper2

$ pwgen 20 -N 1 > vault.passwd
ansible-vault encrypt creds.yaml --vault-password-file vault.passwd  
Encryption successful
$ cat creds.yaml 
$ANSIBLE_VAULT;1.1;AES256
39656463353437333337356361633737383464383231366233386636333965306662323534626131
3964396534396333363939373539393662623164373539620a346565373439646436356438653965
39643266333639356564663961303535353364383163633232366138643132313530346661316533
6236306435613132610a656163653065633866626639613537326233653765353661613337393839
62376662303061353963383330323164633162386336643832376263343634356230613562643533
30363436343465306638653932366166306562393061323636636163373164613630643965636361
34343936323066393763323633336366366566393236613737326530346234393735306261363239
35663430623934323632616161636330353134393435396632663530373932383532316161353963
31393434653165613432326636616636383665316465623036376631313162646435

Nó đơn giản mà. Việc còn lại là dạy chúng ta Nornir-script để truy xuất và áp dụng dữ liệu này.
Để làm điều này, trong tập lệnh của chúng tôi sau dòng khởi tạo nr = InitNornir(config_file=… thêm đoạn mã sau:

...
nr = InitNornir(config_file="config.yaml", dry_run=True) # set dry_run=False, cross your fingers and run again

# enrich Inventory with the encrypted vault data
from ansible_vault import Vault
vault_password_file="inventory/vault.passwd"
vault_file="inventory/creds.yaml"
with open(vault_password_file, "r") as fp:
    password = fp.readline().strip()   
    vault = Vault(password)
    vaultdata = vault.load(open(vault_file).read())

for a in nr.inventory.hosts.keys():
    item = nr.inventory.hosts[a]
    item.username = vaultdata[item.groups[0]]['username']
    item.password = vaultdata[item.groups[0]]['password']
    #print("hostname={}, username={}, password={}n".format(item.hostname, item.username, item.password))

# run tasks
...

Tất nhiên, vault.passwd không nên đặt cạnh creds.yaml như trong ví dụ của tôi. Nhưng chơi thì ok.

Đó là tất cả cho bây giờ. Sẽ có thêm một vài bài viết về Cisco + Zabbix, nhưng đây không phải là một chút về tự động hóa. Và sắp tới tôi dự định viết về RESTCONF trên Cisco.

Nguồn: www.habr.com

Thêm một lời nhận xét