NetPalm a vu le jour en 2020 sous l’impulsion de Tony Nealon et rassemble plusieurs outils, qui ont déjà été évoqués séparément, pour construire une plateforme OpenAPI à destination des équipements réseaux. NetPalm forme une couche d’abstraction pour utiliser napalm, netmiko, ncclient ou requests afin d’accéder aux différents matériels avec la méthode la plus adaptée. L’accès à NetPalm se fait au travers d’une interface OpenAPI v3.
NetPalm utilise Docker pour apporter une touche de modernité et une capacité d’évolution de manière à encaisser une montée en charge.
Le « Pinned Worker » possède un processus et une file d’attente dédiés pour un équipement, les tâches étant synchronisées et traitées pour cet appareil. Un process est créé pour chaque nouvel équipement accédé, jusqu’à concurrence d’un nombre maximum de process par noeud. Le « FIFO Worker » possède une seule file d’attente pour l’ensemble du cluster avec des process qui cherchent à la vider le plus vite possible. Par défaut, dix process sont créés, par worker FIFO, dès le lancement.
Le déploiement le plus simple est composé de quatre conteneurs. Le contrôleur reçoit les requêtes API et, en fonction de la demande, le travail est distribué aux workers au travers du conteneur Redis.
Après avoir réalisé un clone du repo GitHub, les conteneurs sont lancés à l’aide de la commande ci-dessous :
docker-compose up -d --build
La configuration de la solution repose sur le fichier config/defaults.json
avec des valeurs qui peuvent être personnalisées par le contenu du fichier config/config.json
dont un exemple est donné ci-dessous :
{ "api_key": "2a84465a-cf38-46b2-9d86-b84Q7d57f288", "api_key_name" : "x-api-key", "cookie_domain" : "netpalm.local", "listen_port": 9000, "listen_ip":"0.0.0.0", "gunicorn_workers":3, "redis_task_ttl":500, "redis_task_timeout":500, "redis_server":"redis", "redis_port":6379, "redis_core_q":"process", "redis_fifo_q":"fifo", "redis_queue_store":"netpalm_queue_store", "pinned_process_per_node":100, "fifo_process_per_node":10, "txtfsm_index_file":"backend/plugins/ntc-templates/index", "txtfsm_template_server":"http://textfsm.nornir.tech", "custom_scripts":"backend/plugins/custom_scripts/", "jinja2_config_templates":"backend/plugins/jinja2_templates/", "python_service_templates":"backend/plugins/services/", "self_api_call_timeout":15 }
Une mise à l’échelle de NetPalm commence par la possibilité de lancer plusieurs workers pour un contrôleur avec, comme illustration, la commande ci-dessous :
docker-compose up -d --scale netpalm-controller=1 --scale netpalm-worker-pinned=2 --scale netpalm-worker-fifo=3
Il est possible d’aller encore plus loin dans la mise à l’échelle de NetPalm en lançant plusieurs contrôleurs, mais cette approche nécessite de réaliser une fonction de load-balancing pour répartir la charge sur chacun des contrôleurs. A un certain stade de mise à l’échelle, il convient d’envisager de créer un cluster Swarm ou Kubernetes.
La documentation NetPalm se présente sous la forme d’exemples de requêtes API lancées depuis Postman. Il est également possible de tester la solution depuis l’interface de documentation de FastAPI.
Lorsqu’une tâche est soumise à un worker, elle se voit attribuer un numéro d’identification qui est utilisé pour récupérer le résultat de la tâche. Le résultat reste disponible pendant 500 secondes par défaut. L’exemple qui suit effectue un get_facts Napalm sur un switch Arista vEOS-lab.
curl -X POST "http://netpalm.lan:9000/get/napalm?x-api-key=2a84465a-cf38-46b2-9d86-b84Q7d57f288" -H "accept: application/json" -H "x-api-key: 2a84465a-cf38-46b2-9d86-b84Q7d57f288" -H "Content-Type: application/json" -H "Cookie: x-api-key=2a84465a-cf38-46b2-9d86-b84Q7d57f288" -d "{\"library\":\"napalm\",\"connection_args\":{\"device_type\":\"arista_eos\",\"host\":\"spine1\",\"username\":\"admin\",\"password\":\"arista\"},\"command\":\"get_facts\",\"queue_strategy\":\"fifo\",\"cache\":{\"enabled\":true,\"ttl\":300,\"poison\":false}}"
{ "library": "napalm", "connection_args": { "device_type": "arista_eos", "host": "spine1", "username": "admin", "password": "arista" }, "command": "get_facts", "queue_strategy": "fifo", "cache": { "enabled": true, "ttl": 300, "poison": false } }
{ "status": "success", "data": { "task_id": "154ce840-711c-4b04-8aa5-341cfde2c0d2", "created_on": "2022-04-18 15:03:08.552663", "task_queue": "fifo", "task_meta": { "enqueued_at": "2022-04-18 15:03:08.552890", "started_at": null, "ended_at": null, "enqueued_elapsed_seconds": "0", "total_elapsed_seconds": "0", "assigned_worker": null }, "task_status": "queued", "task_result": null, "task_errors": [] } }
curl -X GET "http://netpalm.lan:9000/task/154ce840-711c-4b04-8aa5-341cfde2c0d2?x-api-key=2a84465a-cf38-46b2-9d86-b84Q7d57f288" -H "accept: application/json" -H "x-api-key: 2a84465a-cf38-46b2-9d86-b84Q7d57f288" -H "Cookie: x-api-key=2a84465a-cf38-46b2-9d86-b84Q7d57f288"
{ "status": "success", "data": { "task_id": "154ce840-711c-4b04-8aa5-341cfde2c0d2", "created_on": "2022-04-18 15:03:08.552663", "task_queue": "fifo", "task_meta": { "enqueued_at": "2022-04-18 15:03:08.552890", "started_at": "2022-04-18 15:03:08.582917", "ended_at": "2022-04-18 15:03:09.028790", "enqueued_elapsed_seconds": null, "total_elapsed_seconds": "0", "assigned_worker": "serene_visvesvaraya_fifo_0" }, "task_status": "finished", "task_result": { "get_facts": { "hostname": "spine1", "fqdn": "spine1", "vendor": "Arista", "model": "vEOS-lab", "serial_number": "AAFAEDD12A0869002F0A40C5A53F6F8C", "os_version": "4.26.3M-24689762.4263M", "uptime": 375715, "interface_list": [ "Ethernet1", "Ethernet2", "Ethernet3", "Ethernet4", "Ethernet5", "Ethernet6", "Ethernet7", "Ethernet8", "Management1" ] } }, "task_errors": [] } }
Dans l’exemple ci-dessous, ce sont plusieurs commandes qui sont envoyées avec Napalm.
curl -X POST "http://netpalm.lan:9000/get/napalm?x-api-key=2a84465a-cf38-46b2-9d86-b84Q7d57f288" -H "accept: application/json" -H "x-api-key: 2a84465a-cf38-46b2-9d86-b84Q7d57f288" -H "Content-Type: application/json" -H "Cookie: x-api-key=2a84465a-cf38-46b2-9d86-b84Q7d57f288" -d "{\"library\":\"napalm\",\"connection_args\":{\"device_type\":\"arista_eos\",\"host\":\"spine1\",\"username\":\"admin\",\"password\":\"arista\"},\"command\":[\"show running-config | include hostname\",\"show ip interface brief\"],\"queue_strategy\":\"fifo\",\"cache\":{\"enabled\":true,\"ttl\":300,\"poison\":false}}"
{ "library": "napalm", "connection_args": { "device_type": "arista_eos", "host": "spine1", "username": "admin", "password": "arista" }, "command": [ "show running-config | include hostname", "show ip interface brief" ], "queue_strategy": "fifo", "cache": { "enabled": true, "ttl": 300, "poison": false } }
{ "status": "success", "data": { "task_id": "ecdd94b6-bdba-43c3-a7a8-e123885d59bf", "created_on": "2022-04-18 15:35:53.537767", "task_queue": "fifo", "task_meta": { "enqueued_at": "2022-04-18 15:35:53.538009", "started_at": null, "ended_at": null, "enqueued_elapsed_seconds": "0", "total_elapsed_seconds": "0", "assigned_worker": null }, "task_status": "queued", "task_result": null, "task_errors": [] } }
curl -X GET "http://netpalm.lan:9000/task/ecdd94b6-bdba-43c3-a7a8-e123885d59bf?x-api-key=2a84465a-cf38-46b2-9d86-b84Q7d57f288" -H "accept: application/json" -H "x-api-key: 2a84465a-cf38-46b2-9d86-b84Q7d57f288" -H "Cookie: x-api-key=2a84465a-cf38-46b2-9d86-b84Q7d57f288"
{ "status": "success", "data": { "task_id": "ecdd94b6-bdba-43c3-a7a8-e123885d59bf", "created_on": "2022-04-18 15:35:53.537767", "task_queue": "fifo", "task_meta": { "enqueued_at": "2022-04-18 15:35:53.538009", "started_at": "2022-04-18 15:35:53.565082", "ended_at": "2022-04-18 15:35:54.072954", "enqueued_elapsed_seconds": null, "total_elapsed_seconds": "0", "assigned_worker": "serene_visvesvaraya_fifo_7" }, "task_status": "finished", "task_result": { "show running-config | include hostname": [ "hostname spine1", "" ], "show ip interface brief": [ " Address ", "Interface IP Address Status Protocol MTU Owner ", "--------------- ---------------------- ---------- ------------ -------- ------- ", "Management1 192.168.199.159/24 up up 1500 ", "", "" ] }, "task_errors": [] } }
L’exemple ci-après, montre une utilisation de Netmiko.
curl -X POST "http://netpalm.lan:9000/get/netmiko?x-api-key=2a84465a-cf38-46b2-9d86-b84Q7d57f288" -H "accept: application/json" -H "x-api-key: 2a84465a-cf38-46b2-9d86-b84Q7d57f288" -H "Content-Type: application/json" -H "Cookie: x-api-key=2a84465a-cf38-46b2-9d86-b84Q7d57f288" -d "{\"library\":\"netmiko\",\"connection_args\":{\"device_type\":\"arista_eos\",\"host\":\"leaf1\",\"username\":\"admin\",\"password\":\"arista\"},\"command\":\"show version\",\"args\":{\"use_textfsm\":true},\"queue_strategy\":\"pinned\"}"
{ "library": "netmiko", "connection_args": { "device_type": "arista_eos", "host": "leaf1", "username": "admin", "password": "arista" }, "command": "show version", "args": { "use_textfsm": true }, "queue_strategy": "pinned" }
{ "status": "success", "data": { "task_id": "426f1c15-8f16-44e1-ae0c-1cb5d1ca5f56", "created_on": "2022-04-18 15:50:38.071057", "task_queue": "leaf1", "task_meta": { "enqueued_at": "2022-04-18 15:50:38.071422", "started_at": null, "ended_at": null, "enqueued_elapsed_seconds": "0", "total_elapsed_seconds": "0", "assigned_worker": null }, "task_status": "queued", "task_result": null, "task_errors": [] } }
curl -X GET "http://netpalm.lan:9000/task/426f1c15-8f16-44e1-ae0c-1cb5d1ca5f56?x-api-key=2a84465a-cf38-46b2-9d86-b84Q7d57f288" -H "accept: application/json" -H "x-api-key: 2a84465a-cf38-46b2-9d86-b84Q7d57f288" -H "Cookie: x-api-key=2a84465a-cf38-46b2-9d86-b84Q7d57f288"
{ "status": "success", "data": { "task_id": "426f1c15-8f16-44e1-ae0c-1cb5d1ca5f56", "created_on": "2022-04-18 15:50:38.071057", "task_queue": "leaf1", "task_meta": { "enqueued_at": "2022-04-18 15:50:38.071422", "started_at": "2022-04-18 15:50:38.096104", "ended_at": "2022-04-18 15:50:43.362186", "enqueued_elapsed_seconds": "0", "total_elapsed_seconds": "5", "assigned_worker": "strange_wing_leaf1" }, "task_status": "finished", "task_result": { "show version": [ { "model": "vEOS-lab", "hw_version": "", "serial_number": "ABA19F0010BE9DD64D9AB828EEB0F9DB", "sys_mac": "5000.00cb.38c2", "image": "4.26.3M", "total_memory": "4002528", "free_memory": "3050920" } ] }, "task_errors": [] } }
La puissance proposée par NetPalm demanderait beaucoup de littérature pour être mise en valeur. Dans sa version actuelle (midnight oil – 14/03/2022), la solution possède les APIs suivantes :
POST | /get |
POST | /getconfig |
POST | /get/netmiko |
POST | /getconfig/netmiko |
POST | /get/napalm |
POST | /getconfig/napalm |
POST | /get/puresnmp |
POST | /getconfig/puresnmp |
POST | /get/ncclient |
POST | /getconfig/ncclient |
POST | /get/ncclient/get |
POST | /getconfig/ncclient/get |
POST | /get/restconf |
POST | /getconfig/restconf |
POST | /setconfig |
POST | /setconfig/dry-run |
POST | /setconfig/netmiko |
POST | /setconfig/napalm |
POST | /setconfig/ncclient |
POST | /setconfig/restconf |
GET | /task/{task_id} |
GET | /taskqueue/ |
GET | /taskqueue/{host} |
GET | /workers/ |
POST | /workers/kill/{name} |
GET | /containers/pinned/ |
GET | /template |
POST | /template |
DELETE | /template |
GET | /template/{tmpname} |
POST | /template/match |
GET | /ttptemplate/ |
POST | /ttptemplate/ |
DELETE | /ttptemplate/ |
GET | /ttptemplate/{tmpname} |
GET | /j2template/config/ |
POST | /j2template/config/ |
DELETE | /j2template/config/ |
GET | /j2template/config/{tmpname} |
GET | /j2template/webhook/ |
POST | /j2template/webhook/ |
DELETE | /j2template/webhook/ |
GET | /j2template/webhook/{tmpname} |
POST | /j2template/render/config/{tmpname} |
POST | /j2template/render/webhook/{tmpname} |
POST | /script/add/ |
DELETE | /script/remove/ |
GET | /script/{tmpname} |
POST | /webhook/add/ |
GET | /webhook/{tmpname} |
DELETE | /webhook/remove/ |
POST | /service/add/ |
GET | /service/{tmpname} |
DELETE | /service/remove/ |
GET | /script |
POST | /script |
POST | /script/v1/hello_world_test |
POST | /script/v1/hello_world_advanced_using_netpalm_manager |
POST | /script/v1/hello_world |
POST | /script/v1/hello_world_embedded_pydanticmodel |
GET | /webhook |
GET | /service/instances/ |
GET | /service/instance/{service_id} |
POST | /service/instance/create/example_simple |
PATCH | /service/instance/update/example_simple/{service_id} |
POST | /service/instance/delete/{service_id} |
POST | /service/instance/redeploy/{service_id} |
POST | /service/instance/validate/{service_id} |
POST | /service/instance/healthcheck/{service_id} |
GET | /logout |
GET | /worker-ping |
GET | /cache |
DELETE | /cache |
GET | /cache/{cache_key} |
DELETE | /cache/{cache_key} |
PUT | /reload-extensibles |
GET | /schedule/ |
POST | /schedule/{name} |
DELETE | /schedule/{id} |
PATCH | /schedule/{id} |
GET | /denied |
Pour des raisons de facilité, les exemples présentés ici s’appuient sur des commandes cURL. Pour une approche NetDevOps en exploitation, il conviendra de réaliser une application qui peut être écrite en langage JavaScript, en langage Python, ou même sous forme de playbooks Ansible. En réalité il n’y a aucune restriction sur le langage ou pseudo langage pouvant être utilisé du moment qu’il sait effectuer des requêtes API.