detain / sshwitch
Login and run commands on your network devices via an ssh connection on a remote server. Easy to use PHP static class methods with static chaining support for rapid development. Supports Cisco and Juniper network routers and switches with more easily added.
This package is auto-updated.
Last update: 2026-05-01 06:06:11 UTC
README
sshwitch is a small PHP library that runs commands on network devices
(switches, routers, firewalls) by connecting from a relay box to the device
through RANCID's clogin script.
It's built around an all-static, optionally-chainable API that's pleasant to embed in operations scripts — pass a switch IP and a list of commands, get back the parsed per-command output.
clogin itself supports a long list of vendors: Cisco, Juniper, Extreme
Networks, Procket Networks, Redback, A10, Alteon, Avocent (Cyclades), Bay
Networks (Nortel), ADC-kentrox, Foundry, HP, Hitachi, MRV, Mikrotik,
Netscreen, Nokia (Alcatel-Lucent), Netscaler, Riverstone, Netopia, Xirrus,
Arrcus, and more. This library has been used most heavily against Cisco IOS
and Juniper but the transport is identical for any device clogin knows.
Table of Contents
- Requirements
- Installation
- Quick Start
- Configuration
- How it works
- API Reference
- Examples
- Testing
- Troubleshooting
- License
Requirements
- PHP 7.4 or later (PHP 8.x supported and recommended)
ssh2PHP extension — usually packaged asphp-ssh2/php8.x-ssh2- A relay/jump host reachable over SSH that has RANCID's
cloginscript installed at/usr/libexec/rancid/clogin(the path is hard-coded in the current version) - An SSH keypair trusted by the relay; the public key file lives at
<key>.pubnext to the private key
Installation
Install via Composer:
composer require detain/sshwitch
Then ensure the ssh2 extension is loaded:
php -m | grep ssh2
If it's missing, install it (Debian/Ubuntu: apt install php-ssh2).
Quick Start
<?php require __DIR__ . '/vendor/autoload.php'; use Detain\Sshwitch\Sshwitch; // 1. Tell sshwitch how to reach your relay box define('CLOGIN_SSH_HOST', '10.0.0.1'); define('CLOGIN_SSH_PORT', 22); define('CLOGIN_SSH_USER', 'sshuser'); define('CLOGIN_SSH_KEY', '/home/sshuser/.ssh/id_rsa'); // 2. Run commands against a target switch through that relay $result = Sshwitch::run('switch01.example.net', [ 'show version', 'show ip interface brief', ]); if ($result === false) { fwrite(STDERR, "Failed to talk to the switch.\n"); exit(1); } foreach ($result as $i => $output) { echo "=== Command #{$i} ===\n{$output}\n"; }
Configuration
The library reads four define()'d constants. They must be defined before
calling connect() or run():
| Constant | Description | Example |
|---|---|---|
CLOGIN_SSH_HOST |
IP/hostname of the relay box | '10.0.0.1' |
CLOGIN_SSH_PORT |
SSH port on that relay | 22 |
CLOGIN_SSH_USER |
SSH username on the relay | 'sshuser' |
CLOGIN_SSH_KEY |
Path to private key (public key sits at .pub) |
'/home/sshuser/.ssh/id_rsa' |
The relay box is also where /usr/libexec/rancid/clogin must exist.
RANCID's .cloginrc on that box is what controls device-side credentials.
How it works
┌────────────┐ ssh2 (php-ssh2) ┌─────────────┐ clogin ┌────────────┐
│ your PHP │ ──────────────────► │ relay box │ ───────────► │ switch / │
│ script │ ◄────────────────── │ (CLOGIN_*) │ ◄─────────── │ router │
└────────────┘ └─────────────┘ └────────────┘
connect()opens an SSH session from your PHP process to the relay box using theCLOGIN_SSH_*constants.run($switch, $commands)builds aclogincommand line, prependsshow hostnameso the parser can anchor on the device prompt, and runs it throughssh2_exec().- The combined stream is parsed: the prelude/banner is dropped, each
command's output is captured into its own array entry, and noisy IOS
progress bars (
[##### ] N%) are collapsed to their final entry to keep logs sane. - When
$autoDisconnectis true (default), the SSH session is closed.
API Reference
Methods
| Method | Description |
|---|---|
Sshwitch::connect() |
Opens (and authenticates) the SSH session if not already open. Returns true/false. |
Sshwitch::disconnect() |
Closes the SSH session if one is open. Safe to call when nothing is connected. |
Sshwitch::run(string $switch, $commands, string $type='cisco', array &$return=[]) |
Runs $commands against $switch, returns the parsed output array (or false on failure). |
Sshwitch::__callStatic($name, $args) |
Magic dispatch for the auto-generated getXxx()/setXxx() accessors. |
run()'s $type parameter is currently unused — it's reserved for future
overloads that branch on device family without a signature change. Leave the
default unless you're a contributor wiring a vendor-specific code path.
run()'s $return is an out-parameter that mirrors the return value;
either is fine, depending on which style you prefer.
Properties (via magic accessors)
Every public static property has an auto-generated get/set pair. Setters
return true on success (or the class name in chaining mode); getters
always return the value:
| Property | Type | Default | Get / Set |
|---|---|---|---|
$connection |
resource|false |
false |
getConnection(), setConnection($r) |
$timeout |
int |
10 |
getTimeout(), setTimeout(int) (clogin -t) |
$switch |
string |
'' |
getSwitch(), setSwitch(string) |
$commands |
array |
[] |
getCommands(), setCommands(array) |
$output |
string |
'' |
getOutput(), setOutput(string) |
$hostname |
string |
'' |
getHostname(), setHostname(string) |
$motd |
string |
'' |
getMotd(), setMotd(string) |
$commandOutputs |
array |
[] |
getCommandOutputs(), setCommandOutputs(array) |
$autoDisconnect |
bool |
true |
getAutoDisconnect(), setAutoDisconnect(bool) |
$chaining |
bool |
false |
getChaining(), setChaining(bool) |
Calling a setXxx() for a property that doesn't exist throws
BadMethodCallException (so does any other unknown method).
Static chaining
Set Sshwitch::$chaining = true (or Sshwitch::setChaining(true)) to make
setters and action methods (connect, disconnect, run) return
the class name string instead of their natural return value, allowing PHP's
:: chaining syntax:
Sshwitch::setChaining(true) ::setTimeout(15) ::setAutoDisconnect(false) ::run('switch01.example.net', 'show version'); // Disable chaining when you actually want a value back: $output = Sshwitch::setChaining(false)::getOutput();
Getters never chain — calling getOutput() always returns the value, even
when chaining is on, because chained getters would be useless.
Examples
Run a single command
$out = Sshwitch::run('sw01.example.net', 'show clock'); echo $out[0] ?? 'no output';
Pass commands as a semicolon-separated string
Sshwitch::run('sw01.example.net', 'show version;show clock;show ip route summary');
Reuse one SSH session for several run() calls
Sshwitch::setAutoDisconnect(false); Sshwitch::run('sw01.example.net', 'show version'); Sshwitch::run('sw02.example.net', 'show version'); Sshwitch::disconnect();
Inspect raw vs parsed output
$parsed = Sshwitch::run('sw01.example.net', 'show inventory'); echo "Raw transcript:\n", Sshwitch::getOutput(), "\n\n"; echo "Parsed entries:\n", print_r($parsed, true); echo "Device hostname:\n", Sshwitch::getHostname(), "\n";
Handle failures gracefully
connect() returns false on connect/auth failure; run() returns false
when connect() fails, when ssh2_exec itself returns false, or when the
output can't be parsed:
$result = Sshwitch::run('sw01.example.net', 'show version'); if ($result === false) { error_log('switch run failed; raw output: ' . Sshwitch::getOutput()); }
Testing
The test suite mocks every ssh2_* and stream function in the
Detain\Sshwitch namespace with
php-mock/php-mock-phpunit,
so no real SSH server is ever contacted.
composer install composer test # run the suite composer test:coverage # text coverage report (needs xdebug/pcov)
The PHPUnit configuration lives in phpunit.xml.dist and bootstraps
tests/bootstrap.php, which defines stub CLOGIN_SSH_* constants for the
test run.
Troubleshooting
Undefined constant CLOGIN_SSH_HOST — define the four constants listed
under Configuration before calling any Sshwitch method.
ssh2_connect: Could not connect — verify you can SSH from the host
running PHP to the relay box manually using the same private key and user.
run() returns false but getOutput() shows data — the parser
anchors on <hostname># <command> prompt lines. If the device prompt isn't
present (e.g. cmdline ran on a non-clogin shell), parsing won't match.
Trait "phpmock\phpunit\PHPMock" not found — run
composer install --dev so php-mock/php-mock-phpunit is available.
Idle, hung connections from earlier scripts — call Sshwitch::disconnect()
explicitly when $autoDisconnect is off, otherwise PHP will eventually
close the resource on shutdown.
License
GNU General Public License v3.0 — see LICENSE.