Skip to main content

routeros-client

PyPI Python CI Licence MIT

Client Python pour MikroTik RouterOS, en un seul module sans dépendance obligatoire.

Trois transports interchangeables exposant exactement la même surface d'API — votre code métier ne change pas quand vous changez de transport.

Transport Classe Port par défaut Dépendance
API binaire (défaut) Api 8728 aucune
API binaire sur TLS Api(use_ssl=True) 8729 aucune
SSH SshApi 22 paramiko (uniquement pour l'auth par mot de passe)
REST / JSON (RouterOS v7) RestApi 80 / 443 aucune

Compatible RouterOS v6.x et v7.x, Python 3.9+. Validé en conditions réelles sur RouterOS 7.23.3 (stable).


Installation

pip install routeros-client

Nom de distribution ≠ nom d'import. Le paquet s'installe sous routeros-client mais s'importe sous routeros_api — le nom routeros-api étant déjà occupé sur PyPI par un autre projet. Le nom d'import est délibérément inchangé pour ne rien casser dans les applications existantes.

import routeros_api          # ← quel que soit le nom d'installation

Pour le transport SSH avec authentification par mot de passe :

pip install "routeros-client[ssh]"       # ajoute paramiko

Sans cet extra, SSH reste utilisable par clé ou agent via le client OpenSSH du système.

Le module étant autonome, vous pouvez aussi simplement copier routeros_api.py dans votre projet.


Démarrage rapide

from routeros_api import connect

# Transport API binaire — le comportement par défaut
api = connect("192.168.88.1", "admin", "secret")

print(api.get_identity())                       # 'MikroTik'
for iface in api.print("/interface", proplist="name,type,running"):
    print(iface["name"], iface["type"], iface["running"])

api.close()

Avec fermeture garantie :

from routeros_api import api_session

with api_session("192.168.88.1", "admin", "secret") as api:
    adresses = api.get_resource("/ip/address").find(disabled=False)

Vue « menu » : ResourceProxy

fw = api.get_resource("/ip/firewall/address-list")

fw.add(list="blocked", address="203.0.113.7", comment="abus")
fw.find(list="blocked")                          # liste de dicts
fw.find_one(address="203.0.113.7")               # un dict ou None
fw.remove_where(list="blocked")                  # suppression par filtre

Idempotence

# Crée si absent, met à jour sinon. Retourne (.id, created)
item_id, cree = api.ensure(
    "/ip/firewall/address-list",
    {"list": "blocked", "address": "203.0.113.7"},   # critère de recherche
    {"comment": "mis à jour"},                       # valeurs à appliquer
)

Choisir un transport

Le transport API binaire est celui par défaut : le plus rapide, et le seul à savoir multiplexer (pipelining, listen(), cancel()).

from routeros_api import connect, ssh_session, auto_connect

api = connect(ip, "admin", pwd)                              # API binaire (défaut)
api = connect(ip, "admin", pwd, use_ssl=True)                # API sur TLS (8729)
ros = connect(ip, "admin", pwd, transport="ssh",             # SSH
              key_filename="~/.ssh/id_ed25519")

# Bascule automatique : prend l'API si elle répond, sinon SSH
ros = auto_connect(ip, "admin", pwd, order=("api", "ssh"))

Le code qui suit est identique quel que soit le transport :

with ssh_session(ip, "admin", password=pwd) as ros:
    print(ros.get_identity())
    ros.add("/ip/address", address="10.0.0.1/24", interface="ether1")
    for a in ros.get_resource("/ip/address").find():
        print(a[".id"], a["address"])

Quand utiliser SSH plutôt que l'API

  • le service API est désactivé sur l'équipement, mais SSH est ouvert ;
  • vous voulez un export de configuration : /export ne renvoie rien via l'API binaire sur RouterOS v7 (limite de RouterOS, pas de la bibliothèque), alors que SshApi.export() fonctionne ;
  • vous devez lancer des commandes hors du périmètre de l'API (scripts, /tool fetch, sauvegardes) : ros.run("/system script run mon-script") ;
  • vous voulez transférer des fichiers : ros.sftp_get() / ros.sftp_put() (paramiko requis).

Fonctionnement interne : les commandes protocolaires sont traduites en ligne de commande RouterOS, exécutées dans un canal exec (pas d'analyse d'invite), puis la sortie est re-parsée en enregistrements identiques à ceux de l'API. Trois formats sont tentés, du plus fidèle au plus tolérant : :serialize to=json (v7, .id inclus) → as-value (v6/v7) → print terse. Le mode retenu est mémorisé après la première commande.

Limites du SSH : pas de multiplexage — batch(pipeline=True) retombe en séquentiel, cancel() est sans objet, listen() est émulé par print follow. Compter ~2 à 5 ms de surcoût par commande.


Performance

Pipelining — le gain le plus important

Envoie N commandes en une seule trame et relit les réponses par tag : N allers-retours réseau deviennent 1.

with api.pipeline() as p:
    for ip_bloquee in blocklist:                 # 5 000 entrées
        p.add("/ip/firewall/address-list/add",
              "=list=blocked", f"=address={ip_bloquee}")
print(len(p.responses))

# ou directement sur les écritures en masse
api.bulk_add("/ip/firewall/address-list", items, pipeline=True)

Mesuré sur RouterOS 7.23.3 en LAN, 40 ajouts : 145 ms → 37 ms (×3,9). Le gain croît avec la latence : sur un lien WAN à 20 ms, 200 commandes passent d'environ 4 s à 0,1 s.

À ne pas utiliser pour des commandes qui coupent la session (/system/reboot) ni pour des séquences dont l'ordre d'exécution importe : RouterOS traite les commandes taguées concurremment.

Autres leviers

# Réduire le volume transféré : le routeur n'envoie que les champs demandés
api.print("/ip/dhcp-server/lease", proplist=".id,address,mac-address")

# Mémoire constante sur les très grosses tables (aucune matérialisation)
for bail in api.iter_print("/ip/dhcp-server/lease", proplist="address,mac-address"):
    traiter(bail)

TCP_NODELAY est activé par défaut (jusqu'à ~40 ms de latence évités par commande) et la lecture du tampon est en O(n) au lieu de O(n²).

paramiko est importé paresseusement, à la première connexion SSH : import routeros_api coûte 475 ms au lieu de 982 ms quand paramiko est installé mais que vous n'utilisez que l'API.


Points d'attention en production

api-ssl (8729) sans certificat

Sans certificat configuré — le cas par défaut — RouterOS ne propose que des suites anonymes (ADH-AES256-SHA256), que Python refuse : le handshake échoue. La bibliothèque les réactive automatiquement quand ssl_verify=False :

api = connect(ip, "admin", pwd, use_ssl=True, ssl_verify=False)   # fonctionne

Le chiffrement reste actif mais le pair n'est pas authentifié. Pour une vraie sécurité, installez un certificat sur le routeur et gardez ssl_verify=True.

Clé d'hôte SSH

strict_host_key=True par défaut : la clé du routeur doit être connue (known_hosts). Pour un premier contact en laboratoire, strict_host_key=False désactive la vérification — un avertissement est journalisé, et vous devenez vulnérable à une attaque active.

Erreurs

from routeros_api import RouterOSCommandError, RouterOSConnectionError, RouterOSAuthError

try:
    api.add("/ip/address", address="pas-une-ip")
except RouterOSCommandError as exc:
    print(exc, exc.category)

# Variante sans exception
resp = api.talk("/ip/address/print", raise_on_trap=False)
if not resp.ok:
    print(resp.error_message)

Robustesse

api = connect(ip, "admin", pwd,
              timeout=10,              # timeout socket
              auto_reconnect=True,     # reconnexion transparente
              enable_resilience=True,  # retry avec backoff sur erreur réseau
              rate_limit=20)           # 20 commandes/s maximum

enable_resilience rejoue la commande après une coupure. Pour les commandes non idempotentes (add), préférez ensure().


Développement et tests

python -m venv .venv
.venv/bin/activate                       # Windows : .\.venv\Scripts\Activate.ps1
pip install -e ".[dev]"

# Hors-ligne — 90 tests, aucun routeur nécessaire
python -m unittest test_routeros_api -v

# Contre un routeur réel — 62 tests (ÉCRIT sur l'équipement : laboratoire uniquement)
ROS_HOST=192.168.88.1 ROS_USER=admin ROS_PASSWORD=secret python test_live_router.py

La campagne hors-ligne simule le protocole binaire (faux socket) et le transport SSH (faux backend) : elle couvre l'encodage/décodage, les parseurs CLI, la traduction des commandes, le quoting anti-injection et chaque régression corrigée (tests test_bugN_…). Elle passe avec et sans paramiko installé.

La campagne réelle couvre les quatre transports, les écritures, listen(), export(), ping_host(), le pipelining, les bascules de transport et la cohérence des données entre API et SSH. Tous les objets créés sont supprimés en fin de campagne. Paramétrage via .env.example.

Publication

Le workflow .github/workflows/publish.yml publie sur PyPI via Trusted Publishing (OIDC, aucun token à stocker) lorsqu'une étiquette de version est poussée. Il refuse de publier si l'étiquette ne correspond pas à __version__.

Une seule fois, déclarer l'éditeur de confiance sur https://pypi.org/manage/account/publishing/ (rubrique Add a new pending publisher) :

Champ Valeur
PyPI Project Name routeros-client
Owner jackarten
Repository name routeros-client
Workflow name publish.yml
Environment name pypi

À chaque version :

git tag v0.6.0 && git push origin v0.6.0

Contenu du dépôt

Fichier Rôle
routeros_api.py La bibliothèque — module autonome
test_routeros_api.py Tests hors-ligne (90)
test_live_router.py Campagne contre un routeur réel (62)
CHANGELOG.md 23 bugs corrigés, 9 optimisations, transport SSH — détail et justification
pyproject.toml Packaging, extras [ssh] et [dev], configuration ruff
requirements.txt Dépendances (SSH par mot de passe inclus)
requirements-core.txt Installation minimale, sans dépendance
.env.example Modèle de configuration des tests réels

routeros_api_v0.5.0.bak.py conserve la version précédente ; il est exclu du dépôt par .gitignore.


Migration depuis la v0.5.0

Le code existant fonctionne sans modification : classes, signatures et valeurs de retour sont préservées, les ajouts sont strictement additifs.

Trois différences de comportement, toutes des corrections de bugs (détail dans CHANGELOG.md) :

  1. split_command() retire de nouveau les guillemets englobants — =comment="a b" créait un commentaire contenant littéralement les guillemets. Ancien comportement : RouterOSProtocol.KEEP_QUOTES = True.
  2. Un !trap est signalé après lecture du !done : l'exception est la même, mais le flux n'est plus désynchronisé pour toutes les commandes suivantes.
  3. Les booléens Python sont convertis : disabled=True produit =disabled=yes et non =disabled=True, que RouterOS refusait.

Si vous utilisiez RestApi.add(), il était cassé en v0.5.0 (POST → HTTP 400) : il utilise de nouveau PUT, le verbe de création de l'API REST RouterOS.


Licence

MIT — voir le fichier LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

routeros_client-0.6.0.tar.gz (83.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

routeros_client-0.6.0-py3-none-any.whl (52.7 kB view details)

Uploaded Python 3

File details

Details for the file routeros_client-0.6.0.tar.gz.

File metadata

  • Download URL: routeros_client-0.6.0.tar.gz
  • Upload date:
  • Size: 83.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for routeros_client-0.6.0.tar.gz
Algorithm Hash digest
SHA256 da791203b30529c92e89b842c7d27dd46c323adcbf06181cc3efe3163a09e1e2
MD5 771db34a9c838f751abf29c74e09a1bc
BLAKE2b-256 0f4ffb5a69f68daa6dc9d228f81832d2c6c125d440d2a96c9d5f71e22ad6d221

See more details on using hashes here.

Provenance

The following attestation bundles were made for routeros_client-0.6.0.tar.gz:

Publisher: publish.yml on Jackarten/routeros-client

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file routeros_client-0.6.0-py3-none-any.whl.

File metadata

File hashes

Hashes for routeros_client-0.6.0-py3-none-any.whl
Algorithm Hash digest
SHA256 d24cb1c769fae2e39119e9c1d9fa164c8396c93966f325680386be4ec6196de2
MD5 2949ae70419f4fa9f355372a37f447ef
BLAKE2b-256 0673cb8476943bf0335259bf13e1cbddcc00459fda46d93c8e50316bf44df353

See more details on using hashes here.

Provenance

The following attestation bundles were made for routeros_client-0.6.0-py3-none-any.whl:

Publisher: publish.yml on Jackarten/routeros-client

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.6.4

2 files

0.6.3

2 files

0.6.1

2 files

This release

0.6.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page