NetPalm, un cocktail pour le NetDevOps

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.

Architecture NetPalm
Architecture NetPalm

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.

NetPalm pinned and fifo workers strategy
Stratégies de gestion des files d’attentes des workers

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.

Déploiement simple NetPalm de base
Déploiement NetPalm de base

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
Déploiement simple Docker NetPalm
Déploiement de base Docker NetPalm

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
Déploiement avancé Docker NetPalm
Mise à l’échelle du déploiement Docker NetPalm

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.

Déploiement avancé NetPalm
Déploiement avancé NetPalm

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.

Documentation NetPalm Postman
Documentation NetPalm Postman
Documentation NetPalm OpenAPI
Documentation NetPalm OpenAPI

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.

Dashboard admin NetPalm
Exemple de Frontend d’administration NetPalm