routeros-client
Client Python pour MikroTik RouterOS : une seule installation, quatre transports.
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 |
|---|---|---|
| API binaire (défaut) | Api |
8728 |
| API binaire sur TLS | Api(use_ssl=True) |
8729 |
| SSH | SshApi |
22 |
| REST / JSON (RouterOS v7) | RestApi |
80 / 443 |
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
C'est tout : les quatre transports sont disponibles d'emblée. paramiko, requis par le transport
SSH, est installé automatiquement et chargé paresseusement — un programme qui n'utilise que
l'API binaire ne paie pas son coût de démarrage.
pip install "routeros-client[ssh]"reste accepté, et est strictement équivalent : l'extra est conservé comme alias sans effet pour ne casser aucun script existant.
Nom d'installation ≠ nom d'import
Le nom routeros-api étant déjà occupé sur PyPI par un autre projet, la distribution s'appelle
routeros-client. Les deux noms d'import fonctionnent :
import routeros_client # nom canonique, aligné sur la distribution
import routeros_api # alias de compatibilité, pleinement supporté
Ce ne sont pas deux copies mais le même objet module : isinstance, attributs privés et
monkeypatching se comportent à l'identique dans les deux cas. Le code écrit pour les versions
précédentes continue donc de fonctionner sans la moindre modification.
📘 Guide d'utilisation complet : GUIDE.md.
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 :
/exportne renvoie rien via l'API binaire sur RouterOS v7 (limite de RouterOS, pas de la bibliothèque), alors queSshApi.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().
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_client
coûte 475 ms au lieu de 982 ms si vous n'utilisez que l'API — bien qu'il soit toujours installé.
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_resiliencerejoue la commande après une coupure. Pour les commandes non idempotentes (add), préférezensure().
Développement et tests
python -m venv .venv
.venv/bin/activate # Windows : .\.venv\Scripts\Activate.ps1
pip install -e ".[dev]"
# Hors-ligne — 105 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, l'équivalence des deux noms d'import et chaque régression corrigée
(tests test_bugN_…).
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.1 && git push origin v0.6.1
Contenu du dépôt
| Fichier | Rôle |
|---|---|
routeros_client.py |
La bibliothèque — module autonome |
routeros_api.py |
Alias de compatibilité vers routeros_client |
GUIDE.md |
Guide d'utilisation complet, 20 chapitres |
test_routeros_api.py |
Tests hors-ligne (105) |
test_live_router.py |
Campagne contre un routeur réel (62) |
CHANGELOG.md |
27 bugs corrigés, 11 optimisations, transport SSH — détail et justification |
pyproject.toml |
Packaging, configuration ruff |
requirements.txt |
Dépendance d'exécution (paramiko) |
.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) :
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.- Un
!trapest 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. - Les booléens Python sont convertis :
disabled=Trueproduit=disabled=yeset 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file routeros_client-0.6.4.tar.gz.
File metadata
- Download URL: routeros_client-0.6.4.tar.gz
- Upload date:
- Size: 94.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
aaf6d0bf65422fbbe046b822dc416da95e50be1dbd8959f8c48cddcad5c8b5f9
|
|
| MD5 |
2be1a95f2f1d31316421cdf4b82bf3d3
|
|
| BLAKE2b-256 |
a155dd17321d5bd98b886e776a67adf0f77bacf6b3f6246e4e43445ac68b63a5
|
Provenance
The following attestation bundles were made for routeros_client-0.6.4.tar.gz:
Publisher:
publish.yml on Jackarten/routeros-client
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
routeros_client-0.6.4.tar.gz -
Subject digest:
aaf6d0bf65422fbbe046b822dc416da95e50be1dbd8959f8c48cddcad5c8b5f9 - Sigstore transparency entry: 2681758653
- Sigstore integration time:
-
Permalink:
Jackarten/routeros-client@17c07c8c457ba5683d78ee6ae3e43508f29a46db -
Branch / Tag:
refs/heads/main - Owner: https://github.com/Jackarten
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@17c07c8c457ba5683d78ee6ae3e43508f29a46db -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file routeros_client-0.6.4-py3-none-any.whl.
File metadata
- Download URL: routeros_client-0.6.4-py3-none-any.whl
- Upload date:
- Size: 50.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a02dc91edcb7b5ef7e76906528ddbe01b6bece9110871620620f7c93eb7a4503
|
|
| MD5 |
ab9d6c721b5e14a0960d459bb35c72f0
|
|
| BLAKE2b-256 |
95301e7e9e5fecebfaa35cc38a3408d44a3860106f9a1bdf2380bd182da7dad4
|
Provenance
The following attestation bundles were made for routeros_client-0.6.4-py3-none-any.whl:
Publisher:
publish.yml on Jackarten/routeros-client
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
routeros_client-0.6.4-py3-none-any.whl -
Subject digest:
a02dc91edcb7b5ef7e76906528ddbe01b6bece9110871620620f7c93eb7a4503 - Sigstore transparency entry: 2681758729
- Sigstore integration time:
-
Permalink:
Jackarten/routeros-client@17c07c8c457ba5683d78ee6ae3e43508f29a46db -
Branch / Tag:
refs/heads/main - Owner: https://github.com/Jackarten
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@17c07c8c457ba5683d78ee6ae3e43508f29a46db -
Trigger Event:
workflow_dispatch
-
Statement type: