diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..a678e8d --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,142 @@ +name: CI +on: [push, pull_request] +jobs: + phpstan: + name: PHPStan + runs-on: ubuntu-20.04 + strategy: + matrix: + php-version: [7.3] + steps: + - uses: actions/checkout@v2 + - uses: shivammathur/setup-php@v2 + with: + coverage: none + php-version: ${{ matrix.php-version }} + extensions: json + tools: cs2pr + + - name: Install dependencies + run: sudo apt install php${{ matrix.php-version }}-snmp + + - name: Cache dependencies installed with composer + uses: actions/cache@v1 + with: + path: ~/.composer/cache + key: php-${{ matrix.php-version }} + restore-keys: php-${{ matrix.php-version }} + + - name: Install dependencies with composer + run: COMPOSER_ARGS="--prefer-stable" make + + - name: Run a static analysis with phpstan/phpstan + env: + PHPSTAN_ARGS: --error-format=checkstyle + run: make -is static-analysis | cs2pr + + coding-standards: + name: Coding Standards + runs-on: ubuntu-20.04 + strategy: + matrix: + php-version: [7.3] + steps: + - uses: actions/checkout@v2 + - uses: shivammathur/setup-php@v2 + with: + coverage: none + php-version: ${{ matrix.php-version }} + tools: cs2pr + + - name: Install dependencies + run: sudo apt install php${{ matrix.php-version }}-snmp + + - name: Cache dependencies installed with composer + uses: actions/cache@v1 + with: + path: ~/.composer/cache + key: php-${{ matrix.php-version }} + restore-keys: php-${{ matrix.php-version }} + + - name: Install dependencies with composer + run: COMPOSER_ARGS="--prefer-stable" make + + - name: Run squizlabs/php_codesniffer + env: + PHPCS_ARGS: -q --no-colors --report=checkstyle + run: make -is cs | cs2pr + + tests: + name: Tests + runs-on: ubuntu-20.04 + strategy: + matrix: + php-version: [7.3, 7.4] + dependencies: ["", --prefer-lowest] + + steps: + - uses: actions/checkout@v2 + - uses: shivammathur/setup-php@v2 + with: + coverage: none + php-version: ${{ matrix.php-version }} + extensions: json + + - name: Install dependencies + run: sudo apt install php${{ matrix.php-version }}-snmp python3-pip snmp + + - name: Install snmpsim + run: sudo pip3 install snmpsim + + - name: Cache dependencies installed with composer + uses: actions/cache@v1 + with: + path: ~/.composer/cache + key: php-${{ matrix.php-version }}-dependencies-${{ matrix.dependencies }} + restore-keys: php-${{ matrix.php-version }} + + - name: Install dependencies with composer + run: COMPOSER_ARGS="--prefer-stable ${{ matrix.dependencies }}" make + + - name: Run tests + run: make test + + coverage: + name: Tests Coverage + runs-on: ubuntu-20.04 + strategy: + matrix: + php-version: [7.4] + + steps: + - uses: actions/checkout@v2 + - uses: shivammathur/setup-php@v2 + with: + coverage: pcov + php-version: ${{ matrix.php-version }} + extensions: json + + - name: Install dependencies + run: sudo apt install php${{ matrix.php-version }}-snmp python3-pip snmp + + - name: Install snmpsim + run: sudo pip3 install snmpsim + + - name: Cache dependencies installed with composer + uses: actions/cache@v1 + with: + path: ~/.composer/cache + key: php-${{ matrix.php-version }} + restore-keys: php-${{ matrix.php-version }} + + - name: Install dependencies with composer + run: COMPOSER_ARGS="--prefer-stable" make + + - name: Run tests coverage + run: PHPUNIT_ARGS="--coverage-clover coverage/clover.xml" make test + + - name: Report to Coveralls + env: + COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COVERALLS_RUN_LOCALLY: 1 + run: vendor/bin/php-coveralls --coverage_clover coverage/clover.xml --json_path coverage/coveralls.json diff --git a/.gitignore b/.gitignore index 8da4a33..d896cee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,9 @@ +/coverage/ /vendor/ + /.phpcs-cache +/.phpunit.result.cache /composer.lock /phpcs.xml /phpstan.neon +/phpunit.xml diff --git a/.scrutinizer.yml b/.scrutinizer.yml deleted file mode 100644 index dae628f..0000000 --- a/.scrutinizer.yml +++ /dev/null @@ -1,17 +0,0 @@ -build: - environment: - elasticsearch: false - memcached: false - mongodb: false - mysql: false - neo4j: false - postgresql: false - rabbitmq: false - redis: false - -tools: - external_code_coverage: true - -checks: - php: - code_rating: true diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b06e1e7..0000000 --- a/.travis.yml +++ /dev/null @@ -1,38 +0,0 @@ -dist: trusty -language: php -sudo: required - -php: - - 7.2 - - 7.3 - - nightly - -before_install: - - echo "extension = snmp.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini - - composer self-update - -before_script: - - travis_retry composer update --prefer-dist - -script: ./vendor/bin/phpunit - -jobs: - include: - - stage: Coding Standard - php: 7.2 - script: ./vendor/bin/phpcs - - - stage: Static Analysis - php: 7.2 - script: - - ./vendor/bin/phpstan analyse -c phpstan.neon.dist -l max src - -cache: - directories: - - $HOME/.composer/cache - -after_script: - # upload clover.xml file to Scrutinizer to analyze it - - | - if [ "$TRAVIS_PHP_VERSION" == "7.2" ]; then wget https://scrutinizer-ci.com/ocular.phar; fi - if [ "$TRAVIS_PHP_VERSION" == "7.2" ]; then php ocular.phar code-coverage:upload --format=php-clover temp/clover.xml; fi diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..50c1340 --- /dev/null +++ b/Makefile @@ -0,0 +1,37 @@ +COMPOSER_ARGS += --no-interaction --no-progress --no-suggest + +.PHONY: build +build: vendor + +.PHONY: vendor +vendor: vendor/lock + +vendor/lock: composer.json + composer update $(COMPOSER_ARGS) + touch vendor/lock + +.PHONY: test +test: + vendor/bin/phpunit $(PHPUNIT_ARGS) + +.PHONY: cs +cs: + vendor/bin/phpcs $(PHPCS_ARGS) + +.PHONY: fix +fix: + vendor/bin/phpcbf + +.PHONY: static-analysis +static-analysis: + vendor/bin/phpstan analyse $(PHPSTAN_ARGS) + +.PHONY: check +check: build cs static-analysis test + +.PHONY: clean +clean: clean-vendor + +.PHONY: clean-vendor +clean-vendor: + rm -rf vendor composer.lock diff --git a/README.md b/README.md new file mode 100644 index 0000000..449bb18 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +PHP SNMP Client +=============== + +[![Build Status](https://github.com/simPod/PHP-SNMP/workflows/CI/badge.svg?branch=master)](https://github.com/simPod/PHP-SNMP/actions) +[![Packagist](https://poser.pugx.org/simpod/php-snmp/v/stable.svg)](https://packagist.org/packages/simpod/php-snmp) +[![Coverage Status](https://coveralls.io/repos/github/simPod/PHP-SNMP/badge.svg?branch=master)](https://coveralls.io/github/simPod/PHP-SNMP?branch=master) diff --git a/composer.json b/composer.json index 1a280f9..695019e 100644 --- a/composer.json +++ b/composer.json @@ -1,14 +1,8 @@ { "name": "simpod/php-snmp", - "description": "PHP Simple Network Management", + "description": "PHP library for connecting to the SNMP agents using various transports; includes small collection of OIDs", "type": "library", "license": "MIT", - "authors": [ - { - "name": "Simon Podlipsky", - "email": "simon@podlipsky.net" - } - ], "keywords": [ "php", "snmp" @@ -17,14 +11,36 @@ "sort-packages": true }, "require": { - "php": "^7.2", - "ext-snmp": "*", - "consistence/consistence": "^1.0|^2.0" + "php": "^7.3 || ^8.0", + "psr/log": "^1.1", + "roave/you-are-using-it-wrong": "^1.4" + }, + "suggest": { + "ext-json": "Required for SNMP transport \"Api\"", + "psr/http-client": "Required for SNMP transport \"Api\"", + "psr/http-factory": "Required for SNMP transport \"Api\"", + "symfony/process": "Required for default implementation of SNMP transport \"Cli\"", + "ext-snmp": "Required for SNMP transport \"Extension\"" }, "require-dev": { + "ext-json": "*", + "ext-snmp": "*", "cdn77/coding-standard": "^3.0", - "phpstan/phpstan": "^0.11.5", - "phpstan/phpstan-strict-rules": "^0.11.0" + "nyholm/psr7": "^1.3", + "php-http/curl-client": "^2.1", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^0.12.25", + "phpstan/phpstan-deprecation-rules": "^0.12.3", + "phpstan/phpstan-phpunit": "^0.12.8", + "phpstan/phpstan-strict-rules": "^0.12.2", + "phpunit/phpunit": "^9.1", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0", + "simpod/php-coveralls-mirror": "^3.0", + "squizlabs/php_codesniffer": "^3.5", + "symfony/process": "^4.3", + "thecodingmachine/phpstan-safe-rule": "^1.0" }, "autoload": { "psr-4": { diff --git a/phpstan.neon.dist b/phpstan.neon.dist index eeea86b..7374d1c 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,11 +1,9 @@ parameters: - memory-limit: -1 level: max paths: - %currentWorkingDirectory%/src - %currentWorkingDirectory%/tests - -includes: - - vendor/phpstan/phpstan-phpunit/extension.neon - - vendor/phpstan/phpstan-phpunit/rules.neon - - vendor/phpstan/phpstan-strict-rules/rules.neon + ignoreErrors: + # extension is badly designed, we know these types won't ever be here + - '~Parameter #1 \$output of method SimPod\\PhpSnmp\\Transport\\ExtensionSnmpClient::processOutput\(\) expects array, array\|string\|false given\.~' + - '~Parameter #1 \$output of method SimPod\\PhpSnmp\\Transport\\ExtensionSnmpClient::processOutput\(\) expects array, array\|false given\.~' diff --git a/phpunit.xml b/phpunit.xml.dist similarity index 50% rename from phpunit.xml rename to phpunit.xml.dist index d50bf82..2e1c3b5 100644 --- a/phpunit.xml +++ b/phpunit.xml.dist @@ -1,11 +1,10 @@ - - @@ -15,18 +14,8 @@ - + src - - - - - diff --git a/src/Exception/CannotParseUnknownValueType.php b/src/Exception/CannotParseUnknownValueType.php new file mode 100644 index 0000000..177805b --- /dev/null +++ b/src/Exception/CannotParseUnknownValueType.php @@ -0,0 +1,16 @@ +getMessage(), $matches) !== 1) { + throw self::new(); + } + + return self::fromOid($matches[1]); + } +} diff --git a/src/Exception/GeneralException.php b/src/Exception/GeneralException.php new file mode 100644 index 0000000..cbf7fdf --- /dev/null +++ b/src/Exception/GeneralException.php @@ -0,0 +1,30 @@ + $oids */ + public static function new(string $error, ?Throwable $previous = null, ?array $oids = null) : self + { + if ($oids !== null) { + $error .= sprintf(', oids: %s', implode(', ', $oids)); + } + + return new self($error, 0, $previous); + } + + /** @param list $oids */ + public static function fromThrowable(Throwable $throwable, ?array $oids = null) : self + { + return self::new($throwable->getMessage(), $throwable, $oids); + } +} diff --git a/src/Exception/InvalidVersionProvided.php b/src/Exception/InvalidVersionProvided.php new file mode 100644 index 0000000..00c9366 --- /dev/null +++ b/src/Exception/InvalidVersionProvided.php @@ -0,0 +1,16 @@ +getMessage(), $matches) !== 1) { + throw self::new(); + } + + return self::fromOid($matches[1]); + } +} diff --git a/src/Exception/NoSuchObjectExists.php b/src/Exception/NoSuchObjectExists.php new file mode 100644 index 0000000..20c2b17 --- /dev/null +++ b/src/Exception/NoSuchObjectExists.php @@ -0,0 +1,32 @@ +getMessage(), $matches) !== 1) { + throw self::new(); + } + + return self::fromOid($matches[1]); + } +} diff --git a/src/SnmpException.php b/src/Exception/SnmpException.php similarity index 72% rename from src/SnmpException.php rename to src/Exception/SnmpException.php index 9b8f1a4..406cfab 100644 --- a/src/SnmpException.php +++ b/src/Exception/SnmpException.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace SimPod\PhpSnmp; +namespace SimPod\PhpSnmp\Exception; use Throwable; diff --git a/src/Exception/SnmpFailed.php b/src/Exception/SnmpFailed.php deleted file mode 100644 index 4151683..0000000 --- a/src/Exception/SnmpFailed.php +++ /dev/null @@ -1,12 +0,0 @@ -getKey(), (float) $keyValuePair->getValue()); - } - ); - } -} diff --git a/src/Helpers/OidStripper.php b/src/Helpers/OidStripper.php new file mode 100644 index 0000000..764cf6f --- /dev/null +++ b/src/Helpers/OidStripper.php @@ -0,0 +1,132 @@ + $leafOidData + * + * @return array + * + * @psalm-template T + * @psalm-param array $leafOidData + * @psalm-return array + */ + public static function stripParent(array $leafOidData) : array + { + return self::batchStripParent([$leafOidData])[0]; + } + + /** + * @param list> $leafOidDataResponses + * + * @return list> + * + * @psalm-template T + * @psalm-param list> $leafOidDataResponses + * @psalm-return list> + */ + public static function batchStripParent(array $leafOidDataResponses) : array + { + $result = []; + foreach ($leafOidDataResponses as $leafOidData) { + $firstKey = array_key_first($leafOidData); + if ($firstKey === null) { + throw GeneralException::new('Expected non-empty array'); + } + + $lastDotPos = strrpos($firstKey, '.'); + if ($lastDotPos === false) { + throw GeneralException::new('Expected keys to be full OIDs'); + } + + $stripLength = $lastDotPos + 1; + + $responseResult = []; + foreach ($leafOidData as $oid => $value) { + $responseResult[(int) substr($oid, $stripLength)] = $value; + } + + $result[] = $responseResult; + } + + return $result; + } + + /** @return array */ + public static function walk(SnmpClient $snmpClient, string $oid, int $maxRepetitions = 40) : array + { + return self::batchStripParentPrefix($snmpClient, [Request::walk($oid, $maxRepetitions)])[0]; + } + + /** + * @param array $requests + * + * @return array> + * + * @psalm-template T + * @psalm-param array $requests + * @psalm-return array> + */ + public static function batchStripParentPrefix(SnmpClient $snmpClient, array $requests) : array + { + $responses = $snmpClient->batch($requests); + + $result = []; + foreach ($requests as $requestKey => $request) { + switch ($request->type) { + case Request::GET: + $i = 0; + $getResult = []; + + foreach ($responses[$requestKey] as $childOid => $value) { + $getResult[substr($childOid, strlen($request->oids[$i]) + 1)] = $value; + $i++; + } + + $result[$requestKey] = $getResult; + + break; + case Request::GET_NEXT: + $i = 0; + $getNextResult = []; + + foreach ($responses[$requestKey] as $childOid => $value) { + if (strpos($childOid, $request->oids[$i]) === 0) { + $childOid = substr($childOid, strlen($request->oids[$i]) + 1); + } + + $getNextResult[$childOid] = $value; + $i++; + } + + $result[$requestKey] = $getNextResult; + + break; + case Request::WALK: + $stripLength = strlen($request->oids[0]) + 1; + + $walkResult = []; + foreach ($responses[$requestKey] as $childOid => $value) { + $walkResult[substr($childOid, $stripLength)] = $value; + } + + $result[$requestKey] = $walkResult; + } + } + + return $result; + } +} diff --git a/src/Helpers/ValueGetter.php b/src/Helpers/ValueGetter.php new file mode 100644 index 0000000..2752c36 --- /dev/null +++ b/src/Helpers/ValueGetter.php @@ -0,0 +1,61 @@ + $response + * + * @return mixed + * + * @psalm-template T + * @psalm-param array $response + * @psalm-return T + */ + public static function first(array $response) + { + $result = array_shift($response); + if ($result === null) { + throw GeneralException::new('Expected non-empty array'); + } + + return $result; + } + + /** @return mixed */ + public static function firstFromSameTree(SnmpClient $snmpClient, string $oid) + { + return self::firstFromSameTrees($snmpClient, [$oid])[0]; + } + + /** + * @param list $oids + * + * @return list + */ + public static function firstFromSameTrees(SnmpClient $snmpClient, array $oids) : array + { + $result = $snmpClient->getNext($oids); + + $i = 0; + foreach ($result as $oid => $value) { + if (strpos($oid, $oids[$i]) !== 0) { + throw NoSuchInstanceExists::fromOid($oids[$i]); + } + + $i++; + } + + return array_values($result); + } +} diff --git a/src/Mib/Arista/EntitySensor.php b/src/Mib/Arista/EntitySensor.php index c96760b..e731b13 100644 --- a/src/Mib/Arista/EntitySensor.php +++ b/src/Mib/Arista/EntitySensor.php @@ -4,36 +4,10 @@ namespace SimPod\PhpSnmp\Mib\Arista; -use SimPod\PhpSnmp\Mib\MibBase; - -class EntitySensor extends MibBase +final class EntitySensor { public const OID_THRESHOLD_LOW_WARNING = '.1.3.6.1.4.1.30065.3.12.1.1.1.1'; public const OID_THRESHOLD_LOW_CRITICAL = '.1.3.6.1.4.1.30065.3.12.1.1.1.2'; public const OID_THRESHOLD_HIGH_WARNING = '.1.3.6.1.4.1.30065.3.12.1.1.1.3'; public const OID_THRESHOLD_HIGH_CRITICAL = '.1.3.6.1.4.1.30065.3.12.1.1.1.4'; - - /** @return int[] */ - public function getThresholdLowWarning() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_THRESHOLD_LOW_WARNING); - } - - /** @return int[] */ - public function getThresholdLowCritical() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_THRESHOLD_LOW_CRITICAL); - } - - /** @return int[] */ - public function getThresholdHighWarning() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_THRESHOLD_HIGH_WARNING); - } - - /** @return int[] */ - public function getThresholdHighCritical() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_THRESHOLD_HIGH_CRITICAL); - } } diff --git a/src/Mib/Cisco/EntitySensor.php b/src/Mib/Cisco/EntitySensor.php index 0d4b189..6c7f8e4 100644 --- a/src/Mib/Cisco/EntitySensor.php +++ b/src/Mib/Cisco/EntitySensor.php @@ -4,12 +4,10 @@ namespace SimPod\PhpSnmp\Mib\Cisco; -use SimPod\PhpSnmp\Mib\MibBase; - /** * See CISCO-ENTITY-SENSOR-MIB */ -class EntitySensor extends MibBase +final class EntitySensor { public const OID_PHYSICAL_SENSOR_TYPE = '.1.3.6.1.4.1.9.9.91.1.1.1.1.1'; public const OID_PHYSICAL_SENSOR_SCALE = '.1.3.6.1.4.1.9.9.91.1.1.1.1.2'; @@ -19,52 +17,4 @@ class EntitySensor extends MibBase public const OID_PHYSICAL_SENSOR_UNITS_DISPLAY = '.1.3.6.1.4.1.9.9.91.1.1.1.1.6'; public const OID_PHYSICAL_SENSOR_VALUE_TIME_STAMP = '.1.3.6.1.4.1.9.9.91.1.1.1.1.7'; public const OID_PHYSICAL_SENSOR_VALUE_UPDATE_RATE = '.1.3.6.1.4.1.9.9.91.1.1.1.1.8'; - - /** @return int[] */ - public function getPhysicalSensorType() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_PHYSICAL_SENSOR_TYPE); - } - - /** @return int[] */ - public function getPhysicalSensorScale() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_PHYSICAL_SENSOR_SCALE); - } - - /** @return int[] */ - public function getPhysicalSensorPrecision() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_PHYSICAL_SENSOR_PRECISION); - } - - /** @return int[] */ - public function getPhysicalSensorValue() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_PHYSICAL_SENSOR_VALUE); - } - - /** @return int[] */ - public function getPhysicalSensorOperStatus() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_PHYSICAL_SENSOR_OPER_STATUS); - } - - /** @return string[] */ - public function getPhysicalSensorUnitsDisplay() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_PHYSICAL_SENSOR_UNITS_DISPLAY); - } - - /** @return int[] */ - public function getPhysicalSensorValueTimeStamp() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_PHYSICAL_SENSOR_VALUE_TIME_STAMP); - } - - /** @return int[] */ - public function getPhysicalSensorValueUpdateRate() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_PHYSICAL_SENSOR_VALUE_UPDATE_RATE); - } } diff --git a/src/Mib/Cisco/EnvMon.php b/src/Mib/Cisco/EnvMon.php index 36673ba..277ec3f 100644 --- a/src/Mib/Cisco/EnvMon.php +++ b/src/Mib/Cisco/EnvMon.php @@ -4,39 +4,13 @@ namespace SimPod\PhpSnmp\Mib\Cisco; -use SimPod\PhpSnmp\Mib\MibBase; - /** * See CISCO-ENVMON-MIB */ -class EnvMon extends MibBase +final class EnvMon { - public const OID_CISCO_ENV_MON_FAN_STATUS_DESRC = '1.3.6.1.4.1.9.9.13.1.4.1.2'; - public const OID_CISCO_ENV_MON_FAN_STATE = '1.3.6.1.4.1.9.9.13.1.4.1.3'; - public const OID_CISCO_ENV_MON_SUPPLY_STATUS_DESC = '1.3.6.1.4.1.9.9.13.1.5.1.2'; - public const OID_CISCO_ENV_MON_SUPPLY_STATE = '1.3.6.1.4.1.9.9.13.1.5.1.3'; - - /** @return string[] */ - public function getCiscoEnvMonFanStatusDescr() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_CISCO_ENV_MON_FAN_STATUS_DESRC); - } - - /** @return int[] */ - public function getCiscoEnvMonFanState() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_CISCO_ENV_MON_FAN_STATE); - } - - /** @return string[] */ - public function getCiscoEnvMonSupplyStatusDescr() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_CISCO_ENV_MON_SUPPLY_STATUS_DESC); - } - - /** @return int[] */ - public function getCiscoEnvMonSupplyState() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_CISCO_ENV_MON_SUPPLY_STATE); - } + public const OID_CISCO_ENV_MON_FAN_STATUS_DESRC = '.1.3.6.1.4.1.9.9.13.1.4.1.2'; + public const OID_CISCO_ENV_MON_FAN_STATE = '.1.3.6.1.4.1.9.9.13.1.4.1.3'; + public const OID_CISCO_ENV_MON_SUPPLY_STATUS_DESC = '.1.3.6.1.4.1.9.9.13.1.5.1.2'; + public const OID_CISCO_ENV_MON_SUPPLY_STATE = '.1.3.6.1.4.1.9.9.13.1.5.1.3'; } diff --git a/src/Mib/Coriant/Groove.php b/src/Mib/Coriant/Groove.php index dc712f1..5de31f5 100644 --- a/src/Mib/Coriant/Groove.php +++ b/src/Mib/Coriant/Groove.php @@ -4,13 +4,10 @@ namespace SimPod\PhpSnmp\Mib\Coriant; -use SimPod\PhpSnmp\Helper\TypeMapper; -use SimPod\PhpSnmp\Mib\MibBase; - /** * See Groove G30 release 2.1.0 https://mibs.observium.org/mib/CORIANT-GROOVE-MIB/ */ -class Groove extends MibBase +final class Groove { public const OID_SYSTEM_POWER_CONSUMPTION_CURRENT = '.1.3.6.1.4.1.42229.1.2.2.2.2'; public const OID_CARD_ADMIN_STATUS = '.1.3.6.1.4.1.42229.1.2.3.3.1.1.3'; @@ -38,160 +35,4 @@ class Groove extends MibBase public const OID_INVENTORY_SERIAL_NUMBER = '.1.3.6.1.4.1.42229.1.2.3.12.1.1.9'; public const OID_INVENTORY_FW_VERSION = '.1.3.6.1.4.1.42229.1.2.3.12.1.1.10'; public const OID_INVENTORY_PART_VERSION = '.1.3.6.1.4.1.42229.1.2.3.12.1.1.11'; - - /** @return float[] */ - public function getSystemPowerConsumptionCurrent() : array - { - return TypeMapper::stringsToFloats($this->getSnmp()->walk(self::OID_SYSTEM_POWER_CONSUMPTION_CURRENT)); - } - - /** @return int[] */ - public function getCardAdminStatus() : array - { - return $this->getSnmp()->walk(self::OID_CARD_ADMIN_STATUS); - } - - /** @return int[] */ - public function getCardOperStatus() : array - { - return $this->getSnmp()->walk(self::OID_CARD_OPER_STATUS); - } - - /** @return int[] */ - public function getCardFanSpeedRate() : array - { - return $this->getSnmp()->walk(self::OID_CARD_FAN_SPEED_RATE); - } - - /** @return float[] */ - public function getCardTemperature() : array - { - return TypeMapper::stringsToFloats($this->getSnmp()->walk(self::OID_CARD_TEMPERATURE)); - } - - /** @return string[] */ - public function getSubcardEquipmentNames() : array - { - return $this->getSnmp()->walk(self::OID_SUBCARD_EQUIPMENT_NAME); - } - - /** @return float[] */ - public function getPortRxOpticalPower() : array - { - return TypeMapper::stringsToFloats($this->getSnmp()->walk(self::OID_PORT_RX_OPTICAL_POWER)); - } - - /** @return float[] */ - public function getPortTxOpticalPower() : array - { - return TypeMapper::stringsToFloats($this->getSnmp()->walk(self::OID_PORT_TX_OPTICAL_POWER)); - } - - /** @return float[] */ - public function getPortRxOpticalPowerLane1() : array - { - return TypeMapper::stringsToFloats($this->getSnmp()->walk(self::OID_PORT_RX_OPTICAL_POWER_LANE_1)); - } - - /** @return float[] */ - public function getPortRxOpticalPowerLane2() : array - { - return TypeMapper::stringsToFloats($this->getSnmp()->walk(self::OID_PORT_RX_OPTICAL_POWER_LANE_2)); - } - - /** @return float[] */ - public function getPortRxOpticalPowerLane3() : array - { - return TypeMapper::stringsToFloats($this->getSnmp()->walk(self::OID_PORT_RX_OPTICAL_POWER_LANE_3)); - } - - /** @return float[] */ - public function getPortRxOpticalPowerLane4() : array - { - return TypeMapper::stringsToFloats($this->getSnmp()->walk(self::OID_PORT_RX_OPTICAL_POWER_LANE_4)); - } - - /** @return float[] */ - public function getPortTxOpticalPowerLane1() : array - { - return TypeMapper::stringsToFloats($this->getSnmp()->walk(self::OID_PORT_TX_OPTICAL_POWER_LANE_1)); - } - - /** @return float[] */ - public function getPortTxOpticalPowerLane2() : array - { - return TypeMapper::stringsToFloats($this->getSnmp()->walk(self::OID_PORT_TX_OPTICAL_POWER_LANE_2)); - } - - /** @return float[] */ - public function getPortTxOpticalPowerLane3() : array - { - return TypeMapper::stringsToFloats($this->getSnmp()->walk(self::OID_PORT_TX_OPTICAL_POWER_LANE_3)); - } - - /** @return float[] */ - public function getPortTxOpticalPowerLane4() : array - { - return TypeMapper::stringsToFloats($this->getSnmp()->walk(self::OID_PORT_TX_OPTICAL_POWER_LANE_4)); - } - - /** @return string[] */ - public function getPortNames() : array - { - return $this->getSnmp()->walk(self::OID_PORT_NAME); - } - - /** @return int[] */ - public function getPortAdminStatuses() : array - { - return $this->getSnmp()->walk(self::OID_PORT_ADMIN_STATUS); - } - - /** @return int[] */ - public function getPortOperStatuses() : array - { - return $this->getSnmp()->walk(self::OID_PORT_OPER_STATUS); - } - - /** @return string[] */ - public function getPortAliasNames() : array - { - return $this->getSnmp()->walk(self::OID_PORT_ALIAS_NAME); - } - - /** @return int[] */ - public function getInventoryEquipmentType() : array - { - return $this->getSnmp()->walk(self::OID_INVENTORY_EQUIPMENT_TYPE); - } - - /** @return string[] */ - public function getInventoryModuleType() : array - { - return $this->getSnmp()->walk(self::OID_INVENTORY_MODULE_TYPE); - } - - /** @return string[] */ - public function getInventorySerialNumber() : array - { - return $this->getSnmp()->walk(self::OID_INVENTORY_SERIAL_NUMBER); - } - - /** @return string[] */ - public function getInventoryFwVersion() : array - { - return $this->getSnmp()->walk(self::OID_INVENTORY_FW_VERSION); - } - - /** @return string[] */ - public function getInventoryVendor() : array - { - return $this->getSnmp()->walk(self::OID_INVENTORY_VENDOR); - } - - /** @return string[] */ - public function getInventoryPartVersion() : array - { - return $this->getSnmp()->walk(self::OID_INVENTORY_PART_VERSION); - } } diff --git a/src/Mib/Dcp/DcpAlarm.php b/src/Mib/Dcp/DcpAlarm.php index ea518ec..e034ef9 100644 --- a/src/Mib/Dcp/DcpAlarm.php +++ b/src/Mib/Dcp/DcpAlarm.php @@ -4,12 +4,10 @@ namespace SimPod\PhpSnmp\Mib\Dcp; -use SimPod\PhpSnmp\Mib\MibBase; - /** * See iso(1).org(3).dod(6).internet(1).private(4).enterprise(1).smartoptics(30826).dcp(2).dcpGeneric(2).dcpAlarm(1) */ -class DcpAlarm extends MibBase +final class DcpAlarm { public const OID_DCP_ALARM_LOG_LIST_INDEX = '.1.3.6.1.4.1.30826.2.2.2.2.2.1.1'; public const OID_DCP_ALARM_LOG_LIST_LOCATION = '.1.3.6.1.4.1.30826.2.2.2.2.2.1.2'; @@ -19,10 +17,4 @@ class DcpAlarm extends MibBase public const OID_DCP_ALARM_LOG_LIST_START_TIME = '.1.3.6.1.4.1.30826.2.2.2.2.2.1.6'; public const OID_DCP_ALARM_LOG_LIST_END_TIME = '.1.3.6.1.4.1.30826.2.2.2.2.2.1.7'; public const OID_DCP_ALARM_LOG_LIST_SEQ_NUMBER = '.1.3.6.1.4.1.30826.2.2.2.2.2.1.8'; - - /** @return array */ - public function getDcpAlarmLogListLocation() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_DCP_ALARM_LOG_LIST_LOCATION); - } } diff --git a/src/Mib/Dcp/DcpInterface.php b/src/Mib/Dcp/DcpInterface.php index 178ee03..d869186 100644 --- a/src/Mib/Dcp/DcpInterface.php +++ b/src/Mib/Dcp/DcpInterface.php @@ -4,12 +4,10 @@ namespace SimPod\PhpSnmp\Mib\Dcp; -use SimPod\PhpSnmp\Mib\MibBase; - /** * See iso(1).org(3).dod(6).internet(1).private(4).enterprise(1).smartoptics(30826).dcp(2).dcpGeneric(2).dcpInterface(1) */ -class DcpInterface extends MibBase +final class DcpInterface { public const OID_DCP_INTERFACE_INDEX = '.1.3.6.1.4.1.30826.2.2.1.1.1.1.1'; public const OID_DCP_INTERFACE_NAME = '.1.3.6.1.4.1.30826.2.2.1.1.1.1.2'; @@ -20,62 +18,4 @@ class DcpInterface extends MibBase public const OID_DCP_INTERFACE_FORMAT = '.1.3.6.1.4.1.30826.2.2.1.1.1.1.7'; public const OID_DCP_INTERFACE_WAVELENGTH = '.1.3.6.1.4.1.30826.2.2.1.1.1.1.8'; public const OID_DCP_INTERFACE_CHANNEL_ID = '.1.3.6.1.4.1.30826.2.2.1.1.1.1.9'; - - /** @return array */ - public function getDcpInterfaceIndex() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_DCP_INTERFACE_INDEX); - } - - /** @return array */ - public function getDcpInterfaceName() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_DCP_INTERFACE_NAME); - } - - /** @return array */ - public function getDcpInterfaceRxPower() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_DCP_INTERFACE_RX_POWER); - } - - /** @return array */ - public function getDcpInterfaceTxPower() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_DCP_INTERFACE_TX_POWER); - } - - /** - * The operational state for the interface. idle(1), down(2), up(3) - * - * @return array - */ - public function getDcpInterfaceStatus() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_DCP_INTERFACE_STATUS); - } - - /** @return array */ - public function getDcpInterfaceAlarm() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_DCP_INTERFACE_ALARM); - } - - /** @return array */ - public function getDcpInterfaceFormat() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_DCP_INTERFACE_FORMAT); - } - - /** @return array */ - public function getDcpInterfaceWavelength() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_DCP_INTERFACE_WAVELENGTH); - } - - /** @return array */ - public function getDcpInterfaceChannelId() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_DCP_INTERFACE_CHANNEL_ID); - } } diff --git a/src/Mib/DismanEvent.php b/src/Mib/DismanEvent.php deleted file mode 100644 index 70c9816..0000000 --- a/src/Mib/DismanEvent.php +++ /dev/null @@ -1,19 +0,0 @@ -getSnmp()->walkFirstDegree(self::OID_SYS_UP_TIME_INSTANCE); - } -} diff --git a/src/Mib/Entity.php b/src/Mib/Entity.php index c82bf10..0c359b0 100644 --- a/src/Mib/Entity.php +++ b/src/Mib/Entity.php @@ -7,7 +7,7 @@ /** * See RFC 4133 https://tools.ietf.org/html/rfc4133 */ -class Entity extends MibBase +final class Entity { public const OID_PHYSICAL_DESCRIPTION = '.1.3.6.1.2.1.47.1.1.1.1.2'; public const OID_PHYSICAL_VENDOR_TYPE = '.1.3.6.1.2.1.47.1.1.1.1.3'; @@ -18,77 +18,10 @@ class Entity extends MibBase public const OID_PHYSICAL_HARDWARE_REV = '.1.3.6.1.2.1.47.1.1.1.1.8'; public const OID_PHYSICAL_FIRMWARE_REV = '.1.3.6.1.2.1.47.1.1.1.1.9'; public const OID_PHYSICAL_SOFTWARE_REV = '.1.3.6.1.2.1.47.1.1.1.1.10'; - public const OID_PHYSICAL_SERIALNUM = '.1.3.6.1.2.1.47.1.1.1.1.11'; public const OID_PHYSICAL_SERIAL_NUM = '.1.3.6.1.2.1.47.1.1.1.1.11'; public const OID_PHYSICAL_MFG_NAME = '.1.3.6.1.2.1.47.1.1.1.1.12'; public const OID_PHYSICAL_MODEL_NAME = '.1.3.6.1.2.1.47.1.1.1.1.13'; public const OID_PHYSICAL_ALIAS = '.1.3.6.1.2.1.47.1.1.1.1.14'; public const OID_PHYSICAL_ASSET_ID = '.1.3.6.1.2.1.47.1.1.1.1.15'; public const OID_PHYSICAL_IS_FRU = '.1.3.6.1.2.1.47.1.1.1.1.16'; - - /** @return string[] */ - public function getPhysicalDescription() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_PHYSICAL_DESCRIPTION); - } - - /** @return string[] */ - public function getPhysicalVendorType() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_PHYSICAL_VENDOR_TYPE); - } - - /** @return int[] */ - public function getPhysicalContainedIn() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_PHYSICAL_CONTAINED_IN); - } - - /** @return int[] */ - public function getPhysicalClass() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_PHYSICAL_CLASS); - } - - /** @return string[] */ - public function getPhysicalName() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_PHYSICAL_NAME); - } - - /** @return string[] */ - public function getPhysicalHardwareRev() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_PHYSICAL_HARDWARE_REV); - } - - /** @return string[] */ - public function getPhysicalFirmwareRev() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_PHYSICAL_FIRMWARE_REV); - } - - /** @return string[] */ - public function getPhysicalSoftwareRev() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_PHYSICAL_SOFTWARE_REV); - } - - /** @return string[] */ - public function getPhysicalSerialNumber() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_PHYSICAL_SERIAL_NUM); - } - - /** @return string[] */ - public function getPhysicalMfgName() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_PHYSICAL_MFG_NAME); - } - - /** @return string[] */ - public function getPhysicalModelName() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_PHYSICAL_MODEL_NAME); - } } diff --git a/src/Mib/EntitySensor.php b/src/Mib/EntitySensor.php index f97dadf..a3c23e8 100644 --- a/src/Mib/EntitySensor.php +++ b/src/Mib/EntitySensor.php @@ -7,7 +7,7 @@ /** * See RFC 3433 https://tools.ietf.org/html/rfc3433 */ -class EntitySensor extends MibBase +final class EntitySensor { public const OID_PHYSICAL_SENSOR_TYPE = '.1.3.6.1.2.1.99.1.1.1.1'; public const OID_PHYSICAL_SENSOR_SCALE = '.1.3.6.1.2.1.99.1.1.1.2'; @@ -17,52 +17,4 @@ class EntitySensor extends MibBase public const OID_PHYSICAL_SENSOR_UNITS_DISPLAY = '.1.3.6.1.2.1.99.1.1.1.6'; public const OID_PHYSICAL_SENSOR_VALUE_TIME_STAMP = '.1.3.6.1.2.1.99.1.1.1.7'; public const OID_PHYSICAL_SENSOR_VALUE_UPDATE_RATE = '.1.3.6.1.2.1.99.1.1.1.8'; - - /** @return int[] */ - public function getPhysicalSensorType() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_PHYSICAL_SENSOR_TYPE); - } - - /** @return int[] */ - public function getPhysicalSensorScale() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_PHYSICAL_SENSOR_SCALE); - } - - /** @return int[] */ - public function getPhysicalSensorPrecision() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_PHYSICAL_SENSOR_PRECISION); - } - - /** @return int[] */ - public function getPhysicalSensorValue() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_PHYSICAL_SENSOR_VALUE); - } - - /** @return int[] */ - public function getPhysicalSensorOperStatus() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_PHYSICAL_SENSOR_OPER_STATUS); - } - - /** @return string[] */ - public function getPhysicalSensorUnitsDisplay() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_PHYSICAL_SENSOR_UNITS_DISPLAY); - } - - /** @return int[] */ - public function getPhysicalSensorValueTimeStamp() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_PHYSICAL_SENSOR_VALUE_TIME_STAMP); - } - - /** @return int[] */ - public function getPhysicalSensorValueUpdateRate() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_PHYSICAL_SENSOR_VALUE_UPDATE_RATE); - } } diff --git a/src/Mib/EntityState.php b/src/Mib/EntityState.php index 78a3791..63552e9 100644 --- a/src/Mib/EntityState.php +++ b/src/Mib/EntityState.php @@ -7,20 +7,8 @@ /** * See RFC 4268 https://tools.ietf.org/html/rfc4268 */ -class EntityState extends MibBase +final class EntityState { public const OID_ENT_STATE_ADMIN = '.1.3.6.1.2.1.131.1.1.1.2'; public const OID_ENT_STATE_OPER = '.1.3.6.1.2.1.131.1.1.1.3'; - - /** @return int[] */ - public function getEntStateAdmin() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_ENT_STATE_ADMIN); - } - - /** @return int[] */ - public function getEntStateOper() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_ENT_STATE_OPER); - } } diff --git a/src/Mib/HostResources.php b/src/Mib/HostResources.php index b282731..33bbe35 100644 --- a/src/Mib/HostResources.php +++ b/src/Mib/HostResources.php @@ -7,7 +7,7 @@ /** * See RFC 2790 https://tools.ietf.org/html/rfc2790 */ -class HostResources extends MibBase +final class HostResources { public const OID_HOST = '.1.3.6.1.2.1.25'; public const OID_HR_SYSTEM = '.1.3.6.1.2.1.25.1'; @@ -113,58 +113,4 @@ class HostResources extends MibBase public const OID_HR_SWRUN_GROUP = '.1.3.6.1.2.1.25.7.3.4'; public const OID_HR_SWRUN_PERF_GROUP = '.1.3.6.1.2.1.25.7.3.5'; public const OID_HR_SWINSTALLED_GROUP = '.1.3.6.1.2.1.25.7.3.6'; - - /** @return int[] */ - public function getHost() : array - { - return $this->getSnmp()->walk(self::OID_HOST); - } - - /** @return string[] */ - public function getHrStorageType() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_HR_STORAGE_TYPE); - } - - /** @return string[] */ - public function getHrStorageDescr() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_HR_STORAGE_DESCR); - } - - /** @return int[] */ - public function getHrStorageSize() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_HR_STORAGE_SIZE); - } - - /** @return int[] */ - public function getHrStorageUsed() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_HR_STORAGE_USED); - } - - /** @return int[] */ - public function getHrDeviceType() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_HR_DEVICE_TYPE); - } - - /** @return string[] */ - public function getHrDeviceDescr() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_HR_DEVICE_DESCR); - } - - /** @return int[] */ - public function getHrDeviceStatus() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_HR_DEVICE_STATUS); - } - - /** @return int[] */ - public function getHrProcessorLoad() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_HR_PROCESSOR_LOAD); - } } diff --git a/src/Mib/Iface.php b/src/Mib/Iface.php index dd1d479..d404fe8 100644 --- a/src/Mib/Iface.php +++ b/src/Mib/Iface.php @@ -4,9 +4,7 @@ namespace SimPod\PhpSnmp\Mib; -use SimPod\PhpSnmp\OidWithIndex; - -class Iface extends MibBase +final class Iface { public const OID_ADMIN_STATUS = '.1.3.6.1.2.1.2.2.1.7'; public const OID_ALIAS = '.1.3.6.1.2.1.31.1.1.1.18'; @@ -29,208 +27,4 @@ class Iface extends MibBase public const OID_IN_DISCARDS = '.1.3.6.1.2.1.2.2.1.13'; public const OID_OUT_DISCARDS = '.1.3.6.1.2.1.2.2.1.19'; public const OID_STACK_STATUS = '.1.3.6.1.2.1.31.1.2.1.3'; - - /** @return int[] */ - public function getAdminStatuses() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_ADMIN_STATUS); - } - - /** @return string[] */ - public function getAliases() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_ALIAS); - } - - /** @return string[] */ - public function getDescriptions() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_DESCRIPTION); - } - - /** @return string[] */ - public function getHcInOctets() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_HC_IN_OCTETS); - } - - public function getHcInOctetsForIndex(int $index) : string - { - return $this->getSnmp()->walkFirstDegree((string) OidWithIndex::new(self::OID_HC_IN_OCTETS, $index))[$index]; - } - - /** @return string[] */ - public function getHcOutOctets() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_HC_OUT_OCTETS); - } - - public function getHcOutOctetsForIndex(int $index) : string - { - return $this->getSnmp()->walkFirstDegree((string) OidWithIndex::new(self::OID_HC_OUT_OCTETS, $index))[$index]; - } - - /** @return string[] */ - public function getHcInBroadcastPackets() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_HC_IN_BROADCAST_PACKETS); - } - - public function getHcInBroadcastPacketsForIndex(int $index) : string - { - return $this->getSnmp()->walkFirstDegree((string) OidWithIndex::new( - self::OID_HC_IN_BROADCAST_PACKETS, - $index - ))[$index]; - } - - /** @return string[] */ - public function getHcOutBroadcastPackets() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_HC_OUT_BROADCAST_PACKETS); - } - - public function getHcOutBroadcastPacketsForIndex(int $index) : string - { - return $this->getSnmp()->walkFirstDegree((string) OidWithIndex::new( - self::OID_HC_OUT_BROADCAST_PACKETS, - $index - ))[$index]; - } - - /** @return string[] */ - public function getHcInMulticastPackets() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_HC_IN_MULTICAST_PACKETS); - } - - public function getHcInMulticastPacketsForIndex(int $index) : string - { - return $this->getSnmp()->walkFirstDegree((string) OidWithIndex::new( - self::OID_HC_IN_MULTICAST_PACKETS, - $index - ))[$index]; - } - - /** @return string[] */ - public function getHcOutMulticastPackets() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_HC_OUT_MULTICAST_PACKETS); - } - - public function getHcOutMulticastPacketsForIndex(int $index) : string - { - return $this->getSnmp()->walkFirstDegree((string) OidWithIndex::new( - self::OID_HC_OUT_MULTICAST_PACKETS, - $index - ))[$index]; - } - - /** @return string[] */ - public function getHcInUnicastPackets() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_HC_IN_UNICAST_PACKETS); - } - - public function getHcInUnicastPacketsForIndex(int $index) : string - { - return $this->getSnmp()->walkFirstDegree((string) OidWithIndex::new( - self::OID_HC_IN_UNICAST_PACKETS, - $index - ))[$index]; - } - - /** @return string[] */ - public function getHcOutUnicastPackets() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_HC_OUT_UNICAST_PACKETS); - } - - public function getHcOutUnicastPacketsForIndex(int $index) : string - { - return $this->getSnmp()->walkFirstDegree((string) OidWithIndex::new( - self::OID_HC_OUT_UNICAST_PACKETS, - $index - ))[$index]; - } - - /** @return int[] */ - public function getInDiscards() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_IN_DISCARDS); - } - - public function getInDiscardsForIndex(int $index) : int - { - return $this->getSnmp()->walkFirstDegree((string) OidWithIndex::new(self::OID_IN_DISCARDS, $index))[$index]; - } - - /** @return int[] */ - public function getOutDiscards() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_OUT_DISCARDS); - } - - public function getOutDiscardsForIndex(int $index) : int - { - return $this->getSnmp()->walkFirstDegree((string) OidWithIndex::new(self::OID_OUT_DISCARDS, $index))[$index]; - } - - /** @return int[] */ - public function getInErrors() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_IN_ERRORS); - } - - public function getInErrorsForIndex(int $index) : int - { - return $this->getSnmp()->walkFirstDegree((string) OidWithIndex::new(self::OID_IN_ERRORS, $index))[$index]; - } - - /** @return int[] */ - public function getOutErrors() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_OUT_ERRORS); - } - - public function getOutErrorsForIndex(int $index) : int - { - return $this->getSnmp()->walkFirstDegree((string) OidWithIndex::new(self::OID_OUT_ERRORS, $index))[$index]; - } - - /** @return string[] */ - public function getNames() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_NAME); - } - - /** @return int[] */ - public function getOperStatuses() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_OPER_STATUS); - } - - /** @return int[] */ - public function getSpeeds() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_SPEED); - } - - /** @return int[] */ - public function getHcSpeeds() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_HC_SPEED); - } - - /** @return int[] */ - public function getTypes() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_TYPE); - } - - /** @return mixed[] */ - public function getStackTable() : array - { - return $this->getSnmp()->walk(self::OID_STACK_STATUS); - } } diff --git a/src/Mib/Ip.php b/src/Mib/Ip.php index 044e972..a29ab41 100644 --- a/src/Mib/Ip.php +++ b/src/Mib/Ip.php @@ -4,20 +4,8 @@ namespace SimPod\PhpSnmp\Mib; -class Ip extends MibBase +final class Ip { public const OID_IP_ADDRESS = '.1.3.6.1.2.1.4.20.1.1'; public const OID_IP_NET_TO_MEDIA_PHYS_ADDRESS = '.1.3.6.1.2.1.4.22.1.2'; - - /** @return string[] */ - public function getIpAddress() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_IP_ADDRESS); - } - - /** @return string[] */ - public function getIpNetToMediaPhysAddress() : array - { - return $this->getSnmp()->walk(self::OID_IP_NET_TO_MEDIA_PHYS_ADDRESS); - } } diff --git a/src/Mib/Lldp.php b/src/Mib/Lldp.php index 475f997..ab8eb55 100644 --- a/src/Mib/Lldp.php +++ b/src/Mib/Lldp.php @@ -4,34 +4,10 @@ namespace SimPod\PhpSnmp\Mib; -final class Lldp extends MibBase +final class Lldp { public const OID_LOCAL_PORT_ID = '.1.0.8802.1.1.2.1.3.7.1.3'; public const OID_REMOTE_PORT_ID = '.1.0.8802.1.1.2.1.4.1.1.7'; public const OID_REMOTE_SYS_NAME = '.1.0.8802.1.1.2.1.4.1.1.9'; public const OID_PORT_CONFIG_ADMIN_STATUS = '.1.0.8802.1.1.2.1.1.6.1.2'; - - /** @return string[] */ - public function getLocalPortIds() : array - { - return $this->getSnmp()->walk(self::OID_LOCAL_PORT_ID); - } - - /** @return string[] */ - public function getRemotePortIds() : array - { - return $this->getSnmp()->walk(self::OID_REMOTE_PORT_ID); - } - - /** @return string[] */ - public function getRemoteSysNames() : array - { - return $this->getSnmp()->walk(self::OID_REMOTE_SYS_NAME); - } - - /** @return int[] */ - public function getPortConfigAdminStatus() : array - { - return $this->getSnmp()->walk(self::OID_PORT_CONFIG_ADMIN_STATUS); - } } diff --git a/src/Mib/Mib.php b/src/Mib/Mib.php deleted file mode 100644 index c6ff7b6..0000000 --- a/src/Mib/Mib.php +++ /dev/null @@ -1,14 +0,0 @@ - null, - HostResources::OID_HR_DEVICE_TYPES => null, - HostResources::OID_HR_FSTYPES => null, - ]; - - /** @var Snmp */ - private $snmp; - - public function __construct(Snmp $snmp) - { - $this->snmp = $snmp; - } - - public function getSnmp() : Snmp - { - return $this->snmp; - } -} diff --git a/src/Mib/Object/EntityClass.php b/src/Mib/Object/EntityClass.php index 03e5c53..a8ddcdc 100644 --- a/src/Mib/Object/EntityClass.php +++ b/src/Mib/Object/EntityClass.php @@ -4,9 +4,7 @@ namespace SimPod\PhpSnmp\Mib\Object; -use Consistence\Enum\Enum; - -class EntityClass extends Enum +final class EntityClass { public const OTHER = 1; public const UNKNOWN = 2; @@ -19,59 +17,4 @@ class EntityClass extends Enum public const MODULE = 9; public const PORT = 10; public const STACK = 11; - - public static function getOther() : self - { - return self::get(self::OTHER); - } - - public static function getUnknown() : self - { - return self::get(self::UNKNOWN); - } - - public static function getChassis() : self - { - return self::get(self::CHASSIS); - } - - public static function getBackplane() : self - { - return self::get(self::BACKPLANE); - } - - public static function getContainer() : self - { - return self::get(self::CONTAINER); - } - - public static function getPowerSupply() : self - { - return self::get(self::POWER_SUPPLY); - } - - public static function getFan() : self - { - return self::get(self::FAN); - } - - public static function getSensor() : self - { - return self::get(self::SENSOR); - } - - public static function getModule() : self - { - return self::get(self::MODULE); - } - - public static function getPort() : self - { - return self::get(self::PORT); - } - - public static function getStack() : self - { - return self::get(self::STACK); - } } diff --git a/src/Mib/Object/HrDeviceStatus.php b/src/Mib/Object/HrDeviceStatus.php index 8fdc542..a693f0c 100644 --- a/src/Mib/Object/HrDeviceStatus.php +++ b/src/Mib/Object/HrDeviceStatus.php @@ -4,7 +4,7 @@ namespace SimPod\PhpSnmp\Mib\Object; -class HrDeviceStatus +final class HrDeviceStatus { public const UNKNOWN = 1; public const RUNNING = 2; diff --git a/src/Mib/Object/HrDeviceType.php b/src/Mib/Object/HrDeviceType.php index 37422fc..993da60 100644 --- a/src/Mib/Object/HrDeviceType.php +++ b/src/Mib/Object/HrDeviceType.php @@ -4,7 +4,7 @@ namespace SimPod\PhpSnmp\Mib\Object; -class HrDeviceType +final class HrDeviceType { public const HR_DEVICE_OTHER = 1; public const HR_DEVICE_UNKNOWN = 2; diff --git a/src/Mib/SNMPFramework.php b/src/Mib/SNMPFramework.php deleted file mode 100644 index 6c64f84..0000000 --- a/src/Mib/SNMPFramework.php +++ /dev/null @@ -1,16 +0,0 @@ -getSnmp()->walkFirstDegree(self::OID_SNMP_ENGINE_TIME); - } -} diff --git a/src/Mib/SnmpFramework.php b/src/Mib/SnmpFramework.php new file mode 100644 index 0000000..e33a1a4 --- /dev/null +++ b/src/Mib/SnmpFramework.php @@ -0,0 +1,10 @@ +getSnmp()->walkFirstDegree(self::OID_DESCRIPTION); - } - - /** @return string[] */ - public function getLocation() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_LOCATION); - } - - /** @return string[] */ - public function getName() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_NAME); - } - - /** @return int[] */ - public function getUptime() : array - { - return $this->getSnmp()->walkFirstDegree(self::OID_UPTIME); - } } diff --git a/src/OidWithIndex.php b/src/OidWithIndex.php deleted file mode 100644 index d6447c7..0000000 --- a/src/OidWithIndex.php +++ /dev/null @@ -1,32 +0,0 @@ -oid = $oid; - $this->index = $index; - } - - public static function new(string $oid, int $index) : self - { - return new self($oid, $index); - } - - public function __toString() : string - { - return sprintf('%s.%d', $this->oid, $this->index); - } -} diff --git a/src/Snmp.php b/src/Snmp.php deleted file mode 100644 index 18e5948..0000000 --- a/src/Snmp.php +++ /dev/null @@ -1,302 +0,0 @@ -community = $community; - $this->host = $host; - $this->retry = $retry; - $this->timeout = $timeout; - $this->version = $version; - - $this->secName = $community; - $this->secLevel = $secLevel; - $this->authProtocol = $authProtocol; - $this->authPassphrase = $authPassphrase; - $this->privProtocol = $privProtocol; - $this->privPassphrase = $privPassphrase; - snmp_set_oid_output_format(SNMP_OID_OUTPUT_NUMERIC); - } - - /** @return mixed[] */ - public function walkFirstDegree(string $walkedOid) : array - { - $result = $this->realWalk($walkedOid); - - $processedResult = []; - - $oidPrefix = null; - foreach ($result as $oid => $value) { - $length = strrpos($oid, '.'); - assert(is_int($length)); - - if ($oidPrefix !== null && $oidPrefix !== substr($oid, 0, $length)) { - throw new SnmpFailed('Requested OID tree is not a first degree indexed SNMP value'); - } - - $oidPrefix = substr($oid, 0, $length); - - $processedResult[substr($oid, $length + 1)] = $this->parseSnmpValue($value); - } - - return $processedResult; - } - - /** @return mixed[] */ - public function realWalk(string $oid) : array - { - switch ($this->getVersion()) { - case '1': - $result = @snmprealwalk( - $this->host, - $this->community, - $oid, - $this->getTimeout(), - $this->getRetry() - ); - break; - case '2c': - $result = @snmp2_real_walk( - $this->host, - $this->community, - $oid, - $this->getTimeout(), - $this->getRetry() - ); - break; - case '3': - $result = @snmp3_real_walk( - $this->host, - $this->getSecName(), - $this->getSecLevel(), - $this->getAuthProtocol(), - $this->getAuthPassphrase(), - $this->getPrivProtocol(), - $this->getPrivPassphrase(), - $oid, - $this->getTimeout(), - $this->getRetry() - ); - break; - default: - throw new SnmpFailed('Invalid SNMP version: ' . $this->getVersion()); - } - - if ($result === false) { - throw new SnmpFailed('Could not perform walk for OID ' . $oid); - } - - return $result; - } - - public function getSecName() : string - { - return $this->secName; - } - - public function getSecLevel() : string - { - return $this->secLevel; - } - - public function getAuthProtocol() : string - { - return $this->authProtocol; - } - - public function getAuthPassphrase() : string - { - return $this->authPassphrase; - } - - public function getPrivProtocol() : string - { - return $this->privProtocol; - } - - public function getPrivPassphrase() : string - { - return $this->privPassphrase; - } - - /** @return mixed[] */ - public function walk(string $oid) : array - { - $rawResult = $this->realWalk($oid); - - $result = []; - foreach ($rawResult as $oidKey => $value) { - $result[$oidKey] = $this->parseSnmpValue($value); - } - - return $result; - } - - private function getVersion() : string - { - return $this->version; - } - - private function getTimeout() : int - { - return $this->timeout; - } - - private function getRetry() : int - { - return $this->retry; - } - - /** @return int|string */ - private function parseSnmpValue(string $rawValue) - { - if ($rawValue === '""' || $rawValue === '') { - return ''; - } - - [$type, $value] = explode(':', $rawValue, 2); - - $value = trim($value); - - switch ($type) { - case 'STRING': - $resolvedValue = strpos($value, '"') === 0 ? trim(substr(substr($value, 1), 0, -1)) : $value; - break; - - case 'INTEGER': - if (is_numeric($value)) { - $resolvedValue = (int) $value; - } else { - // find the first digit and offset the string to that point - // just in case there is some mib strangeness going on - preg_match('/\d/', $value, $m, PREG_OFFSET_CAPTURE); - $resolvedValue = (int) substr($value, $m[0][1]); - } - - break; - - case 'Float': - $resolvedValue = (float) $value; - break; - - case 'BITS': - // This is vaguely implemented - $resolvedValue = $value; - break; - - case 'Counter32': - $resolvedValue = (int) $value; - break; - - case 'Counter64': - $resolvedValue = $value; - break; - - case 'Gauge32': - $resolvedValue = (int) $value; - break; - - case 'Hex-STRING': - $resolvedValue = $value; - break; - - case 'IpAddress': - $resolvedValue = $value; - break; - - case 'Opaque': - $resolvedValue = $this->parseSnmpValue(str_replace('Oprague: ', '', $value)); - break; - - case 'OID': - $objectTypes = MibBase::OBJECT_TYPES; - $reversedOidParts = explode('.', strrev($value), 2); - $objectTypeOidBase = strrev($reversedOidParts[1]); - $resolvedValue = array_key_exists( - $objectTypeOidBase, - $objectTypes - ) ? (int) $reversedOidParts[0] : $value; - break; - - case 'Timeticks': - $length = strrpos($value, ')'); - assert(is_int($length)); - $resolvedValue = (int) substr($value, 1, $length - 1); - break; - - default: - throw new SnmpFailed('ERR: Unhandled SNMP return type: ' . $type); - } - - return $resolvedValue; - } -} diff --git a/src/Transport/ApiSnmpClient.php b/src/Transport/ApiSnmpClient.php new file mode 100644 index 0000000..0fd60e0 --- /dev/null +++ b/src/Transport/ApiSnmpClient.php @@ -0,0 +1,221 @@ +client = $client; + $this->requestFactory = $requestFactory; + $this->streamFactory = $streamFactory; + $this->apiHostUrl = $apiHostUrl; + $this->host = $host; + $this->community = $community; + $this->timeout = $timeout; + $this->retries = $retries; + $this->version = $version; + } + + /** @inheritDoc */ + public function get(array $oids) : array + { + return $this->batch([Request::get($oids)])[0]; + } + + /** @inheritDoc */ + public function getNext(array $oids) : array + { + return $this->batch([Request::getNext($oids)])[0]; + } + + /** @inheritDoc */ + public function walk(string $oid, int $maxRepetitions = 40) : array + { + return $this->batch([Request::walk($oid, $maxRepetitions)])[0]; + } + + /** @inheritDoc */ + public function batch(array $requests) : array + { + if ($requests === []) { + throw NoRequestsProvided::new(); + } + + $requestParameters = [ + 'host' => $this->host, + 'community' => $this->community, + 'version' => $this->version, + 'timeout' => $this->timeout, + 'retries' => $this->retries, + 'requests' => array_map( + static function (Request $request) : array { + $requestArray = [ + 'request_type' => $request->type, + 'oids' => $request->oids, + ]; + + if ($request->maxRepetitions !== null) { + $requestArray['max_repetitions'] = $request->maxRepetitions; + } + + return $requestArray; + }, + array_values($requests) + ), + ]; + + $requestKeys = array_keys($requests); + $result = []; + + foreach ($this->doExecuteRequest($requestParameters) as $requestNo => $rawRequestResult) { + $requestResult = []; + for ($i = 0, $iMax = count($rawRequestResult); $i < $iMax; $i += 2) { + $key = $rawRequestResult[$i]; + $value = $rawRequestResult[$i + 1]; + + $requestResult[$key] = $value; + } + + $result[$requestKeys[$requestNo]] = $requestResult; + } + + return $result; + } + + /** + * @param array $requestParameters + * + * @return list> + */ + private function doExecuteRequest(array $requestParameters) : array + { + $request = $this->requestFactory->createRequest('POST', $this->apiHostUrl . self::API_PATH) + ->withBody($this->streamFactory->createStream(json_encode($requestParameters))); + + try { + $response = $this->client->sendRequest($request); + } catch (Throwable $throwable) { + throw GeneralException::fromThrowable($throwable, $this->getRequestsOids($requestParameters)); + } + + try { + /** @var array{error: string}|array{result: list>} $result */ + $result = json_decode((string) $response->getBody(), true, 5, JSON_BIGINT_AS_STRING); + } catch (JsonException $throwable) { + $error = sprintf( + 'Response is not valid JSON [HTTP %d]: "%s"', + $response->getStatusCode(), + (string) $response->getBody() + ); + + throw GeneralException::new($error, $throwable, $this->getRequestsOids($requestParameters)); + } + + if (array_key_exists('error', $result)) { + if (preg_match('~no such object: (.+)~', $result['error'], $matches) === 1) { + throw NoSuchObjectExists::fromOid($matches[1]); + } + + if (preg_match('~no such instance: (.+)~', $result['error'], $matches) === 1) { + throw NoSuchInstanceExists::fromOid($matches[1]); + } + + if (preg_match('~end of mib: (.+)~', $result['error'], $matches) === 1) { + throw EndOfMibReached::fromOid($matches[1]); + } + + throw GeneralException::new($result['error'], null, $this->getRequestsOids($requestParameters)); + } + + if ($response->getStatusCode() !== 200) { + $error = sprintf( + 'Unexpected HTTP status code: %d, response: "%s"', + $response->getStatusCode(), + (string) $response->getBody() + ); + + throw GeneralException::new($error, null, $this->getRequestsOids($requestParameters)); + } + + return $result['result']; + } + + /** + * @param array $requestParameters + * + * @return list + */ + private function getRequestsOids(array $requestParameters) : array + { + $oids = []; + foreach ($requestParameters['requests'] as $request) { + foreach ($request['oids'] as $oid) { + $oids[] = $oid; + } + } + + return $oids; + } +} diff --git a/src/Transport/Cli/ProcessExecutor.php b/src/Transport/Cli/ProcessExecutor.php new file mode 100644 index 0000000..b06690d --- /dev/null +++ b/src/Transport/Cli/ProcessExecutor.php @@ -0,0 +1,11 @@ +commandTimeout = $commandTimeout; + } + + /** @inheritDoc */ + public function execute(array $args) : string + { + try { + $process = new Process($args); + $process->setTimeout($this->commandTimeout); + $process->run(); + + if (! $process->isSuccessful()) { + throw GeneralException::new(trim($process->getErrorOutput())); + } + + return $process->getOutput(); + } catch (Throwable $throwable) { + if ($throwable instanceof SnmpException) { + throw $throwable; + } + + throw GeneralException::fromThrowable($throwable); + } + } +} diff --git a/src/Transport/CliSnmpClient.php b/src/Transport/CliSnmpClient.php new file mode 100644 index 0000000..6560be7 --- /dev/null +++ b/src/Transport/CliSnmpClient.php @@ -0,0 +1,228 @@ +processExecutor = $processExecutor ?? new SymfonyProcessProcessExecutor(120); + $this->processArgs = [ + '-ObenU', + '--hexOutputLength=0', + '-t', + (string) $timeout, + '-r', + (string) $retries, + '-v', + $version, + '-c', + $community, + $host, + ]; + $this->useBulk = $version === '2c'; + } + + /** @inheritDoc */ + public function get(array $oids) : array + { + try { + $output = $this->processExecutor->execute(array_merge(['snmpget'], $this->processArgs, $oids)); + } catch (Throwable $throwable) { + // check for SNMP v1 + if (preg_match('~\(noSuchName\).+Failed object: (.+?)$~ms', $throwable->getMessage(), $matches) === 1) { + throw NoSuchInstanceExists::fromOid($matches[1]); + } + + throw $this->processException($throwable, $oids); + } + + return $this->processOutput($output); + } + + /** @inheritDoc */ + public function getNext(array $oids) : array + { + try { + $output = $this->processExecutor->execute(array_merge(['snmpgetnext'], $this->processArgs, $oids)); + } catch (Throwable $throwable) { + // check for SNMP v1 + if (preg_match('~\(noSuchName\).+Failed object: (.+?)$~ms', $throwable->getMessage(), $matches) === 1) { + throw EndOfMibReached::fromOid($matches[1]); + } + + throw $this->processException($throwable, $oids); + } + + return $this->processOutput($output); + } + + /** @inheritDoc */ + public function walk(string $oid, int $maxRepetitions = 40) : array + { + $walker = 'snmpwalk'; + $args = $this->processArgs; + if ($this->useBulk) { + $walker = 'snmpbulkwalk'; + $args = array_merge($args, ['-Cr' . $maxRepetitions]); + } + + try { + $output = $this->processExecutor->execute(array_merge([$walker], $args, [$oid])); + } catch (Throwable $throwable) { + throw $this->processException($throwable, [$oid]); + } + + $result = $this->processOutput($output); + if ($result === []) { + throw NoSuchInstanceExists::fromOid($oid); + } + + return $result; + } + + /** @return array */ + private function processOutput(string $output) : array + { + $outputLength = strlen($output); + $lineStartPos = 0; + $result = []; + + while ($lineStartPos < $outputLength) { + $newLinePos = strpos($output, "\n", $lineStartPos); + assert($newLinePos !== false); + + $quotesPos = strpos($output, '"', $lineStartPos); + if ($quotesPos === false || $quotesPos > $newLinePos) { + $lineEndPos = $this->getLineEndPos($output, $newLinePos, $lineStartPos); + + $result = $this->processOutputLine($result, substr($output, $lineStartPos, $lineEndPos)); + + $lineStartPos = $newLinePos + 1; + + continue; + } + + while (true) { + $endQuotesPos = strpos($output, '"', $quotesPos + 1); + assert($endQuotesPos !== false); + + if ($output[$endQuotesPos - 1] !== '\\' && $output[$endQuotesPos - 2] !== '\\') { + break; + } + + $quotesPos = $endQuotesPos; + } + + $newLinePos = strpos($output, "\n", $endQuotesPos); + assert($newLinePos !== false); + + $lineEndPos = $this->getLineEndPos($output, $newLinePos, $lineStartPos); + + $result = $this->processOutputLine($result, substr($output, $lineStartPos, $lineEndPos)); + + $lineStartPos = $newLinePos + 1; + } + + return $result; + } + + private function getLineEndPos(string $output, int $newLinePos, int $i) : int + { + if ($output[$newLinePos - 1] === "\r") { + return $newLinePos - $i - 1; + } + + return $newLinePos - $i; + } + + /** + * @param array $result + * + * @return array + */ + private function processOutputLine(array $result, string $line) : array + { + // check for SNMP v1 + if (strpos($line, 'End of MIB') === 0) { + if ($result !== []) { + return $result; + } + + throw EndOfMibReached::new(); + } + + [$oid, $value] = explode(' = ', $line, 2); + + if (strpos($value, 'No Such Object') === 0) { + throw NoSuchObjectExists::fromOid($oid); + } + + if (strpos($value, 'No Such Instance') === 0) { + throw NoSuchInstanceExists::fromOid($oid); + } + + if (strpos($value, 'No more variables left in this MIB View') === 0) { + if ($result !== []) { + return $result; + } + + throw EndOfMibReached::fromOid($oid); + } + + $result[$oid] = ValueParser::parse($value); + + return $result; + } + + /** @param list $oids */ + private function processException(Throwable $throwable, array $oids) : Throwable + { + if ($throwable instanceof SnmpException) { + return GeneralException::fromThrowable($throwable, $oids); + } + + return GeneralException::new('Failed to execute SNMP CLI command', $throwable, $oids); + } +} diff --git a/src/Transport/ExtensionSnmpClient.php b/src/Transport/ExtensionSnmpClient.php new file mode 100644 index 0000000..0d5dae4 --- /dev/null +++ b/src/Transport/ExtensionSnmpClient.php @@ -0,0 +1,129 @@ +snmp = new SNMP($snmpVersion, $host, $community, $timeoutMs, $retry); + $this->snmp->oid_output_format = SNMP_OID_OUTPUT_NUMERIC; + $this->snmp->exceptions_enabled = SNMP::ERRNO_ANY; + if ($snmpVersion !== SNMP::VERSION_3) { + return; + } + + $this->snmp->setSecurity($secLevel, $authProtocol, $authPassphrase, $privProtocol, $privPassphrase); + } + + /** @inheritDoc */ + public function get(array $oids) : array + { + try { + $output = $this->snmp->get($oids); + } catch (Throwable $throwable) { + throw $this->processException($throwable, $oids); + } + + return $this->processOutput($output); + } + + /** @inheritDoc */ + public function getNext(array $oids) : array + { + try { + $output = $this->snmp->getnext($oids); + } catch (Throwable $throwable) { + throw $this->processException($throwable, $oids); + } + + return $this->processOutput($output); + } + + /** @inheritDoc */ + public function walk(string $oid, int $maxRepetitions = 40) : array + { + try { + $output = $this->snmp->walk($oid, false, $maxRepetitions); + } catch (Throwable $throwable) { + throw $this->processException($throwable, [$oid]); + } + + return $this->processOutput($output); + } + + /** + * @param array $output + * + * @return array + */ + private function processOutput(array $output) : array + { + $result = []; + foreach ($output as $oid => $value) { + $result[$oid] = ValueParser::parse($value); + } + + return $result; + } + + /** @param list $oids */ + private function processException(Throwable $throwable, array $oids) : Throwable + { + if (strpos($throwable->getMessage(), 'No Such Object') !== false) { + return NoSuchObjectExists::fromThrowable($throwable); + } + + if (strpos($throwable->getMessage(), 'No Such Instance') !== false + || strpos($throwable->getMessage(), 'noSuchName') !== false) { + return NoSuchInstanceExists::fromThrowable($throwable); + } + + if (strpos($throwable->getMessage(), 'No more variables left in this MIB View') !== false) { + return EndOfMibReached::fromThrowable($throwable); + } + + return GeneralException::fromThrowable($throwable, $oids); + } +} diff --git a/src/Transport/FallbackSnmpClient.php b/src/Transport/FallbackSnmpClient.php new file mode 100644 index 0000000..ab9357d --- /dev/null +++ b/src/Transport/FallbackSnmpClient.php @@ -0,0 +1,95 @@ + */ + private $snmpClients; + + /** @param iterable $snmpClients */ + public function __construct(LoggerInterface $logger, iterable $snmpClients) + { + if ($snmpClients === []) { + throw GeneralException::new('No SNMP clients provided'); + } + + $this->logger = $logger; + $this->snmpClients = $snmpClients; + } + + /** @inheritDoc */ + public function get(array $oids) : array + { + return $this->tryClients( + static function (SnmpClient $client) use ($oids) : array { + return $client->get($oids); + } + ); + } + + /** @inheritDoc */ + public function getNext(array $oids) : array + { + return $this->tryClients( + static function (SnmpClient $client) use ($oids) : array { + return $client->getNext($oids); + } + ); + } + + /** @inheritDoc */ + public function walk(string $oid, int $maxRepetitions = 40) : array + { + return $this->tryClients( + static function (SnmpClient $client) use ($oid, $maxRepetitions) : array { + return $client->walk($oid, $maxRepetitions); + } + ); + } + + /** @inheritDoc */ + public function batch(array $requests) : array + { + // @phpstan-ignore-next-line some bug in phpstan for the moment, but we know the types are correct... + return $this->tryClients( + static function (SnmpClient $client) use ($requests) : array { + return $client->batch($requests); + } + ); + } + + /** + * @param callable(SnmpClient): array $requestCallback + * + * @return array + */ + private function tryClients(callable $requestCallback) : array + { + foreach ($this->snmpClients as $key => $snmpClient) { + try { + return $requestCallback($snmpClient); + } catch (GeneralException $exception) { + $this->logger->warning( + 'SNMP request failed', + [ + 'clientKey' => $key, + 'client' => $snmpClient, + 'exception' => $exception, + ] + ); + } + } + + /** @phpstan-ignore-next-line $exception will always be there */ + throw GeneralException::new('All SNMP clients failed, last error: ' . $exception->getMessage(), $exception); + } +} diff --git a/src/Transport/Request.php b/src/Transport/Request.php new file mode 100644 index 0000000..1cb021f --- /dev/null +++ b/src/Transport/Request.php @@ -0,0 +1,46 @@ + */ + public $oids; + + /** @var int|null */ + public $maxRepetitions; + + /** @param list $oids */ + private function __construct(string $type, array $oids, ?int $maxRepetitions = null) + { + $this->type = $type; + $this->oids = $oids; + $this->maxRepetitions = $maxRepetitions; + } + + /** @param list $oids */ + public static function get(array $oids) : self + { + return new self(self::GET, $oids); + } + + /** @param list $oids */ + public static function getNext(array $oids) : self + { + return new self(self::GET_NEXT, $oids); + } + + public static function walk(string $oid, int $maxRepetitions = 40) : self + { + return new self(self::WALK, [$oid], $maxRepetitions); + } +} diff --git a/src/Transport/SimpleBatch.php b/src/Transport/SimpleBatch.php new file mode 100644 index 0000000..bb40e72 --- /dev/null +++ b/src/Transport/SimpleBatch.php @@ -0,0 +1,36 @@ + $request) { + switch ($request->type) { + case Request::GET: + $result[$key] = $this->get($request->oids); + + break; + case Request::GET_NEXT: + $result[$key] = $this->getNext($request->oids); + + break; + case Request::WALK: + $result[$key] = $this->walk($request->oids[0], (int) $request->maxRepetitions); + } + } + + return $result; + } +} diff --git a/src/Transport/SnmpClient.php b/src/Transport/SnmpClient.php new file mode 100644 index 0000000..f725243 --- /dev/null +++ b/src/Transport/SnmpClient.php @@ -0,0 +1,36 @@ + $oids + * + * @return array + */ + public function get(array $oids) : array; + + /** + * @param list $oids + * + * @return array + */ + public function getNext(array $oids) : array; + + /** @return array */ + public function walk(string $oid, int $maxRepetitions = 40) : array; + + /** + * @param array $requests + * + * @return array> + * + * @psalm-template T + * @psalm-param array $requests + * @psalm-return array> + */ + public function batch(array $requests) : array; +} diff --git a/src/Transport/ValueParser.php b/src/Transport/ValueParser.php new file mode 100644 index 0000000..b92fcfd --- /dev/null +++ b/src/Transport/ValueParser.php @@ -0,0 +1,46 @@ +expectException(EndOfMibReached::class); + $this->expectExceptionMessage( + 'No more variables left in this MIB View (It is past the end of the MIB tree), tried oid: .1.4' + ); + + throw EndOfMibReached::fromThrowable($exception); + } + + public function testFromThrowableWithUnexpectedMessage() : void + { + $exception = new Exception('unexpected message'); + + $this->expectException(EndOfMibReached::class); + $this->expectExceptionMessage('No more variables left in this MIB View (It is past the end of the MIB tree)'); + + throw EndOfMibReached::fromThrowable($exception); + } +} diff --git a/tests/Exception/NoSuchInstanceTest.php b/tests/Exception/NoSuchInstanceTest.php new file mode 100644 index 0000000..b75a2fd --- /dev/null +++ b/tests/Exception/NoSuchInstanceTest.php @@ -0,0 +1,32 @@ +expectException(NoSuchInstanceExists::class); + $this->expectExceptionMessage('No Such Instance currently exists at this OID: .1.4'); + + throw NoSuchInstanceExists::fromThrowable($exception); + } + + public function testFromThrowableWithUnexpectedMessage() : void + { + $exception = new Exception('unexpected message'); + + $this->expectException(NoSuchInstanceExists::class); + $this->expectExceptionMessage('No Such Instance currently exists at this OID'); + + throw NoSuchInstanceExists::fromThrowable($exception); + } +} diff --git a/tests/Exception/NoSuchObjectTest.php b/tests/Exception/NoSuchObjectTest.php new file mode 100644 index 0000000..926c34f --- /dev/null +++ b/tests/Exception/NoSuchObjectTest.php @@ -0,0 +1,32 @@ +expectException(NoSuchObjectExists::class); + $this->expectExceptionMessage('No Such Object available on this agent at this OID: .1.4'); + + throw NoSuchObjectExists::fromThrowable($exception); + } + + public function testFromThrowableWithUnexpectedMessage() : void + { + $exception = new Exception('unexpected message'); + + $this->expectException(NoSuchObjectExists::class); + $this->expectExceptionMessage('No Such Object available on this agent at this OID'); + + throw NoSuchObjectExists::fromThrowable($exception); + } +} diff --git a/tests/Helpers/OidsStripperTest.php b/tests/Helpers/OidsStripperTest.php new file mode 100644 index 0000000..eb21b0b --- /dev/null +++ b/tests/Helpers/OidsStripperTest.php @@ -0,0 +1,154 @@ + 'a', + '.1.2.3.2' => 'b', + '.1.2.3.3' => 'c', + ]; + + $expected = [ + 1 => 'a', + 2 => 'b', + 3 => 'c', + ]; + + self::assertSame($expected, OidStripper::stripParent($leafOidData)); + } + + public function testStripParentEmptyData() : void + { + $this->expectExceptionObject(GeneralException::new('Expected non-empty array')); + + OidStripper::stripParent([]); + } + + public function testStripParentInvalidKeys() : void + { + $this->expectExceptionObject(GeneralException::new('Expected keys to be full OIDs')); + + OidStripper::stripParent(['something strange' => 123]); + } + + public function testBatchStripParent() : void + { + $leafOidDataResponses = [ + [ + '.1.2.3.1' => 'a', + '.1.2.3.2' => 'b', + '.1.2.3.3' => 'c', + ], + ['.4.5.6.8' => 'd'], + ]; + + $expected = [ + [ + 1 => 'a', + 2 => 'b', + 3 => 'c', + ], + [8 => 'd'], + ]; + + self::assertSame($expected, OidStripper::batchStripParent($leafOidDataResponses)); + } + + public function testBatchStripParentEmptyData() : void + { + $this->expectExceptionObject(GeneralException::new('Expected non-empty array')); + + OidStripper::batchStripParent([[]]); + } + + public function testBatchStripParentInvalidKeys() : void + { + $this->expectExceptionObject(GeneralException::new('Expected keys to be full OIDs')); + + OidStripper::batchStripParent([['something strange' => 123]]); + } + + public function testWalk() : void + { + $response = [ + [ + '.1.2.3.1' => 'a', + '.1.2.3.2' => 'b', + '.1.2.3.3' => 'c', + ], + ]; + + $expected = [ + '3.1' => 'a', + '3.2' => 'b', + '3.3' => 'c', + ]; + + $snmpClient = $this->createMock(SnmpClient::class); + $snmpClient + ->expects(self::once()) + ->method('batch') + ->with([Request::walk($oid = '.1.2')]) + ->willReturn($response); + + self::assertSame($expected, OidStripper::walk($snmpClient, $oid)); + } + + public function testBatchStripParentPrefix() : void + { + $requests = [ + 'walk' => Request::walk('.1.2'), + 'get' => Request::get(['.1.3.6', '.1.3.4']), + 'getNext' => Request::getNext(['.1.3.6', '.1.3.3.666']), + ]; + + $responses = [ + 'walk' => [ + '.1.2.3.1' => 'a', + '.1.2.3.2' => 'b', + '.1.2.3.3' => 'c', + ], + 'get' => [ + '.1.3.6.1.2' => 'd', + '.1.3.4.1' => 'e', + ], + 'getNext' => [ + '.1.3.6.1.1' => 'f', + '.1.3.4.1' => 'g', + ], + ]; + + $expected = [ + 'walk' => [ + '3.1' => 'a', + '3.2' => 'b', + '3.3' => 'c', + ], + 'get' => [ + '1.2' => 'd', + '1' => 'e', + ], + 'getNext' => [ + '1.1' => 'f', + '.1.3.4.1' => 'g', + ], + ]; + + $snmpClient = $this->createMock(SnmpClient::class); + $snmpClient->expects(self::once())->method('batch')->with($requests)->willReturn($responses); + + self::assertSame($expected, OidStripper::batchStripParentPrefix($snmpClient, $requests)); + } +} diff --git a/tests/Helpers/ValueGetterTest.php b/tests/Helpers/ValueGetterTest.php new file mode 100644 index 0000000..cb5cc89 --- /dev/null +++ b/tests/Helpers/ValueGetterTest.php @@ -0,0 +1,82 @@ + $expected = 'a']; + + self::assertSame($expected, ValueGetter::first($response)); + } + + public function testFirstWithUnexpectedData() : void + { + $this->expectExceptionObject(GeneralException::new('Expected non-empty array')); + + ValueGetter::first([]); + } + + public function testFirstFromSameTree() : void + { + $snmpClient = $this->createMock(SnmpClient::class); + $snmpClient->expects(self::once()) + ->method('getNext') + ->willReturn(['.1.2.3.1' => $expected = 'a']); + + self::assertSame($expected, ValueGetter::firstFromSameTree($snmpClient, '.1.2.3')); + } + + public function testFirstFromSameTreeDoesntExist() : void + { + $snmpClient = $this->createMock(SnmpClient::class); + $snmpClient->expects(self::once()) + ->method('getNext') + ->willReturn(['.1.2.4.1' => 'a']); + + $this->expectExceptionObject(NoSuchInstanceExists::fromOid('.1.2.3')); + ValueGetter::firstFromSameTree($snmpClient, '.1.2.3'); + } + + public function testFirstFromSameTrees() : void + { + $snmpClient = $this->createMock(SnmpClient::class); + $snmpClient->expects(self::once()) + ->method('getNext') + ->willReturn( + [ + '.1.2.3.1' => 'a', + '.1.2.6.1' => 'b', + ] + ); + + $expected = ['a', 'b']; + + self::assertSame($expected, ValueGetter::firstFromSameTrees($snmpClient, ['.1.2.3', '.1.2.6'])); + } + + public function testFirstFromSameTreesDoesntExist() : void + { + $snmpClient = $this->createMock(SnmpClient::class); + $snmpClient->expects(self::once()) + ->method('getNext') + ->willReturn( + [ + '.1.2.3.1' => 'a', + '.1.2.7.1' => 'b', + ] + ); + + $this->expectExceptionObject(NoSuchInstanceExists::fromOid('.1.2.6')); + ValueGetter::firstFromSameTrees($snmpClient, ['.1.2.3', '.1.2.6']); + } +} diff --git a/tests/Transport/ApiSnmpClientTest.php b/tests/Transport/ApiSnmpClientTest.php new file mode 100644 index 0000000..dd2f241 --- /dev/null +++ b/tests/Transport/ApiSnmpClientTest.php @@ -0,0 +1,473 @@ +createApiSnmp(); + + $response = <<client->method('sendRequest') + ->with( + self::callback( + static function (RequestInterface $request) : bool { + $json = <<getUri() === 'http://localhost/snmp-proxy' + && self::jsonIsIdentical($json, (string) $request->getBody()); + } + ) + ) + ->willReturn(new Response(200, [], $response)); + + $result = $apiSnmp->get(['.1.3.6.1.2.1.25.2.3.1.2.1', '.1.3.6.1.2.1.25.2.3.1.2.4']); + + self::assertSame( + [ + '.1.3.6.1.2.1.25.2.3.1.2.1' => '.1.3.6.1.2.1.25.2.1.2', + '.1.3.6.1.2.1.25.2.3.1.2.4' => '.1.3.6.1.2.1.25.2.1.9', + ], + $result + ); + } + + public function testGetNext() : void + { + $apiSnmp = $this->createApiSnmp(); + + $response = <<client->method('sendRequest') + ->with( + self::callback( + static function (RequestInterface $request) : bool { + $json = <<getUri() === 'http://localhost/snmp-proxy' + && self::jsonIsIdentical($json, (string) $request->getBody()); + } + ) + ) + ->willReturn(new Response(200, [], $response)); + + $result = $apiSnmp->getNext(['.1.3.6.1.2.1.25.2.3.1.2', '.1.3.6.1.2.1.25.2.3.1.2.3']); + + self::assertSame( + [ + '.1.3.6.1.2.1.25.2.3.1.2.1' => '.1.3.6.1.2.1.25.2.1.2', + '.1.3.6.1.2.1.25.2.3.1.2.4' => '.1.3.6.1.2.1.25.2.1.9', + ], + $result + ); + } + + public function testWalk() : void + { + $apiSnmp = $this->createApiSnmp(); + + $response = <<client->method('sendRequest') + ->with( + self::callback( + static function (RequestInterface $request) : bool { + $json = <<getUri() === 'http://localhost/snmp-proxy' + && self::jsonIsIdentical($json, (string) $request->getBody()); + } + ) + ) + ->willReturn(new Response(200, [], $response)); + + $result = $apiSnmp->walk('.1.3.6.1.2.1.31.1.1.1.15'); + + self::assertSame( + [ + '.1.3.6.1.2.1.31.1.1.1.15.1000001' => 100000, + '.1.3.6.1.2.1.31.1.1.1.15.1000003' => 60000, + '.1.3.6.1.2.1.31.1.1.1.15.1000005' => 80000, + ], + $result + ); + } + + public function testBatch() : void + { + $apiSnmp = $this->createApiSnmp(); + + $response = <<client->method('sendRequest') + ->with( + self::callback( + static function (RequestInterface $request) : bool { + $json = <<getUri() === 'http://localhost/snmp-proxy' + && self::jsonIsIdentical($json, (string) $request->getBody()); + } + ) + ) + ->willReturn(new Response(200, [], $response)); + + $result = $apiSnmp->batch( + [ + 'get' => Request::get(['.1.3.6.1.2.1.25.2.3.1.2.1', '.1.3.6.1.2.1.25.2.3.1.2.4']), + 'walk' => Request::walk('.1.3.6.1.2.1.31.1.1.1.15'), + 'getNext' => Request::getNext(['.1.3.6.1.2.1.25.2.3.1.2', '.1.3.6.1.2.1.25.2.3.1.2.3']), + ] + ); + + self::assertSame( + [ + 'get' => [ + '.1.3.6.1.2.1.25.2.3.1.2.1' => '.1.3.6.1.2.1.25.2.1.2', + '.1.3.6.1.2.1.25.2.3.1.2.4' => '.1.3.6.1.2.1.25.2.1.9', + ], + 'walk' => [ + '.1.3.6.1.2.1.31.1.1.1.15.1000001' => 100000, + '.1.3.6.1.2.1.31.1.1.1.15.1000003' => 60000, + '.1.3.6.1.2.1.31.1.1.1.15.1000005' => 80000, + ], + 'getNext' => [ + '.1.3.6.1.2.1.25.2.3.1.2.1' => '.1.3.6.1.2.1.25.2.1.2', + '.1.3.6.1.2.1.25.2.3.1.2.4' => '.1.3.6.1.2.1.25.2.1.9', + ], + ], + $result + ); + } + + public function testBatchNoRequests() : void + { + $this->expectExceptionObject(NoRequestsProvided::new()); + + $this->createApiSnmp()->batch([]); + } + + public function testThatParametersAreCorrectlyPropagatedToTheJsonRequest() : void + { + $this->client = $this->createMock(Client::class); + $psr17Factory = new Psr17Factory(); + + $apiSnmp = new ApiSnmpClient( + $this->client, + $psr17Factory, + $psr17Factory, + 'http://somewhere', + 'lorem', + 'ipsum', + 50, + 5, + '1' + ); + + $response = <<client->method('sendRequest') + ->with( + self::callback( + static function (RequestInterface $request) : bool { + $json = <<getUri() === 'http://somewhere/snmp-proxy' + && self::jsonIsIdentical($json, (string) $request->getBody()); + } + ) + ) + ->willReturn(new Response(200, [], $response)); + + $result = $apiSnmp->get(['.1.3.6.1.2.1.2.2.1.2.1000009']); + + self::assertSame(['.1.3.6.1.2.1.2.2.1.2.1000009' => 'Port-Channel9'], $result); + } + + public function testErrorJsonDecodingResponse() : void + { + $this->client = $this->createMock(Client::class); + $psr17Factory = new Psr17Factory(); + + $apiSnmp = new ApiSnmpClient( + $this->client, + $psr17Factory, + $psr17Factory, + 'http://somewhere', + 'lorem', + 'ipsum', + 50, + 5, + '1' + ); + + $response = '{wow this is not a valid json response'; + $this->client->method('sendRequest')->willReturn(new Response(500, [], $response)); + + $this->expectExceptionObject( + GeneralException::new(sprintf('Response is not valid JSON [HTTP 500]: "%s", oids: .1.3.6', $response)) + ); + + $apiSnmp->get(['.1.3.6']); + } + + public function testErrorUnexpectedStatusCodeResponse() : void + { + $this->client = $this->createMock(Client::class); + $psr17Factory = new Psr17Factory(); + + $apiSnmp = new ApiSnmpClient( + $this->client, + $psr17Factory, + $psr17Factory, + 'http://somewhere', + 'lorem', + 'ipsum', + 50, + 5, + '1' + ); + + $response = '{"wow": "this server responds with JSON"}'; + $this->client->method('sendRequest')->willReturn(new Response(500, [], $response)); + + $error = sprintf('Unexpected HTTP status code: 500, response: "%s"', $response); + $this->expectExceptionObject(GeneralException::new($error, null, ['.1.3.6'])); + + $apiSnmp->get(['.1.3.6']); + } + + public function testWalkWithEndOfMibError() : void + { + $apiSnmp = $this->createApiSnmp(); + + $this->client->method('sendRequest') + ->willReturn(new Response(500, [], '{"error": "end of mib: .1.15"}')); + + $this->expectExceptionObject(EndOfMibReached::fromOid('.1.15')); + + $apiSnmp->walk('.1.15'); + } + + public function testWalkWithNoSuchInstanceError() : void + { + $apiSnmp = $this->createApiSnmp(); + + $this->client->method('sendRequest') + ->willReturn(new Response(500, [], '{"error": "no such instance: .1.3.6.1.2.1.1.1"}')); + + $this->expectExceptionObject(NoSuchInstanceExists::fromOid('.1.3.6.1.2.1.1.1')); + + $apiSnmp->walk('.1.3.6.1.2.1.1.1'); + } + + public function testWalkWithNoSuchObjectError() : void + { + $apiSnmp = $this->createApiSnmp(); + + $this->client->method('sendRequest') + ->willReturn(new Response(500, [], '{"error": "no such object: .1.4"}')); + + $this->expectExceptionObject(NoSuchObjectExists::fromOid('.1.4')); + + $apiSnmp->walk('.1.4'); + } + + public function testWalkWithRequestError() : void + { + $apiSnmp = $this->createApiSnmp(); + + $this->client->method('sendRequest') + ->willThrowException(new Exception('some error')); + + $this->expectExceptionObject(GeneralException::new('some error')); + + $apiSnmp->walk('.1.4'); + } + + public function testWalkWithUnexpectedError() : void + { + $apiSnmp = $this->createApiSnmp(); + + $this->client->method('sendRequest') + ->willReturn(new Response(500, [], '{"error": "something unexpected happened"}')); + + $this->expectExceptionObject(GeneralException::new('something unexpected happened')); + + $apiSnmp->walk('.1.4'); + } + + private function createApiSnmp() : ApiSnmpClient + { + $this->client = $this->createMock(Client::class); + $psr17Factory = new Psr17Factory(); + + return new ApiSnmpClient($this->client, $psr17Factory, $psr17Factory, 'http://localhost'); + } +} diff --git a/tests/Transport/Cli/SymfonyProcessProcessExecutorTest.php b/tests/Transport/Cli/SymfonyProcessProcessExecutorTest.php new file mode 100644 index 0000000..aa16448 --- /dev/null +++ b/tests/Transport/Cli/SymfonyProcessProcessExecutorTest.php @@ -0,0 +1,40 @@ +expectExceptionObject(GeneralException::new(sprintf('sh: 1: exec: %s: not found', $command))); + + $executor = new SymfonyProcessProcessExecutor(1); + $executor->execute([$command]); + } + + public function testTimeout() : void + { + $this->expectExceptionObject( + GeneralException::new(sprintf('The process "%s" exceeded the timeout of 1 seconds', "'sleep' '5'")) + ); + + $time = microtime(true); + + $executor = new SymfonyProcessProcessExecutor(1); + $executor->execute(['sleep', '5']); + + self::assertLessThan(2, microtime(true) - $time); + } +} diff --git a/tests/Transport/CliSnmpClientTest.php b/tests/Transport/CliSnmpClientTest.php new file mode 100644 index 0000000..a8c430e --- /dev/null +++ b/tests/Transport/CliSnmpClientTest.php @@ -0,0 +1,353 @@ + ['file', '/dev/null', 'w'], 2 => ['file', '/dev/null', 'w']], $pipes); + if ($process === false) { + self::fail('failed to initiate SNMP agent'); + } + + self::$process = $process; + } + + public static function tearDownAfterClass() : void + { + if (self::$process === null) { + return; + } + + shell_exec(sprintf('pkill -2 -P %d', proc_get_status(self::$process)['pid'])); + self::$process = null; + } + + public function __destruct() + { + self::tearDownAfterClass(); + } + + public function testGet() : void + { + $result = $this->createCliSnmp()->get(['.1.3.6.1.2.1.25.2.3.1.2.1', '.1.3.6.1.2.1.25.2.3.1.2.4']); + + self::assertSame( + [ + '.1.3.6.1.2.1.25.2.3.1.2.1' => '.1.3.6.1.2.1.25.2.1.2', + '.1.3.6.1.2.1.25.2.3.1.2.4' => '.1.3.6.1.2.1.25.2.1.9', + ], + $result + ); + } + + public function testGetNext() : void + { + $result = $this->createCliSnmp()->getNext(['.1.3.6.1.2.1.25.2.3.1.2', '.1.3.6.1.2.1.25.2.3.1.2.3']); + + self::assertSame( + [ + '.1.3.6.1.2.1.25.2.3.1.2.1' => '.1.3.6.1.2.1.25.2.1.2', + '.1.3.6.1.2.1.25.2.3.1.2.4' => '.1.3.6.1.2.1.25.2.1.9', + ], + $result + ); + } + + public function testWalk() : void + { + $result = $this->createCliSnmp()->walk('.1.3.6.1.2.1.31.1.1.1.15'); + + self::assertSame( + [ + '.1.3.6.1.2.1.31.1.1.1.15.1000001' => 100000, + '.1.3.6.1.2.1.31.1.1.1.15.1000003' => 60000, + '.1.3.6.1.2.1.31.1.1.1.15.1000005' => 80000, + ], + $result + ); + } + + public function testWalkSnmpClientUsesCarriageReturnsAndLineFeeds() : void + { + $output = ".1.3.6.1.2.1.31.1.1.1.15.1000001 = Gauge32: 100000\r\n" + . ".1.3.6.1.2.1.31.1.1.1.15.1000003 = Gauge32: 60000\r\n" + . ".1.3.6.1.2.1.31.1.1.1.15.1000005 = Gauge32: 80000\r\n"; + + $processExecutor = $this->createMock(ProcessExecutor::class); + $processExecutor->method('execute')->willReturn($output); + + $result = $this->createCliSnmp('2c', $processExecutor)->walk('.1.3.6.1.2.1.31.1.1.1.15'); + + self::assertSame( + [ + '.1.3.6.1.2.1.31.1.1.1.15.1000001' => 100000, + '.1.3.6.1.2.1.31.1.1.1.15.1000003' => 60000, + '.1.3.6.1.2.1.31.1.1.1.15.1000005' => 80000, + ], + $result + ); + } + + public function testWalkWithOldSnmpVersion() : void + { + $result = $this->createCliSnmp('1')->walk('.1.3.6.1.2.1.31.1.1.1.15'); + + self::assertSame( + [ + '.1.3.6.1.2.1.31.1.1.1.15.1000001' => 100000, + '.1.3.6.1.2.1.31.1.1.1.15.1000003' => 60000, + '.1.3.6.1.2.1.31.1.1.1.15.1000005' => 80000, + ], + $result + ); + } + + public function testWalkWholeTree() : void + { + $result = $this->createCliSnmp()->walk('.1.3'); + + self::assertSame( + [ + '.1.3.6.1.2.1.1.1.0' => 'Cisco IOS Software, C2960S Software (C2960S-UNIVERSALK9-M), ' + . "Version 12.2(58)SE2, RELEASE SOFTWARE (fc1)\n" + . "Technical Support: http://www.cisco.com/techsupport\n" + . "Copyright (c) 1986-2011 by \\\"Cisco Systems, Inc.\\\"\n" + . 'Compiled Thu 21-Jul-11 02:22 by prod_rel_team', + '.1.3.6.1.2.1.1.3.0' => 293718542, + '.1.3.6.1.2.1.2.2.1.2.47' => 'Ethernet47', + '.1.3.6.1.2.1.2.2.1.2.48' => 'Ethernet48', + '.1.3.6.1.2.1.2.2.1.2.49001' => 'Ethernet49/1', + '.1.3.6.1.2.1.2.2.1.2.50001' => 'Ethernet50/1', + '.1.3.6.1.2.1.2.2.1.2.1000008' => 'Port-Channel8', + '.1.3.6.1.2.1.2.2.1.2.1000009' => 'Port-Channel9', + '.1.3.6.1.2.1.2.2.1.2.2002002' => 'Vlan2002', + '.1.3.6.1.2.1.2.2.1.2.2002019' => 'Vlan2019', + '.1.3.6.1.2.1.2.2.1.2.2002020' => 'Vlan2020', + '.1.3.6.1.2.1.2.2.1.2.5000000' => 'Loopback0', + '.1.3.6.1.2.1.2.2.1.14.8' => 0, + '.1.3.6.1.2.1.2.2.1.14.9' => 226, + '.1.3.6.1.2.1.2.2.1.14.10' => 256, + '.1.3.6.1.2.1.2.2.1.14.11' => 296, + '.1.3.6.1.2.1.4.20.1.1.10.100.192.2' => '10.100.192.2', + '.1.3.6.1.2.1.4.20.1.1.10.110.27.254' => '10.110.27.254', + '.1.3.6.1.2.1.4.20.1.1.66.208.216.74' => '66.208.216.74', + '.1.3.6.1.2.1.4.22.1.2.2000955.185.152.67.97' => '91 E2 BA E3 5A 61', + '.1.3.6.1.2.1.4.22.1.2.2000955.185.152.67.99' => '53 54 00 5F 41 D0', + '.1.3.6.1.2.1.4.22.1.2.2000955.185.152.67.100' => '53 54 00 4C 5A 5D', + '.1.3.6.1.2.1.4.22.1.2.2000955.185.152.67.102' => '53 54 00 A9 A8 3B', + '.1.3.6.1.2.1.4.22.1.2.2000955.185.152.67.104' => '53 54 00 5A A0 CA', + '.1.3.6.1.2.1.25.2.3.1.2.1' => '.1.3.6.1.2.1.25.2.1.2', + '.1.3.6.1.2.1.25.2.3.1.2.2' => '.1.3.6.1.2.1.25.2.1.2', + '.1.3.6.1.2.1.25.2.3.1.2.3' => '.1.3.6.1.2.1.25.2.1.2', + '.1.3.6.1.2.1.25.2.3.1.2.4' => '.1.3.6.1.2.1.25.2.1.9', + '.1.3.6.1.2.1.31.1.1.1.6.46' => '1884401752869190', + '.1.3.6.1.2.1.31.1.1.1.6.47' => '1883620653799494', + '.1.3.6.1.2.1.31.1.1.1.6.48' => '1884283891426650', + '.1.3.6.1.2.1.31.1.1.1.6.49001' => '2494191363092125', + '.1.3.6.1.2.1.31.1.1.1.6.50001' => '17658827020872235', + '.1.3.6.1.2.1.31.1.1.1.15.1000001' => 100000, + '.1.3.6.1.2.1.31.1.1.1.15.1000003' => 60000, + '.1.3.6.1.2.1.31.1.1.1.15.1000005' => 80000, + '.1.3.6.1.2.1.47.1.1.1.1.13.30' => '4E 4D 2D 33 32 41 20 20 20 20 20 20 20 20 20 20 20 20 ' + . 'FF FF FF FF FF FF FF', + '.1.3.6.1.6.3.10.2.1.3.0' => 2937024, + ], + $result + ); + } + + public function testWalkLastMibElement() : void + { + $result = $this->createCliSnmp()->walk('.1.7'); + + self::assertSame(['.1.7.8.9' => "Don't know what I'm"], $result); + } + + public function testWalkLastMibElementAndSnmpVersion1() : void + { + $result = $this->createCliSnmp('1')->walk('.1.7'); + + self::assertSame(['.1.7.8.9' => "Don't know what I'm"], $result); + } + + public function testWalkInvalidVersion() : void + { + $this->expectExceptionObject(InvalidVersionProvided::new('whatever')); + + $this->createCliSnmp('whatever')->walk('.1.15'); + } + + public function testWalkWithNoSuchObjectError() : void + { + // todo find real example + $output = ".1.3.5 = No Such Object\n"; + + $processExecutor = $this->createMock(ProcessExecutor::class); + $processExecutor->method('execute')->willReturn($output); + + $this->expectExceptionObject(NoSuchObjectExists::fromOid('.1.3.5')); + + $this->createCliSnmp('2c', $processExecutor)->walk('.1.3.5'); + } + + public function testWalkWithNoSuchInstanceError() : void + { + $this->expectExceptionObject(NoSuchInstanceExists::fromOid('.1.3.5')); + + $this->createCliSnmp()->walk('.1.3.5'); + } + + public function testWalkWithSnmpVersion1AndNoSuchInstanceError() : void + { + $this->expectExceptionObject(NoSuchInstanceExists::fromOid('.1.3.5')); + + $this->createCliSnmp('1')->walk('.1.3.5'); + } + + public function testWalkWithEndOfMibError() : void + { + $this->expectExceptionObject(EndOfMibReached::fromOid('.1.15')); + + $this->createCliSnmp()->walk('.1.15'); + } + + public function testWalkWithSnmpVersion1AndEndOfMibError() : void + { + $this->expectExceptionObject(EndOfMibReached::new()); + + $this->createCliSnmp('1')->walk('.1.15'); + } + + public function testGetWithNoSuchInstanceError() : void + { + $this->expectExceptionObject(NoSuchInstanceExists::fromOid('.1.3.5')); + + $this->createCliSnmp()->get(['.1.3.5']); + } + + public function testGetWithSnmpVersion1AndNoSuchInstanceError() : void + { + $this->expectExceptionObject(NoSuchInstanceExists::fromOid('.1.3.5')); + + $this->createCliSnmp('1')->get(['.1.3.5']); + } + + public function testGetNextWithEndOfMibError() : void + { + $this->expectExceptionObject(EndOfMibReached::fromOid('.1.15')); + + $this->createCliSnmp()->getNext(['.1.15']); + } + + public function testGetNextWithSnmpVersion1AndEndOfMibError() : void + { + $this->expectExceptionObject(EndOfMibReached::fromOid('.1.15')); + + $this->createCliSnmp('1')->getNext(['.1.15']); + } + + public function testWalkWithUnknownTypeError() : void + { + $this->expectExceptionObject(CannotParseUnknownValueType::new('OPAQUE')); + + $this->createCliSnmp()->walk('.1.6.6.6.666'); + } + + public function testGetExecutorSnmpException() : void + { + $processExecutor = $this->createMock(ProcessExecutor::class); + $processExecutor->method('execute')->willThrowException($exception = GeneralException::new('test')); + + $this->expectExceptionObject(GeneralException::new('test', $exception, ['.1.3.5'])); + + $this->createCliSnmp('2c', $processExecutor)->get(['.1.3.5']); + } + + public function testGetExecutorException() : void + { + $processExecutor = $this->createMock(ProcessExecutor::class); + $processExecutor->method('execute')->willThrowException($exception = new Exception('test')); + + $this->expectExceptionObject( + GeneralException::new('Failed to execute SNMP CLI command', $exception, ['.1.3.5']) + ); + + $this->createCliSnmp('2c', $processExecutor)->get(['.1.3.5']); + } + + public function testGetNextExecutorSnmpException() : void + { + $processExecutor = $this->createMock(ProcessExecutor::class); + $processExecutor->method('execute')->willThrowException($exception = GeneralException::new('test')); + + $this->expectExceptionObject(GeneralException::new('test', $exception, ['.1.3.5'])); + + $this->createCliSnmp('2c', $processExecutor)->getNext(['.1.3.5']); + } + + public function testGetNextExecutorException() : void + { + $processExecutor = $this->createMock(ProcessExecutor::class); + $processExecutor->method('execute')->willThrowException($exception = new Exception('test')); + + $this->expectExceptionObject( + GeneralException::new('Failed to execute SNMP CLI command', $exception, ['.1.3.5']) + ); + + $this->createCliSnmp('2c', $processExecutor)->getNext(['.1.3.5']); + } + + public function testWalkExecutorSnmpException() : void + { + $processExecutor = $this->createMock(ProcessExecutor::class); + $processExecutor->method('execute')->willThrowException($exception = GeneralException::new('test')); + + $this->expectExceptionObject(GeneralException::new('test', $exception, ['.1.3.5'])); + + $this->createCliSnmp('2c', $processExecutor)->walk('.1.3.5'); + } + + public function testWalkExecutorException() : void + { + $processExecutor = $this->createMock(ProcessExecutor::class); + $processExecutor->method('execute')->willThrowException($exception = new Exception('test')); + + $this->expectExceptionObject( + GeneralException::new('Failed to execute SNMP CLI command', $exception, ['.1.3.5']) + ); + + $this->createCliSnmp('2c', $processExecutor)->walk('.1.3.5'); + } + + private function createCliSnmp(string $version = '2c', ?ProcessExecutor $processExecutor = null) : CliSnmpClient + { + return new CliSnmpClient(self::SNMP_HOST, 'public', 1, 3, $version, $processExecutor); + } +} diff --git a/tests/Transport/ExtensionSnmpClientTest.php b/tests/Transport/ExtensionSnmpClientTest.php new file mode 100644 index 0000000..ba68b2c --- /dev/null +++ b/tests/Transport/ExtensionSnmpClientTest.php @@ -0,0 +1,247 @@ + ['file', '/dev/null', 'w'], 2 => ['file', '/dev/null', 'w']], $pipes); + if ($process === false) { + self::fail('failed to initiate SNMP agent'); + } + + self::$process = $process; + } + + public static function tearDownAfterClass() : void + { + if (self::$process === null) { + return; + } + + shell_exec(sprintf('pkill -2 -P %d', proc_get_status(self::$process)['pid'])); + self::$process = null; + } + + public function __destruct() + { + self::tearDownAfterClass(); + } + + public function testGet() : void + { + $result = $this->createExtensionSnmp()->get(['.1.3.6.1.2.1.2.2.1.14.9', '.1.3.6.1.2.1.4.20.1.1.10.100.192.2']); + + self::assertSame( + [ + '.1.3.6.1.2.1.2.2.1.14.9' => 226, + '.1.3.6.1.2.1.4.20.1.1.10.100.192.2' => '10.100.192.2', + ], + $result + ); + } + + public function testGetNext() : void + { + $result = $this->createExtensionSnmp()->getNext( + ['.1.3.6.1.2.1.2.2.1.14.9', '.1.3.6.1.2.1.4.20.1.1.10.100.192.2'] + ); + + self::assertSame( + [ + '.1.3.6.1.2.1.2.2.1.14.10' => 256, + '.1.3.6.1.2.1.4.20.1.1.10.110.27.254' => '10.110.27.254', + ], + $result + ); + } + + public function testWalk() : void + { + $result = $this->createExtensionSnmp()->walk('.1.3.6.1.2.1.31.1.1.1.15'); + + self::assertSame( + [ + '.1.3.6.1.2.1.31.1.1.1.15.1000001' => 100000, + '.1.3.6.1.2.1.31.1.1.1.15.1000003' => 60000, + '.1.3.6.1.2.1.31.1.1.1.15.1000005' => 80000, + ], + $result + ); + } + + public function testWalkWholeTree() : void + { + $result = $this->createExtensionSnmp()->walk('.1.3'); + + self::assertSame( + [ + '.1.3.6.1.2.1.1.1.0' => 'Cisco IOS Software, C2960S Software (C2960S-UNIVERSALK9-M), ' + . "Version 12.2(58)SE2, RELEASE SOFTWARE (fc1)\n" + . "Technical Support: http://www.cisco.com/techsupport\n" + . "Copyright (c) 1986-2011 by \\\"Cisco Systems, Inc.\\\"\n" + . 'Compiled Thu 21-Jul-11 02:22 by prod_rel_team', + '.1.3.6.1.2.1.1.3.0' => 293718542, + '.1.3.6.1.2.1.2.2.1.2.47' => 'Ethernet47', + '.1.3.6.1.2.1.2.2.1.2.48' => 'Ethernet48', + '.1.3.6.1.2.1.2.2.1.2.49001' => 'Ethernet49/1', + '.1.3.6.1.2.1.2.2.1.2.50001' => 'Ethernet50/1', + '.1.3.6.1.2.1.2.2.1.2.1000008' => 'Port-Channel8', + '.1.3.6.1.2.1.2.2.1.2.1000009' => 'Port-Channel9', + '.1.3.6.1.2.1.2.2.1.2.2002002' => 'Vlan2002', + '.1.3.6.1.2.1.2.2.1.2.2002019' => 'Vlan2019', + '.1.3.6.1.2.1.2.2.1.2.2002020' => 'Vlan2020', + '.1.3.6.1.2.1.2.2.1.2.5000000' => 'Loopback0', + '.1.3.6.1.2.1.2.2.1.14.8' => 0, + '.1.3.6.1.2.1.2.2.1.14.9' => 226, + '.1.3.6.1.2.1.2.2.1.14.10' => 256, + '.1.3.6.1.2.1.2.2.1.14.11' => 296, + '.1.3.6.1.2.1.4.20.1.1.10.100.192.2' => '10.100.192.2', + '.1.3.6.1.2.1.4.20.1.1.10.110.27.254' => '10.110.27.254', + '.1.3.6.1.2.1.4.20.1.1.66.208.216.74' => '66.208.216.74', + '.1.3.6.1.2.1.4.22.1.2.2000955.185.152.67.97' => '91 E2 BA E3 5A 61', + '.1.3.6.1.2.1.4.22.1.2.2000955.185.152.67.99' => '53 54 00 5F 41 D0', + '.1.3.6.1.2.1.4.22.1.2.2000955.185.152.67.100' => '53 54 00 4C 5A 5D', + '.1.3.6.1.2.1.4.22.1.2.2000955.185.152.67.102' => '53 54 00 A9 A8 3B', + '.1.3.6.1.2.1.4.22.1.2.2000955.185.152.67.104' => '53 54 00 5A A0 CA', + '.1.3.6.1.2.1.25.2.3.1.2.1' => '.1.3.6.1.2.1.25.2.1.2', + '.1.3.6.1.2.1.25.2.3.1.2.2' => '.1.3.6.1.2.1.25.2.1.2', + '.1.3.6.1.2.1.25.2.3.1.2.3' => '.1.3.6.1.2.1.25.2.1.2', + '.1.3.6.1.2.1.25.2.3.1.2.4' => '.1.3.6.1.2.1.25.2.1.9', + '.1.3.6.1.2.1.31.1.1.1.6.46' => '1884401752869190', + '.1.3.6.1.2.1.31.1.1.1.6.47' => '1883620653799494', + '.1.3.6.1.2.1.31.1.1.1.6.48' => '1884283891426650', + '.1.3.6.1.2.1.31.1.1.1.6.49001' => '2494191363092125', + '.1.3.6.1.2.1.31.1.1.1.6.50001' => '17658827020872235', + '.1.3.6.1.2.1.31.1.1.1.15.1000001' => 100000, + '.1.3.6.1.2.1.31.1.1.1.15.1000003' => 60000, + '.1.3.6.1.2.1.31.1.1.1.15.1000005' => 80000, + '.1.3.6.1.2.1.47.1.1.1.1.13.30' => '4E 4D 2D 33 32 41 20 20 20 20 20 20 20 20 20 20 20 20 ' + . 'FF FF FF FF FF FF FF', + '.1.3.6.1.6.3.10.2.1.3.0' => 2937024, + ], + $result + ); + } + + public function testWalkLastMibElement() : void + { + $result = $this->createExtensionSnmp()->walk('.1.7'); + + self::assertSame(['.1.7.8.9' => "Don't know what I'm"], $result); + } + + public function testWalkLastMibElementAndSnmpVersion1() : void + { + $result = $this->createExtensionSnmp('1')->walk('.1.7'); + + self::assertSame(['.1.7.8.9' => "Don't know what I'm"], $result); + } + + public function testWalkWithInvalidVersion() : void + { + $this->expectExceptionObject(InvalidVersionProvided::new('whatever')); + + (new ExtensionSnmpClient('', '', 0, 0, 'whatever'))->walk('.1.15'); + } + + public function testWalkWithEndOfMibError() : void + { + $this->expectExceptionObject(EndOfMibReached::fromOid('.1.15')); + + $this->createExtensionSnmp()->walk('.1.15'); + } + + public function testWalkWithNoSuchInstanceError() : void + { + $this->expectExceptionObject(NoSuchInstanceExists::fromOid('.1.3.5')); + + $this->createExtensionSnmp()->walk('.1.3.5'); + } + + public function testWalkWithSnmpVersion1AndNoSuchInstanceError() : void + { + $this->expectExceptionObject(NoSuchInstanceExists::fromOid('.1.3.5')); + + $this->createExtensionSnmp('1')->walk('.1.3.5'); + } + + public function testWalkWithSnmpVersion1AndEndOfMibError() : void + { + // SNMP v1 reports NoSuchInstance instead of EndOfMib + $this->expectExceptionObject(NoSuchInstanceExists::fromOid('.1.15')); + + $this->createExtensionSnmp('1')->walk('.1.15'); + } + + public function testGetWithNoSuchInstanceError() : void + { + $this->expectExceptionObject(NoSuchInstanceExists::fromOid('.1.3.5')); + + $this->createExtensionSnmp()->get(['.1.3.5']); + } + + public function testGetWithSnmpVersion1AndNoSuchInstanceError() : void + { + $this->expectExceptionObject(NoSuchInstanceExists::fromOid('.1.3.5')); + + $this->createExtensionSnmp('1')->get(['.1.3.5']); + } + + public function testGetNextWithEndOfMibError() : void + { + $this->expectExceptionObject(EndOfMibReached::fromOid('.1.15')); + + $this->createExtensionSnmp()->getNext(['.1.15']); + } + + public function testGetNextWithSnmpVersion1AndEndOfMibError() : void + { + // SNMP v1 reports NoSuchInstance instead of EndOfMib + $this->expectExceptionObject(NoSuchInstanceExists::fromOid('.1.15')); + + $this->createExtensionSnmp('1')->getNext(['.1.15']); + } + + public function testWalkWithUnknownTypeError() : void + { + $this->expectExceptionObject(CannotParseUnknownValueType::new('OPAQUE')); + + $this->createExtensionSnmp()->walk('.1.6.6.6.666'); + } + + public function testWalkHostThatDoesntExist() : void + { + $this->expectExceptionObject(GeneralException::new('No response from 127.0.0.1:1, oids: .1.6.6.6.666')); + + (new ExtensionSnmpClient('127.0.0.1:1', 'public', 100, 0))->walk('.1.6.6.6.666'); + } + + private function createExtensionSnmp(string $version = '2c') : ExtensionSnmpClient + { + return new ExtensionSnmpClient(self::SNMP_HOST, 'public', 1000000, 3, $version); + } +} diff --git a/tests/Transport/FallbackSnmpClientTest.php b/tests/Transport/FallbackSnmpClientTest.php new file mode 100644 index 0000000..bf2ea38 --- /dev/null +++ b/tests/Transport/FallbackSnmpClientTest.php @@ -0,0 +1,149 @@ +createMock(SnmpClient::class); + $client1->expects(self::once()) + ->method('get') + ->with($oids = ['.1.2.3']) + ->willReturn($expected = ['.1.2.3' => 123]); + + $fallbackClient = new FallbackSnmpClient(new NullLogger(), [$client1]); + $result = $fallbackClient->get($oids); + + self::assertSame($expected, $result); + } + + public function testGetNext() : void + { + $client1 = $this->createMock(SnmpClient::class); + $client1->expects(self::once()) + ->method('getNext') + ->with($oids = ['.1.2.3']) + ->willReturn($expected = ['.1.2.3' => 123]); + + $fallbackClient = new FallbackSnmpClient(new NullLogger(), [$client1]); + $result = $fallbackClient->getNext($oids); + + self::assertSame($expected, $result); + } + + public function testWalk() : void + { + $client1 = $this->createMock(SnmpClient::class); + $client1->expects(self::once()) + ->method('walk') + ->with($oid = '.1.2.3') + ->willReturn($expected = ['.1.2.3' => 123]); + + $fallbackClient = new FallbackSnmpClient(new NullLogger(), [$client1]); + $result = $fallbackClient->walk($oid); + + self::assertSame($expected, $result); + } + + public function testBatch() : void + { + $requests = [ + 'walk' => Request::walk('.1.2.3', 10), + 'get' => Request::get(['.4.5.6']), + ]; + + $client1 = $this->createMock(SnmpClient::class); + $expected = [ + 'walk' => ['.1.2.3' => 123], + 'get' => ['.4.5.6' => 456], + ]; + $client1->expects(self::once())->method('batch')->with($requests)->willReturn($expected); + + $fallbackClient = new FallbackSnmpClient(new NullLogger(), [$client1]); + $result = $fallbackClient->batch($requests); + + self::assertSame($expected, $result); + } + + public function testOnlyLastClientWorks() : void + { + $client1 = $this->createMock(SnmpClient::class); + $client1->expects(self::once()) + ->method('get') + ->with($oids = ['.1.2.3']) + ->willThrowException($exception1 = GeneralException::new('an error')); + + $client2 = $this->createMock(SnmpClient::class); + $client2->expects(self::once()) + ->method('get') + ->with($oids = ['.1.2.3']) + ->willThrowException($exception2 = GeneralException::new('other error')); + + $client3 = $this->createMock(SnmpClient::class); + $client3->expects(self::once()) + ->method('get') + ->with($oids = ['.1.2.3']) + ->willReturn($expected = ['.1.2.3' => 123]); + + $logger = new TestLogger(); + + $fallbackClient = new FallbackSnmpClient($logger, [$client1, $client2, $client3]); + $result = $fallbackClient->get($oids); + + self::assertSame($expected, $result); + self::assertCount(2, $logger->records); + + $logEntry = $logger->records[0]; + self::assertSame('SNMP request failed', $logEntry['message']); + self::assertSame('warning', $logEntry['level']); + self::assertSame(0, $logEntry['context']['clientKey']); + self::assertSame($client1, $logEntry['context']['client']); + self::assertSame($exception1, $logEntry['context']['exception']); + + $logEntry = $logger->records[1]; + self::assertSame('SNMP request failed', $logEntry['message']); + self::assertSame('warning', $logEntry['level']); + self::assertSame(1, $logEntry['context']['clientKey']); + self::assertSame($client2, $logEntry['context']['client']); + self::assertSame($exception2, $logEntry['context']['exception']); + } + + public function testAllClientsFail() : void + { + $client1 = $this->createMock(SnmpClient::class); + $client1->expects(self::once()) + ->method('get') + ->with($oids = ['.1.2.3']) + ->willThrowException(GeneralException::new('an error')); + + $client2 = $this->createMock(SnmpClient::class); + $client2->expects(self::once()) + ->method('get') + ->with($oids = ['.1.2.3']) + ->willThrowException($expected = GeneralException::new('other error')); + + $fallbackClient = new FallbackSnmpClient(new NullLogger(), [$client1, $client2]); + + $this->expectExceptionObject($expected); + + $fallbackClient->get($oids); + } + + public function testNoClientsProvided() : void + { + $this->expectExceptionObject(GeneralException::new('No SNMP clients provided')); + + new FallbackSnmpClient(new NullLogger(), []); + } +} diff --git a/tests/Transport/SimpleBatchTest.php b/tests/Transport/SimpleBatchTest.php new file mode 100644 index 0000000..8be31e7 --- /dev/null +++ b/tests/Transport/SimpleBatchTest.php @@ -0,0 +1,132 @@ + $requests + * @param list> $result + * + * @dataProvider providerBatch + */ + public function testBatch(SnmpClient $snmpClient, array $requests, array $result) : void + { + $batchSnmpClient = $this->createBatchSnmpClient($snmpClient); + + self::assertSame($result, $batchSnmpClient->batch($requests)); + } + + /** @return iterable */ + public function providerBatch() : iterable + { + $snmpClient = $this->createMock(SnmpClient::class); + $snmpClient->expects(self::once())->method('get')->with(['.1.2.3'])->willReturn(['.1.2.3' => 123]); + + yield 'single get' => [$snmpClient, [Request::get(['.1.2.3'])], [['.1.2.3' => 123]]]; + + $snmpClient = $this->createMock(SnmpClient::class); + $snmpClient->expects(self::once())->method('getNext')->with(['.1.2.3'])->willReturn(['.1.2.3.1' => 1231]); + + yield 'single getNext' => [$snmpClient, [Request::getNext(['.1.2.3'])], [['.1.2.3.1' => 1231]]]; + + $snmpClient = $this->createMock(SnmpClient::class); + $snmpClient->expects(self::once())->method('walk')->with('.1.2.3', 10)->willReturn(['.1.2.3.4.5' => 12345]); + + yield 'single walk' => [$snmpClient, [Request::walk('.1.2.3', 10)], [['.1.2.3.4.5' => 12345]]]; + + $snmpClient = $this->createMock(SnmpClient::class); + $snmpClient + ->expects(self::once()) + ->id('get') + ->method('get') + ->with(['.1.2.3', '.4.5.6']) + ->willReturn(['.1.2.3' => 123, '.4.5.6' => 456]); + + $snmpClient + ->expects(self::exactly(2)) + ->method('walk') + ->id('walk') + ->after('get') + ->withConsecutive( + ['.1.2.3', 10], + ['.3.2.1', 10] + ) + ->willReturnOnConsecutiveCalls( + ['.1.2.3.4.5' => 12345], + ['.3.2.1.1.2' => 32112] + ); + + $snmpClient + ->expects(self::once()) + ->after('walk') + ->method('getNext') + ->with(['.7.8.9']) + ->willReturn(['.7.8.9.1' => 7891]); + + yield 'multiple requests' => [ + $snmpClient, + [ + 'get' => Request::get(['.1.2.3', '.4.5.6']), + 'walk' => Request::walk('.1.2.3', 10), + 'getNext' => Request::getNext(['.7.8.9']), + 'anotherWalk' => Request::walk('.3.2.1', 10), + ], + [ + 'get' => ['.1.2.3' => 123, '.4.5.6' => 456], + 'walk' => ['.1.2.3.4.5' => 12345], + 'getNext' => ['.7.8.9.1' => 7891], + 'anotherWalk' => ['.3.2.1.1.2' => 32112], + ], + ]; + } + + public function testBatchNoRequests() : void + { + $this->expectExceptionObject(NoRequestsProvided::new()); + + $batchSnmpClient = $this->createBatchSnmpClient($this->createMock(SnmpClient::class)); + $batchSnmpClient->batch([]); + } + + private function createBatchSnmpClient(SnmpClient $snmpClient) : SnmpClient + { + return new class ($snmpClient) implements SnmpClient { + use SimpleBatch; + + /** @var SnmpClient */ + private $snmpClient; + + public function __construct(SnmpClient $snmpClient) + { + $this->snmpClient = $snmpClient; + } + + /** @inheritDoc */ + public function get(array $oids) : array + { + return $this->snmpClient->get($oids); + } + + /** @inheritDoc */ + public function getNext(array $oids) : array + { + return $this->snmpClient->getNext($oids); + } + + /** @inheritDoc */ + public function walk(string $oid, int $maxRepetitions = 40) : array + { + return $this->snmpClient->walk($oid, $maxRepetitions); + } + }; + } +} diff --git a/tests/Transport/ValueParserTest.php b/tests/Transport/ValueParserTest.php new file mode 100644 index 0000000..d28500e --- /dev/null +++ b/tests/Transport/ValueParserTest.php @@ -0,0 +1,44 @@ +> */ + public function providerParse() : iterable + { + yield 'Counter64' => ['Counter64: 123456', '123456']; + yield 'Hex-STRING' => ['Hex-STRING: A1 B2 C3', 'A1 B2 C3']; + yield 'IpAddress' => ['IpAddress: 127.0.0.1', '127.0.0.1']; + yield 'OID' => ['OID: .1.2.3', '.1.2.3']; + yield 'STRING' => ['STRING: "abc"', 'abc']; + yield 'empty string' => ['""', '']; + yield 'INTEGER' => ['INTEGER: 123', 123]; + yield 'Counter32' => ['Counter32: 123', 123]; + yield 'Gauge32' => ['Gauge32: 123', 123]; + yield 'Timeticks' => ['Timeticks: (3171608622) 367 days, 2:01:26.22', 3171608622]; + } + + public function testParseWithUnknownValueType() : void + { + $this->expectExceptionObject(CannotParseUnknownValueType::new('Wow')); + + ValueParser::parse('Wow: lol'); + } +} diff --git a/tests/Transport/data/public.snmprec b/tests/Transport/data/public.snmprec new file mode 100644 index 0000000..55b4ea9 --- /dev/null +++ b/tests/Transport/data/public.snmprec @@ -0,0 +1,65 @@ +# system description, multi-line string test +1.3.6.1.2.1.1.1.0|4x|436973636f20494f5320536f6674776172652c2043323936305320536f66747761726520284332393630532d554e4956455253414c4b392d4d292c2056657273696f6e2031322e32283538295345322c2052454c4541534520534f4654574152452028666331290a546563686e6963616c20537570706f72743a20687474703a2f2f7777772e636973636f2e636f6d2f74656368737570706f72740a436f707972696768742028632920313938362d323031312062792022436973636f2053797374656d732c20496e632e220a436f6d70696c6564205468752032312d4a756c2d31312030323a32322062792070726f645f72656c5f7465616d + +# system uptime +1.3.6.1.2.1.1.3.0|67|293718542 + +# interface descriptions +1.3.6.1.2.1.2.2.1.2.47|4|Ethernet47 +1.3.6.1.2.1.2.2.1.2.48|4|Ethernet48 +1.3.6.1.2.1.2.2.1.2.49001|4|Ethernet49/1 +1.3.6.1.2.1.2.2.1.2.50001|4|Ethernet50/1 +1.3.6.1.2.1.2.2.1.2.1000008|4|Port-Channel8 +1.3.6.1.2.1.2.2.1.2.1000009|4|Port-Channel9 +1.3.6.1.2.1.2.2.1.2.2002002|4|Vlan2002 +1.3.6.1.2.1.2.2.1.2.2002019|4|Vlan2019 +1.3.6.1.2.1.2.2.1.2.2002020|4|Vlan2020 +1.3.6.1.2.1.2.2.1.2.5000000|4|Loopback0 + +# interface in errors +1.3.6.1.2.1.2.2.1.14.8|65|0 +1.3.6.1.2.1.2.2.1.14.9|65|226 +1.3.6.1.2.1.2.2.1.14.10|65|256 +1.3.6.1.2.1.2.2.1.14.11|65|296 + +# IP addresses +1.3.6.1.2.1.4.20.1.1.10.100.192.2|64e|\nd\xc0\x02 +1.3.6.1.2.1.4.20.1.1.10.110.27.254|64e|\nn\x1b\xfe +1.3.6.1.2.1.4.20.1.1.66.208.216.74|64e|B\xd0\xd8J + +# IP addresses to MAC addresses +1.3.6.1.2.1.4.22.1.2.2000955.185.152.67.97|4e|\x91\xe2\xba\xe3Za +1.3.6.1.2.1.4.22.1.2.2000955.185.152.67.99|4e|ST\x00_A\xd0 +1.3.6.1.2.1.4.22.1.2.2000955.185.152.67.100|4e|ST\x00LZ] +1.3.6.1.2.1.4.22.1.2.2000955.185.152.67.102|4e|ST\x00\xa9\xa8; +1.3.6.1.2.1.4.22.1.2.2000955.185.152.67.104|4e|ST\x00Z\xa0\xca + +# host resource storage type +1.3.6.1.2.1.25.2.3.1.2.1|6|1.3.6.1.2.1.25.2.1.2 +1.3.6.1.2.1.25.2.3.1.2.2|6|1.3.6.1.2.1.25.2.1.2 +1.3.6.1.2.1.25.2.3.1.2.3|6|1.3.6.1.2.1.25.2.1.2 +1.3.6.1.2.1.25.2.3.1.2.4|6|1.3.6.1.2.1.25.2.1.9 + +# interface in octets +1.3.6.1.2.1.31.1.1.1.6.46|70|1884401752869190 +1.3.6.1.2.1.31.1.1.1.6.47|70|1883620653799494 +1.3.6.1.2.1.31.1.1.1.6.48|70|1884283891426650 +1.3.6.1.2.1.31.1.1.1.6.49001|70|2494191363092125 +1.3.6.1.2.1.31.1.1.1.6.50001|70|17658827020872235 + +# interface HC speed +1.3.6.1.2.1.31.1.1.1.15.1000001|66|100000 +1.3.6.1.2.1.31.1.1.1.15.1000003|66|60000 +1.3.6.1.2.1.31.1.1.1.15.1000005|66|80000 + +# model name may contain long Hex-STRING (test that LF is not present in the parsed value) +1.3.6.1.2.1.47.1.1.1.1.13.30|4x|4e4d2d333241202020202020202020202020ffffffffffffff + +# SNMP engine time +1.3.6.1.6.3.10.2.1.3.0|2|2937024 + +# made up thing just for testing - didn't find real examples :D +1.6.6.6.666.0|68e|\xaa\xbb\xcc + +# made up OID to test End Of MIB +1.7.8.9|4|Don't know what I'm diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 45cf08f..f3c23c6 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace SimPod\GraphQL\Utils; +namespace SimPod\PhpSnmp\Tests; use function date_default_timezone_set; use function error_reporting;