diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bbf97224..abdb0987 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -10,25 +10,37 @@ stages: - teardown - publish -lint: - image: node:8 - stage: lint +############################################################## +# # +# Jobs and commands templates # +# # +############################################################## +.install_unittest_packages_cmd: &install_unittest_packages_cmd +- npm run install-test + +.run_unittest_cmd: &run_unittest_cmd +- npm run test-only + +.job_definition: &job_definition tags: - docker-executor - script: - - npm run install-test - - npm run lint -# BIG-IP 13.x and BIG-IP 14.0, unittests only (without coverage check) -test_node4: - image: node:4 +.test_job_definition: &test_job_definition + extends: + - .job_definition stage: test + +.harness_deployment_definition: &harness_deployment_definition + image: ${CICD_CONTAINER_DEPLOY} tags: - - docker-executor + - cm-official-docker-executor + +.run_unittest: + extends: + - .test_job_definition script: - - npm run install-test - - npm install mocha@5.2.0 - - npm run test-only + - *install_unittest_packages_cmd + - *run_unittest_cmd artifacts: name: ${CI_COMMIT_REF_NAME}_unittests_artifacts paths: @@ -36,75 +48,74 @@ test_node4: when: on_failure expire_in: 3 days +############################################################## +# # +# Jobs # +# # +############################################################## + +lint: + extends: + - .test_job_definition + image: node:8 + stage: lint + script: + - *install_unittest_packages_cmd + - npm run lint + +# BIG-IP 13.x and BIG-IP 14.0, unittests only (without coverage check) +test_node4: + extends: + - .run_unittest + image: node:4 + # just in case, unittests only (without coverage check) test_node6: + extends: + - .run_unittest image: node:6 - stage: test - tags: - - docker-executor - script: - - npm run install-test - - npm run test-only - artifacts: - name: ${CI_COMMIT_REF_NAME}_unittests_artifacts - paths: - - test/artifacts - when: on_failure - expire_in: 3 days # BIG-IP 14.1+, unittests only (without coverage check) test_node8: - stage: test - tags: - - docker-executor - script: - - npm run install-test - - npm run test-only - artifacts: - name: ${CI_COMMIT_REF_NAME}_unittests_artifacts - paths: - - test/artifacts - when: on_failure - expire_in: 3 days + extends: + - .run_unittest + image: node:8 # mostly for containers, unittests only (without coverage check) test_node_latest: + extends: + - .run_unittest image: node:latest - stage: test - tags: - - docker-executor - script: - - npm run install-test - - npm run test-only - artifacts: - name: ${CI_COMMIT_REF_NAME}_unittests_artifacts - paths: - - test/artifacts - when: on_failure - expire_in: 3 days -# run tests and check code coverage -coverage: - stage: test +# packages audit +npm_audit: + extends: + - .test_job_definition + allow_failure: true script: # install jq - apt-get update - apt-get install -y jq # install node modules - - npm run install-test + - *install_unittest_packages_cmd # npm audit - install includes audit, but perform specific check and fail if needed - - audit_report=$(npm audit --json) - - echo $audit_report + - audit_report=$(npm audit --json) || echo "" + - echo "$audit_report" - actions=$(echo $audit_report | jq .actions | jq length) - if [ $actions -ne 0 ]; then echo 'ERROR! vulnerabilities exist'; exit 1; fi - # unit tests + +# run tests and check code coverage +coverage: + extends: + - .test_job_definition + script: + - *install_unittest_packages_cmd + # run tests with coverage report - npm test artifacts: name: ${CI_COMMIT_REF_NAME}_unittests_coverage paths: - coverage - tags: - - cm-official-docker-executor build_rpm: image: f5devcentral/containthedocs:rpmbuild @@ -145,23 +156,10 @@ build_docs: - docs/_build/html expire_in: 1 month -# for this job following variables should be defined: -# CICD_AUTH_OS_USERNAME - VIO user -# CICD_AUTH_OS_PASSWORD - VIO password -# CICD_AUTH_OS_PROJECT - VIO project -# or -# CICD_AUTH_OS_TOKEN - VIO auth token -# CICD_AUTH_OS_PROJECT - VIO project -# Also, variable to *enable* device pipeline should exist -# REQ_DEVICE_PIPELINE - boolean deploy_env: - image: ${CICD_CONTAINER_DEPLOY} + extends: + - .harness_deployment_definition stage: deploy - tags: - - cm-official-docker-executor - variables: - PROJECT_DECLARATION: ${CI_PROJECT_DIR}/test/functional/deployment/declaration.yml - CUSTOM_DECLARATION: "yes" artifacts: name: ${CI_COMMIT_REF_NAME}_bigip.harness_info paths: @@ -171,24 +169,10 @@ deploy_env: variables: - $REQ_DEVICE_PIPELINE == "true" script: - - export PROJECT_NAME=$([ "${CICD_PROJECT_NAME}" == "" ] && echo "test_functional_harness" || echo "${CICD_PROJECT_NAME}") - - export PROJECT_DIR="/root/deploy-projects/${PROJECT_NAME}" - - declaration=$(sed "s/_DEPLOYMENT_NAME_/${PROJECT_NAME}/g" "${PROJECT_DECLARATION}") - - echo "$declaration" > "${PROJECT_DECLARATION}" - - cat "${PROJECT_DECLARATION}" - - cd /root/cicd-bigip-deploy && make configure && - make printvars && - make setup && ls -als ${PROJECT_DIR} && - cp ${PROJECT_DIR}/harness_facts_flat.json ${CI_PROJECT_DIR}/harness_facts_flat.json + - $SHELL ./scripts/functional-testing/setup.sh test_functional: stage: functional test - script: - - export TEST_HARNESS_FILE=${CI_PROJECT_DIR}/harness_facts_flat.json - # really only need dev dependencies - - npm run install-test - - ls ./dist -ls - - npm run test-functional # troubleshooting functional test failures typically requires looking at logs, one of which is # the restnoded log that is captured by the functional tests. This saves off the folder # containing that log as an artifact to speed up the troubleshooting process @@ -204,29 +188,24 @@ test_functional: variables: # enable this job - $RUN_FUNCTIONAL_TESTS == "true" + script: + - export TEST_HARNESS_FILE=${CI_PROJECT_DIR}/harness_facts_flat.json + - ls ./dist -ls + # really only need dev dependencies + - *install_unittest_packages_cmd + - npm install mocha@7.1.0 + - npm run test-functional -# should be executed manually to remove the harness teardown_env: - image: ${CICD_CONTAINER_DEPLOY} + extends: + - .harness_deployment_definition stage: teardown - tags: - - cm-official-docker-executor - variables: - PROJECT_DECLARATION: ${CI_PROJECT_DIR}/test/functional/deployment/declaration.yml - CUSTOM_DECLARATION: "yes" - script: - - export PROJECT_NAME=$([ "${CICD_PROJECT_NAME}" == "" ] && echo "test_functional_harness" || echo "${CICD_PROJECT_NAME}") - - export PROJECT_DIR="/root/deploy-projects/${PROJECT_NAME}" - - declaration=$(sed "s/_DEPLOYMENT_NAME_/${PROJECT_NAME}/g" "${PROJECT_DECLARATION}") - - echo "$declaration" > "${PROJECT_DECLARATION}" - - cat "${PROJECT_DECLARATION}" - - cd /root/cicd-bigip-deploy && make configure && - make printvars && - make teardown when: manual only: variables: - $REQ_DEVICE_PIPELINE == "true" + script: + - $SHELL ./scripts/functional-testing/teardown.sh # Publish to internal artifactory # Note: Will publish when new tags are pushed and use the current build in dist directory @@ -289,6 +268,7 @@ pages: only: # only update on designated, stable branch - develop + - doc-release-branch # Publish docs to clouddocs.f5networks.net publish_docs_to_staging: diff --git a/CHANGELOG.md b/CHANGELOG.md index b959b0de..6e225e12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,25 @@ # Changelog Changes to this project are documented in this file. More detail and links can be found in the Telemetry Streaming [Document Revision History](https://clouddocs.f5.com/products/extensions/f5-telemetry-streaming/latest/revision-history.html). +## 1.10.0 +### Added +- AUTOTOOL-1111: Enable configurable polling with Telemetry_Endpoints (BIG-IP paths) and multiple system poller support +- AUTOTOOL-1148: Allow 'OR' logic by adding ifAnyMatch functionality. +- AUTOTOOL-853: Support F5 systems (ex: Viprion) that have multiple hosts +### Fixed +- AUTOTOOL-1051: Event Listener unable to classify AFM DoS event +- AUTOTOOL-1037: Splunk legacy tmstats - include last_cycle_count +- AUTOTOOL-1019: Splunk legacy tmstats - add tenant and application data +- AUTOTOOL-1128: Declarations with large secrets may timeout +- AUTOTOOL-1154: Passphrases should be obfuscated in consumer trace files +- AUTOTOOL-1147: Add 'profiles' data (profiles attached to Virtual Server) to 'virtualServers' +- AUTOTOOL-896: [GitHub #26](https://github.com/F5Networks/f5-telemetry-streaming/pull/26): Use baseMac instead of hostname to fetch CM device +- AUTOTOOL-1160: cipherText validation when protected by SecureVault +- AUTOTOOL-1239: Caching data about the host device to speed up declaration processing +### Changed +- AUTOTOOL-1062: Update NPM packages +### Removed + ## 1.9.0 ### Added - AUTOTOOL-725 and AUTOTOOL-755: Add support for GSLB WideIP and Pools Config and Stats diff --git a/SUPPORT.md b/SUPPORT.md index 4649083a..4a9481dd 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -19,6 +19,7 @@ Currently supported versions: |------------------|---------------|---------------------|-----------------| | TS 1.8.0 | Feature | 03-Dec-2019 | 03-Mar-2020 | | TS 1.9.0 | Feature | 28-Jan-2020 | 28-Apr-2020 | +| TS 1.10.0 | Feature | 10-Mar-2020 | 10-Jun-2020 | Versions no longer supported: diff --git a/contributing/README.md b/contributing/README.md index a41fe37f..6741853f 100644 --- a/contributing/README.md +++ b/contributing/README.md @@ -108,7 +108,7 @@ How does the project handle a typical `POST` request? "trace": false, "format": "default" }, - "schemaVersion": "1.9.0" + "schemaVersion": "1.10.0" } } ``` @@ -181,7 +181,7 @@ Collect the raw data from the device by adding a new endpoint to the paths confi ```javascript { - "endpoint": "/mgmt/tm/sys/global-settings" + "path": "/mgmt/tm/sys/global-settings" } ``` @@ -189,11 +189,11 @@ Collect the raw data from the device by adding a new endpoint to the paths confi ```javascript { - "endpoint": "/mgmt/tm/sys/someEndpoint", // REST endpoint + "path": "/mgmt/tm/sys/someEndpoint", // REST endpoint "includeStats": true, // Certain data is only available via /mgmt/tm/sys/someEndpoint as opposed to /mgmt/tm/sys/someEndpoint/stats, this property accomodates for this by making call to /stats (for each item) and adding that data to the original object "expandReferences": { "membersReference": { "endpointSuffix": "/stats" } }, // Certain data requires getting a list of objects and then in each object expanding/following references to a child object. 'membersReference' is the name of that key (currently looking under 'items' in the data returned) and will result in self link data being retrived and 'membersReference' key being replaced with that data. If 'endpointSuffix' is supplied, a suffix is added to each self link prior to retrieval, otherwise, the value of self link as is will be used. In cases like gslb where both config and stats are needed, both the `link` and `link/stats` need to be fetched, hence, the resulting config is "expandReferences": { "membersReference": { "includeStats": true } }, which is equivalent to "expandReferences": { "membersReference": { "endpointSuffix": "", "includeStats": true } }. TODO: revisit keywords/ naming here to consolidate and avoid confusion "endpointFields": [ "name", "fullPath", "selfLink", "ipProtocol", "mask" ], // Will collect only these fields from the endoint. Useful when using includeStats and the same property exists in both endpoints. Also can be used instead of a large exclude/include statement in properties.json - "body": "{ \"command\": \"run\", \"utilCmdArgs\": \"-c \\\"/bin/df -P | /usr/bin/tr -s ' ' ','\\\"\" }", // Certain information may require using POST instead of GET and require an HTTP body, if body is defined that gets used along with a POST + "body": "{ \"command\": \"run\", \"utilCmdArgs\": \"-c \\\"/bin/df -P | /usr/bin/tr -s ' ' ','\\\"\" }", // Certain information may require using POST instead of GET and require an HTTP body, if body is defined that gets used along with a POST. Body can be either string or object "name": "someStatRef", // Alternate name to reference in properties.json, default is to use the endpoint "ignoreCached": true // Invalidate cached response of previous request to endpoint } diff --git a/contributing/process_release.md b/contributing/process_release.md index d642f9ae..131e8ea3 100644 --- a/contributing/process_release.md +++ b/contributing/process_release.md @@ -23,7 +23,7 @@ * [package.json](package.json) * [package-lock.json](package-lock.json) * [project.spec](project.spec) (not required starting from 1.5) - * [src/lib/constants.js](src/lib/constants.js) + * [src/lib/constants.js](src/lib/constants.js) (not required starting from 1.10) * [src/schema/latest/base_schema.json](src/schema/latest/base_schema.json) * [contributing/README.md](contributing/README.md) (example of response, optional) * [docs/conf.py](docs/conf.py) @@ -42,6 +42,8 @@ * 1.4.0 - 8.6 MB * 1.7.0 - 8.6 MB * 1.8.0 - 9.5 MB + * 1.9.0 - 9.5 MB + * 1.10.0 - 9.5 MB * Install build to BIG-IP, navigate to folder `/var/config/rest/iapps/f5-telemetry/` and check following: * Run `du -sh` and check that folder's size (shouldn't be much greater than previous versions): * 1.4.0 - 65 MB @@ -49,6 +51,8 @@ * 1.6.0 - 66 MB * 1.7.0 - 66 MB * 1.8.0 - 73 MB + * 1.9.0 - 73 MB + * 1.10.0 - 76 MB * Check `nodejs/node_modules` folder - if you see `eslint`, `mocha` or something else from [package.json](package.json) `devDependencies` section - something wrong with build process. Probably some `npm` flags are work as not expected and it MUST BE FIXED before publishing. * Ensure that all tests (unit tests and functional tests passed) * Create pre-release tag and push it to GitLab: diff --git a/docs/conf.py b/docs/conf.py index 8f8317dc..08084c91 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -78,7 +78,7 @@ # The short X.Y version. version = u'' # The full version, including alpha/beta/rc tags. -release = u'1.9.0' +release = u'1.10.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/custom-endpoints.rst b/docs/custom-endpoints.rst new file mode 100644 index 00000000..9e317e83 --- /dev/null +++ b/docs/custom-endpoints.rst @@ -0,0 +1,134 @@ +Appendix B: Configuring Custom Endpoints +======================================== + +.. WARNING:: Configuring custom Endpoints and multiple System poller support is currently an EXPERIMENTAL feature, and the associated API could change based on testing and user feedback. + +.. NOTE:: Custom endpoints are currently for BIG-IP only. + +Telemetry Streaming v1.10 allows you to define a list of named endpoints with paths in a new **Telemetry_Endpoints** class, and includes the ability to define multiple system pollers that can fetch specific custom endpoint(s). + + +Using the Telemetry_Endpoints class +----------------------------------- +The Telemetry_Endpoints class is where you define your endpoints and their paths. + + + ++--------------------+------------+---------------------------------------------------------------------------------------------------------+ +| Parameter | Required? | Description/Notes | ++====================+============+=========================================================================================================+ +| class | Yes | Telemetry_Endpoints | ++--------------------+------------+---------------------------------------------------------------------------------------------------------+ +| basePath | No | Optional base path value to prepend to each individual endpoint path you specify in "items" | ++--------------------+------------+---------------------------------------------------------------------------------------------------------+ +| enable | No | Whether you want to enable this class. The default is **true**. | ++--------------------+------------+---------------------------------------------------------------------------------------------------------+ +| items | Yes | Object with each property an endpoint with their own properties. | ++--------------------+------------+---------------------------------------------------------------------------------------------------------+ +| \- name | No | Optional name for the item | ++--------------------+------------+---------------------------------------------------------------------------------------------------------+ +| \- path | Yes | Path to query data from | ++--------------------+------------+---------------------------------------------------------------------------------------------------------+ + + +For example, your declaration could include the following snippet, which contains endpoints for profiles, and for total connections for a virtual: + +.. code-block:: json + + { + "Endpoints_Profiles": { + "class": "Telemetry_Endpoints", + "basePath": "/mgmt/tm/ltm/profile", + "items": { + "radiusProfiles": { + "name": "radiusProfiles", + "path": "radius/stats" + }, + "ipOtherProfiles": { + "name": "ipOtherProfiles", + "path": "ipother/stats" + } + } + }, + "Endpoints_Misc": { + "class": "Telemetry_Endpoints", + "items": { + "clientside.totConns": { + "name": "virtualTotConns", + "path": "/mgmt/ltm/virtual/stats?$select=clientside.totConns" + }, + "virtualAddress": { + "path": "/mgmt/tm/ltm/virtual-address/stats" + } + } + } + } + +| + +Creating System Pollers specific to the custom endpoint +------------------------------------------------------- +Because you might want to specify different polling intervals for the custom endpoints, v1.10.0 also enables the ability to create an system poller specific to an endpoint or array of endpoints. To do this, you use the new **endpointList** property in your system poller definition. + +EndpointList is simply a list of endpoints to use in data collection, and can include the following types: + +* **Array** |br| When using an array, the item in the array must be one of the following: |br| |br| + + 1. Name of the Telemetry_Endpoints object (for example, ``Endpoints_Profiles``) + 2. Name of Telemetry_Endpoints object and the endpoint object key (``Endpoints_Profiles/radiusProfiles``) + 3. A Telemetry_Endpoint (name is required). For example: + + .. code-block:: json + + { + "path": "mgmt/tm/net/vlan/stats", + "name": "requiredWhenInline" + } + + 4. A Telemetry_Endpoints definition + +* **String** |br| The name of the Telemetry_Endpoints object + +* **Object** An object that conforms to the definition of the Telemetry_Endpoints class. + +The following is an example the system pollers, which correspond to the preceding Telemetry_Endpoints example: + +.. code-block:: json + + { + "Custom_System_Poller1": { + "class": "Telemetry_System_Poller", + "interval": 60, + "enable": false, + "endpointList": "Endpoints_Profiles", + "trace": true + }, + "Custom_System_Poller2": { + "class": "Telemetry_System_Poller", + "interval": 720, + "enable": true, + "endpointList": [ + "Endpoints_Misc/clientside.totConns", + { + "path": "mgmt/tm/net/vlan/stats", + "name": "requiredWhenInline" + } + ] + } + } + +| + + +Example declaration for using custom Endpoints with specific pollers +-------------------------------------------------------------------- +The following example contains a complete example declaration for Telemetry Streaming, which includes the snippets in the examples above. + +.. literalinclude:: ../examples/declarations/system_custom_endpoints.json + :language: json + + + +.. |br| raw:: html + +
\ No newline at end of file diff --git a/docs/data-modification.rst b/docs/data-modification.rst index ea2ac5bf..fc4f2eb9 100644 --- a/docs/data-modification.rst +++ b/docs/data-modification.rst @@ -215,6 +215,53 @@ Example 2: As result of the actions chain analysis, the Telemetry System will fetch **virtualServers** only and not **pools** (and not anything else) because only **virtualServers** should be included in the result's output. +| + +.. _valuebased: + +Value-based matching +-------------------- +.. sidebar:: :fonticon:`fa fa-info-circle fa-lg` Version Notice: + + Support for value-based matching is available in TS v1.10.0 and later + +Telemetry Streaming v1.10 adds the **ifAnyMatch** functionality to the existing value-based matching logic. Value-based matching means that TS can filter based on the value of **ifAnyMatch** instead of just the presence of the field. You can provide multiple values, and the *Action* (**includeData, excludeData or setTag**, described in detail in the section starting with :ref:`include`) is triggered if any of the blocks in the array evaluate to true. + +The following example snippet uses the **includeData** action, so if any of the virtual servers in the **test** tenant are either enabled or disabled (and have a state of **available**), then *only* the virtualServer data is included. And because it uses **includeData**, the action must evaluate to true to occur, so if none of the virtualServers have a state of available, then ALL data is included. + +.. code-block:: bash + + "actions": [ + { + "includeData": {}, + "ifAnyMatch": [ + { + "virtualServers": { + "/test/*": { + "enabledState": "enabled", + "availabilityState": "available" + } + } + }, + { + "virtualServers": { + "/test/*": { + "enabledState": "disabled", + "availabilityState": "available" + } + } + } + ], + "locations": { + "virtualServers": { + ".*": true + } + } + }, + + +For a complete declaration with value-based matching, see :ref:`value`. + | | diff --git a/docs/declarations.rst b/docs/declarations.rst index f5d27110..16d49762 100644 --- a/docs/declarations.rst +++ b/docs/declarations.rst @@ -61,7 +61,6 @@ Example 4: iHealth Poller | - .. _referencedpollers: Example 5: Referenced Pollers @@ -76,3 +75,37 @@ Example 5: Referenced Pollers :ref:`Back to top` | + +.. _customendpoint: + +Example 6: Custom Endpoints +--------------------------- +.. IMPORTANT:: Configuring custom endpoints and multiple system pollers specific to those endpoints is currently EXPERIMENTAL and is available in TS v1.10.0 and later. See :doc:`custom-endpoints` for more information on this feature. + +| + +.. literalinclude:: ../examples/declarations/system_custom_endpoints.json + :language: json + + + + +:ref:`Back to top` + +| + +.. _value: + +Example 7: Value-based matching +------------------------------- +.. IMPORTANT:: Value-based matching is available in TS v1.10.0 and later. See :ref:`valuebased` for more information on this feature. + +.. literalinclude:: ../examples/declarations/action_matching.json + :language: json + + + + +:ref:`Back to top` + +| \ No newline at end of file diff --git a/docs/faq.rst b/docs/faq.rst index 80712bb5..7ba97bb0 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -106,6 +106,23 @@ Telemetry Streaming does not currently enforce validation of the data that an ev For complete information and examples, see :ref:`char-encoding`. +| + +.. _contract: + +**What is F5's Automation Toolchain API Contract?** + +The API Contract for the F5 Automation Toolchain (Telemetry Streaming, AS3, and Declarative Onboarding) is our assurance that we will not make arbitrary breaking changes to our API. We take this commitment seriously. We semantically version our declarative API schemas ("xx.yy.zz") and do not make breaking changes within a minor ("yy") or patch ("zz") releases. For example, early declarations using AS3 schema "3.0.0" are accepted by all subsequent minor releases including "3.16.0." + +As of January 2020, no breaking changes have been made to AS3, Declarative Onboarding, or Telemetry Streaming since inception. None are anticipated at this time. A breaking change, if any, will be noted by a change to the major release number ("xx"). For example, the AS3 schema version would become "4.0.0." + +| + +.. _viprion: + +**Can I use Telemetry Streaming on F5 devices with multiple hosts, such as the Viprion platform?** + +Beginning with TS v1.10.0, you can use Telemetry Streaming on F5 devices with multiple hosts, such as the Viprion platform and vCMP systems. In versions prior to v1.10, devices with multiple hosts were not supported. .. |intro| raw:: html diff --git a/docs/index.rst b/docs/index.rst index 9f00061c..892d7bfc 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -58,6 +58,7 @@ Previous buttons to explore the documentation. troubleshooting revision-history schema-reference + custom-endpoints .. |video| raw:: html diff --git a/docs/output-example.rst b/docs/output-example.rst index 04f4c43a..a0d4017e 100644 --- a/docs/output-example.rst +++ b/docs/output-example.rst @@ -9,6 +9,9 @@ Use this page to see the type of information that Telemetry Streaming collects. System Information ------------------ +The following shows the system information that Telemetry Streaming collects. + +.. NOTE:: For some of the output to appear, you must have the applicable BIG-IP module licensed and provisioned (for example, you must have BIG-IP DNS provisioned to get GSLB wide IP and Pool information). .. literalinclude:: ../examples/output/system_poller/output.json :language: json diff --git a/docs/revision-history.rst b/docs/revision-history.rst index 9f3b84ee..c598746c 100644 --- a/docs/revision-history.rst +++ b/docs/revision-history.rst @@ -11,8 +11,12 @@ Document Revision History - Description - Date + * - 1.10.0 + - Updated the documentation for Telemetry Streaming v1.10.0. This release contains the following changes: |br| * Added a feature (currently EXPERIMENTAL) for configuring custom endpoints (see :doc:`custom-endpoints`) |br| * Added **ifAnyMatch** functionality to the existing value-based matching logic (see :ref:`valuebased`) |br| * Added support for F5 devices with multiple hosts (see the :ref:`FAQ`) |br| |br| Issues Resolved: |br| * Event Listener unable to classify AFM DoS event |br| * Splunk legacy tmstats - include last_cycle_count |br| * Splunk legacy tmstats - add tenant and application data |br| * Declarations with large secrets may timeout |br| * Passphrases should be obfuscated in consumer trace files |br| * Add 'profiles' data (profiles attached to Virtual Server) to 'virtualServers' |br| * Use baseMac instead of hostname to fetch CM device (`GitHub Issue 26 `_) |br| * cipherText validation when protected by SecureVault |br| * Caching data about the host device to speed up declaration processing + - 03-10-20 + * - 1.9.0 - - Updated the documentation for Telemetry Streaming v1.9.0. This release contains the following changes: |br| * Username and passphrase are now optional on the AWS CloudWatch consumer (see the important note in :ref:`awscloud-ref`) |br| * Added detailed information about character encoding and Telemetry Streaming (see :ref:`char-encoding`) |br| |br| Issues Resolved: |br| * Basic auth does not work with ElasticSearch consumer + - Updated the documentation for Telemetry Streaming v1.9.0. This release contains the following changes: |br| * Added support for gathering configuration information and statistics for GSLB Wide IP and Pools (see :ref:`System Information example output`) |br| * Username and passphrase are now optional on the AWS CloudWatch consumer (see the important note in :ref:`awscloud-ref`) |br| * Added detailed information about character encoding and Telemetry Streaming (see :ref:`char-encoding`) |br| * Added a FAQ entry to define the F5 Automation Toolchain API contract (see :ref:`What is the Automation Toolchain API Contract?`) |br| |br| Issues Resolved: |br| * Basic auth does not work with ElasticSearch consumer |br| * Some Splunk legacy tmstats datamodels have a period in property name instead of underscore - 01-28-20 * - 1.8.0 diff --git a/examples/declarations/action_matching.json b/examples/declarations/action_matching.json new file mode 100644 index 00000000..4faa5f1e --- /dev/null +++ b/examples/declarations/action_matching.json @@ -0,0 +1,49 @@ +{ + "class": "Telemetry", + "My_System": { + "class": "Telemetry_System", + "systemPoller": { + "interval": 60, + "actions": [ + { + "includeData": {}, + "ifAnyMatch": [ + { + "virtualServers": { + "/test/*": { + "enabledState": "enabled", + "availabilityState": "available" + } + } + }, + { + "virtualServers": { + "/test/*": { + "enabledState": "disabled", + "availabilityState": "available" + } + } + } + ], + "locations": { + "virtualServers": { + ".*": true + } + } + }, + { + "excludeData": {}, + "ifAllMatch": { + "system": { + "licenseReady": "no", + "provisionReady": "no" + } + }, + "locations": { + ".*": true + } + } + ] + } + } +} diff --git a/examples/declarations/system_custom_endpoints.json b/examples/declarations/system_custom_endpoints.json new file mode 100644 index 00000000..8dcc5c97 --- /dev/null +++ b/examples/declarations/system_custom_endpoints.json @@ -0,0 +1,66 @@ +{ + "class": "Telemetry", + "Endpoints_Profiles": { + "class": "Telemetry_Endpoints", + "basePath": "/mgmt/tm/ltm/profile", + "items": { + "radiusProfiles": { + "name": "radiusProfiles", + "path": "radius/stats" + }, + "ipOtherProfiles": { + "name": "ipOtherProfiles", + "path": "ipother/stats" + } + } + }, + "Endpoints_Misc": { + "class": "Telemetry_Endpoints", + "items": { + "clientside.totConns": { + "name": "virtualTotConns", + "path": "/mgmt/ltm/virtual/stats?$select=clientside.totConns" + }, + "virtualAddress": { + "path": "/mgmt/tm/ltm/virtual-address/stats" + } + } + }, + "Custom_System": { + "class": "Telemetry_System", + "systemPoller": [ + "Custom_System_Poller1", + "Custom_System_Poller2", + { + "interval": 60 + } + ], + "enable": true, + "trace": true + }, + "Custom_System_Poller1": { + "class": "Telemetry_System_Poller", + "interval": 60, + "enable": false, + "endpointList": "Endpoints_Profiles", + "trace": true + }, + "Custom_System_Poller2": { + "class": "Telemetry_System_Poller", + "interval": 720, + "enable": true, + "endpointList": [ + "Endpoints_Misc/clientside.totConns", + { + "path": "mgmt/tm/net/vlan/stats", + "name": "requiredWhenInline" + } + ] + }, + "Default_System": { + "class": "Telemetry_System", + "systemPoller": { + "interval": 360 + } + } +} diff --git a/examples/output/system_poller/output.json b/examples/output/system_poller/output.json index 876172b2..8e5f8ae7 100644 --- a/examples/output/system_poller/output.json +++ b/examples/output/system_poller/output.json @@ -280,7 +280,19 @@ "ipProtocol": "tcp", "tenant": "Common", "pool": "/Common/foofoo.app/foofoo_pool", - "application": "foofoo.app" + "application": "foofoo.app", + "profiles": { + "/Common/tcp": { + "name": "/Common/tcp", + "tenant": "Common" + }, + "/Common/app/http": { + "name": "/Common/app/http", + "tenant": "Common", + "application": "app" + } + } + }, "/Example_Tenant/A1/serviceMain": { "clientside.bitsIn": 0, @@ -294,7 +306,8 @@ "ipProtocol": "tcp", "tenant": "Example_Tenant", "pool": "/Example_Tenant/A1/barbar_pool", - "application": "A1" + "application": "A1", + "profiles": {} }, "/Example_Tenant/A1/serviceMain-Redirect": { "clientside.bitsIn": 0, @@ -305,7 +318,18 @@ "enabledState": "enabled", "name": "/Example_Tenant/A1/serviceMain-Redirect", "tenant": "Example_Tenant", - "application": "A1" + "application": "A1", + "profiles": { + "/Common/customTcp": { + "name": "/Common/customTcp", + "tenant": "Common" + }, + "/Common/app/http": { + "name": "/Common/app/http", + "tenant": "Common", + "application": "app" + } + } } }, "pools": { diff --git a/package-lock.json b/package-lock.json index fba5c962..4715b99c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "f5-telemetry", - "version": "1.9.0", + "version": "1.10.0-2", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -164,11 +164,18 @@ } }, "@f5devcentral/f5-teem": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@f5devcentral/f5-teem/-/f5-teem-1.1.0.tgz", - "integrity": "sha512-naNf4ZB5+H+qfpYvhfW+cxHZ37uXFc736UzQDhUNpymIO5NCZqSTunf7gg+STCZFNGYQmWf1czeHRBpMEOJn8w==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@f5devcentral/f5-teem/-/f5-teem-1.2.0.tgz", + "integrity": "sha512-d9H3bzFcqREDdZNcqcbAOr+AFCOy3Alc8kudh3FkTqfWXqy/ooIuTb9Y+1Q8QayCWvp3fuXvQksprMuwQpnnfQ==", "requires": { - "uuid": "^3.3.2" + "uuid": "^3.4.0" + }, + "dependencies": { + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + } } }, "@sinonjs/commons": { @@ -232,11 +239,11 @@ } }, "ajv": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", - "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz", + "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==", "requires": { - "fast-deep-equal": "^2.0.1", + "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" @@ -313,13 +320,182 @@ "dev": true }, "array-includes": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.0.3.tgz", - "integrity": "sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0=", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.1.tgz", + "integrity": "sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ==", "dev": true, "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.7.0" + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0", + "is-string": "^1.0.5" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.4", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz", + "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.1.5", + "is-regex": "^1.0.5", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimleft": "^2.1.1", + "string.prototype.trimright": "^2.1.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "is-callable": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", + "dev": true + }, + "is-regex": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", + "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "object-inspect": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", + "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", + "dev": true + }, + "string.prototype.trimleft": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", + "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } + }, + "string.prototype.trimright": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", + "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } + } + } + }, + "array.prototype.flat": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz", + "integrity": "sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.4", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz", + "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.1.5", + "is-regex": "^1.0.5", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimleft": "^2.1.1", + "string.prototype.trimright": "^2.1.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "is-callable": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", + "dev": true + }, + "is-regex": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", + "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "object-inspect": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", + "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", + "dev": true + }, + "string.prototype.trimleft": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", + "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } + }, + "string.prototype.trimright": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", + "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } + } } }, "asn1": { @@ -361,19 +537,26 @@ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "aws-sdk": { - "version": "2.564.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.564.0.tgz", - "integrity": "sha512-X5MbcebjQ3iPNBvZ27WZyMEVCleBLqot2hqVz2M9XvMDR4B8qqPuteWrtbLu+DVjENvVD7Oj0BOIjrYEVWacFA==", - "requires": { - "buffer": "^4.9.1", - "events": "^1.1.1", - "ieee754": "^1.1.13", - "jmespath": "^0.15.0", - "querystring": "^0.2.0", - "sax": "^1.2.1", - "url": "^0.10.3", - "uuid": "^3.3.2", - "xml2js": "^0.4.19" + "version": "2.621.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.621.0.tgz", + "integrity": "sha512-wf87zTPXx2cILc9kAKTXcSrAc+vCc7BxE7G8vPEWAreCDucLHbynachYQvwO5ql+I3Eq651/X2XjnY01niSTNw==", + "requires": { + "buffer": "4.9.1", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.15.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "uuid": "3.3.2", + "xml2js": "0.4.19" + }, + "dependencies": { + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + } } }, "aws-sign2": { @@ -382,9 +565,9 @@ "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" }, "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz", + "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==" }, "balanced-match": { "version": "1.0.0", @@ -851,6 +1034,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, "requires": { "object-keys": "^1.0.12" } @@ -942,6 +1126,7 @@ "version": "1.16.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.16.0.tgz", "integrity": "sha512-xdQnfykZ9JMEiasTAJZJdMWCQ1Vm00NBw79/AWi7ELfZuuPCSOMDZbT9mkOfSctVtfhb+sAAzrm+j//GjjLHLg==", + "dev": true, "requires": { "es-to-primitive": "^1.2.0", "function-bind": "^1.1.1", @@ -959,6 +1144,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", + "dev": true, "requires": { "is-callable": "^1.1.4", "is-date-object": "^1.0.1", @@ -1087,42 +1273,54 @@ } }, "eslint-import-resolver-node": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz", - "integrity": "sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.3.tgz", + "integrity": "sha512-b8crLDo0M5RSe5YG8Pu2DYBj71tSB6OvXkfzwbJU2w7y8P4/yo0MyF8jU26IEuEuHF2K5/gcAJE3LhQGqBBbVg==", "dev": true, "requires": { "debug": "^2.6.9", - "resolve": "^1.5.0" + "resolve": "^1.13.1" + }, + "dependencies": { + "resolve": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz", + "integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + } } }, "eslint-module-utils": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.4.1.tgz", - "integrity": "sha512-H6DOj+ejw7Tesdgbfs4jeS4YMFrT8uI8xwd1gtQqXssaR0EQ26L+2O/w6wkYFy2MymON0fTwHmXBvvfLNZVZEw==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.5.2.tgz", + "integrity": "sha512-LGScZ/JSlqGKiT8OC+cYRxseMjyqt6QO54nl281CK93unD89ijSeRV6An8Ci/2nvWVKe8K/Tqdm75RQoIOCr+Q==", "dev": true, "requires": { - "debug": "^2.6.8", + "debug": "^2.6.9", "pkg-dir": "^2.0.0" } }, "eslint-plugin-import": { - "version": "2.18.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.18.2.tgz", - "integrity": "sha512-5ohpsHAiUBRNaBWAF08izwUGlbrJoJJ+W9/TBwsGoR1MnlgfwMIKrFeSjWbt6moabiXW9xNvtFz+97KHRfI4HQ==", + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.20.1.tgz", + "integrity": "sha512-qQHgFOTjguR+LnYRoToeZWT62XM55MBVXObHM6SKFd1VzDcX/vqT1kAz8ssqigh5eMj8qXcRoXXGZpPP6RfdCw==", "dev": true, "requires": { "array-includes": "^3.0.3", + "array.prototype.flat": "^1.2.1", "contains-path": "^0.1.0", "debug": "^2.6.9", "doctrine": "1.5.0", "eslint-import-resolver-node": "^0.3.2", - "eslint-module-utils": "^2.4.0", + "eslint-module-utils": "^2.4.1", "has": "^1.0.3", "minimatch": "^3.0.4", "object.values": "^1.1.0", "read-pkg-up": "^2.0.0", - "resolve": "^1.11.0" + "resolve": "^1.12.0" }, "dependencies": { "doctrine": { @@ -1256,14 +1454,14 @@ "dev": true }, "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==" }, "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "fast-levenshtein": { "version": "2.0.6", @@ -1448,7 +1646,8 @@ "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true }, "functional-red-black-tree": { "version": "1.0.1", @@ -1568,6 +1767,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, "requires": { "function-bind": "^1.1.1" } @@ -1589,7 +1789,8 @@ "has-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", - "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=" + "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", + "dev": true }, "has-unicode": { "version": "2.0.1", @@ -1815,12 +2016,14 @@ "is-callable": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", - "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==" + "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", + "dev": true }, "is-date-object": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=" + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", + "dev": true }, "is-fullwidth-code-point": { "version": "1.0.0", @@ -1847,6 +2050,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "dev": true, "requires": { "has": "^1.0.1" } @@ -1857,10 +2061,17 @@ "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", "dev": true }, + "is-string": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", + "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==", + "dev": true + }, "is-symbol": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", + "dev": true, "requires": { "has-symbols": "^1.0.0" } @@ -2259,16 +2470,16 @@ } }, "mime-db": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", - "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz", + "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==" }, "mime-types": { - "version": "2.1.24", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", - "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "version": "2.1.26", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz", + "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==", "requires": { - "mime-db": "1.40.0" + "mime-db": "1.43.0" } }, "mimic-fn": { @@ -2389,9 +2600,9 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "mustache": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mustache/-/mustache-3.1.0.tgz", - "integrity": "sha512-3Bxq1R5LBZp7fbFPZzFe5WN4s0q3+gxZaZuZVY+QctYJiCiVgXHOTIC0/HgZuOPFt/6BQcx5u0H2CUOxT/RoGQ==" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.0.0.tgz", + "integrity": "sha512-FJgjyX/IVkbXBXYUwH+OYwQKqWpFPLaLVESd70yHjSDunwzV2hZOoTBvPf4KLoxesUzzyfTH6F784Uqd7Wm5yA==" }, "mute-stream": { "version": "0.0.7", @@ -2664,7 +2875,8 @@ "object-inspect": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.6.0.tgz", - "integrity": "sha512-GJzfBZ6DgDAmnuaM3104jR4s1Myxr3Y3zfIyN4z3UdqN69oSRacNK8UhnobDdC+7J2AHCjGwxQubNJfE70SXXQ==" + "integrity": "sha512-GJzfBZ6DgDAmnuaM3104jR4s1Myxr3Y3zfIyN4z3UdqN69oSRacNK8UhnobDdC+7J2AHCjGwxQubNJfE70SXXQ==", + "dev": true }, "object-is": { "version": "1.0.1", @@ -2675,7 +2887,8 @@ "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true }, "object.assign": { "version": "4.1.0", @@ -2701,25 +2914,95 @@ "has": "^1.0.3" } }, - "object.getownpropertydescriptors": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", - "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", - "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.5.1" - } - }, "object.values": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.0.tgz", - "integrity": "sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.1.tgz", + "integrity": "sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==", "dev": true, "requires": { "define-properties": "^1.1.3", - "es-abstract": "^1.12.0", + "es-abstract": "^1.17.0-next.1", "function-bind": "^1.1.1", "has": "^1.0.3" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.4", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz", + "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.1.5", + "is-regex": "^1.0.5", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimleft": "^2.1.1", + "string.prototype.trimright": "^2.1.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "is-callable": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", + "dev": true + }, + "is-regex": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", + "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "object-inspect": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", + "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", + "dev": true + }, + "string.prototype.trimleft": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", + "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } + }, + "string.prototype.trimright": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", + "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } + } } }, "once": { @@ -2993,9 +3276,9 @@ "dev": true }, "psl": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.4.0.tgz", - "integrity": "sha512-HZzqCGPecFLyoRj5HLfuDSKYTJkAfB5thKBIkRHtGjWwY7p1dAyveIbXIq4tO0KYfDF2tHqPUgY9SDnGm00uFw==" + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.7.0.tgz", + "integrity": "sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ==" }, "pump": { "version": "3.0.0", @@ -3101,9 +3384,9 @@ } }, "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", "requires": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", @@ -3112,7 +3395,7 @@ "extend": "~3.0.2", "forever-agent": "~0.6.1", "form-data": "~2.3.2", - "har-validator": "~5.1.0", + "har-validator": "~5.1.3", "http-signature": "~1.2.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", @@ -3122,7 +3405,7 @@ "performance-now": "^2.1.0", "qs": "~6.5.2", "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", + "tough-cookie": "~2.5.0", "tunnel-agent": "^0.6.0", "uuid": "^3.3.2" } @@ -3206,9 +3489,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" }, "semver": { "version": "5.7.1", @@ -3380,18 +3663,18 @@ "dev": true }, "ssh2": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-0.8.5.tgz", - "integrity": "sha512-TkvzxSYYUSQ8jb//HbHnJVui4fVEW7yu/zwBxwro/QaK2EGYtwB+8gdEChwHHuj142c5+250poMC74aJiwApPw==", + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-0.8.7.tgz", + "integrity": "sha512-/u1BO12kb0lDVxJXejWB9pxyF3/ncgRqI9vPCZuPzo05pdNDzqUeQRavScwSPsfMGK+5H/VRqp1IierIx0Bcxw==", "dev": true, "requires": { - "ssh2-streams": "~0.4.4" + "ssh2-streams": "~0.4.8" } }, "ssh2-streams": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/ssh2-streams/-/ssh2-streams-0.4.6.tgz", - "integrity": "sha512-jXq/nk2K82HuueO9CTCdas/a0ncX3fvYzEPKt1+ftKwE5RXTX25GyjcpjBh2lwVUYbk0c9yq6cBczZssWmU3Tw==", + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/ssh2-streams/-/ssh2-streams-0.4.8.tgz", + "integrity": "sha512-auxXfgYySz2vYw7TMU7PK7vFI7EPvhvTH8/tZPgGaWocK4p/vwCMiV3icz9AEkb0R40kOKZtFtqYIxDJyJiytw==", "dev": true, "requires": { "asn1": "~0.2.0", @@ -3442,6 +3725,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz", "integrity": "sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw==", + "dev": true, "requires": { "define-properties": "^1.1.3", "function-bind": "^1.1.1" @@ -3451,6 +3735,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz", "integrity": "sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg==", + "dev": true, "requires": { "define-properties": "^1.1.3", "function-bind": "^1.1.1" @@ -3731,19 +4016,12 @@ "dev": true }, "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" - }, - "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" - } + "psl": "^1.1.28", + "punycode": "^2.1.1" } }, "traverse": { @@ -3839,15 +4117,6 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, - "util.promisify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", - "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", - "requires": { - "define-properties": "^1.1.2", - "object.getownpropertydescriptors": "^2.0.3" - } - }, "uuid": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", @@ -4011,19 +4280,18 @@ } }, "xml2js": { - "version": "0.4.22", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.22.tgz", - "integrity": "sha512-MWTbxAQqclRSTnehWWe5nMKzI3VmJ8ltiJEco8akcC6j3miOhjjfzKum5sId+CWhfxdOs/1xauYr8/ZDBtQiRw==", + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", "requires": { "sax": ">=0.6.0", - "util.promisify": "~1.0.0", - "xmlbuilder": "~11.0.0" + "xmlbuilder": "~9.0.1" } }, "xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" }, "y18n": { "version": "4.0.0", diff --git a/package.json b/package.json index 5713cd37..ce32f3dc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "f5-telemetry", - "version": "1.9.0", + "version": "1.10.0-2", "author": "F5 Networks", "license": "Apache-2.0", "repository": { @@ -12,7 +12,7 @@ "install-test": "npm install --no-optional", "lint": "eslint src test", "test-functional": "mocha \"./test/functional/testRunner.js\" --opts ./test/functional/.mocha.opts", - "test-only": "mocha --recursive \"./test/unit/**/*.js\" --opts ./test/unit/.mocha.opts", + "test-only": "mocha --opts ./test/unit/.mocha.opts", "test": "nyc --all npm run test-only", "build": "./scripts/build/buildRpm.sh" }, @@ -32,33 +32,30 @@ ] }, "dependencies": { - "@f5devcentral/f5-teem": "^1.1.0", - "ajv": "^6.5.4", + "@f5devcentral/f5-teem": "^1.2.0", + "ajv": "^6.12.0", "ajv-async": "^1.0.1", - "aws-sdk": "^2.369.0", + "aws-sdk": "^2.621.0", "commander": "^2.19.0", "deep-diff": "^1.0.2", "elasticsearch": "^15.3.0", "jsonwebtoken": "^8.5.1", "kafka-node": "^2.6.1", - "mustache": "^3.0.0", + "mustache": "^4.0.0", "node-statsd": "0.1.1", - "request": "^2.88.0" + "request": "^2.88.2" }, "devDependencies": { "@f5devcentral/eslint-config-f5-atg": "latest", "chai": "^4.2.0", "chai-as-promised": "^7.1.1", - "eslint": "^5.16.0", - "eslint-config-airbnb-base": "^13.1.0", - "eslint-plugin-import": "^2.17.3", - "icrdk": "git://github.com/f5devcentral/f5-icontrollx-dev-kit#master", + "icrdk": "git://github.com/f5devcentral/f5-icontrollx-dev-kit.git#master", "mocha": "^5.2.0", "nock": "10.0.0", "nyc": "^14.1.1", "proxyquire": "^2.1.3", "sinon": "^7.4.1", - "ssh2": "^0.8.2", + "ssh2": "^0.8.7", "winston": "^2.4.4" }, "eslintConfig": { diff --git a/scripts/functional-testing/setup.sh b/scripts/functional-testing/setup.sh new file mode 100644 index 00000000..77818f2b --- /dev/null +++ b/scripts/functional-testing/setup.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +set -e + +# BIG-IP deployment tool variables stored in GitLab: +# CICD_AUTH_OS_USERNAME - VIO user +# CICD_AUTH_OS_PASSWORD - VIO password +# CICD_AUTH_OS_PROJECT - VIO project +# or +# CICD_AUTH_OS_TOKEN - VIO auth token +# CICD_AUTH_OS_PROJECT - VIO project + +# BIG-IP deployment tool variables: +export CUSTOM_DECLARATION="yes" +export PROJECT_DECLARATION="${CI_PROJECT_DIR}/test/functional/deployment/declaration.yml" +export PROJECT_NAME=$([ "${CICD_PROJECT_NAME}" == "" ] && echo "test_functional_harness" || echo "${CICD_PROJECT_NAME}") +export PROJECT_DIR="/root/deploy-projects/${PROJECT_NAME}" + +echo "CUSTOM_DECLARATION = ${CUSTOM_DECLARATION}" +echo "PROJECT_NAME = ${PROJECT_NAME}" +echo "PROJECT_DIR = ${PROJECT_DIR}" +echo "PROJECT_DECLARATION = ${PROJECT_DECLARATION}" + +declaration=$(sed "s/_DEPLOYMENT_NAME_/${PROJECT_NAME}/g" "${PROJECT_DECLARATION}") +echo "$declaration" > "${PROJECT_DECLARATION}" +cat "${PROJECT_DECLARATION}" + +# ready to start deployment +cd /root/cicd-bigip-deploy +make configure +make printvars +make setup + +# for debugging purpose only +ls -als ${PROJECT_DIR} + +# copy info about deployed harness to project's folder to create artifcat +cp ${PROJECT_DIR}/harness_facts_flat.json ${CI_PROJECT_DIR}/harness_facts_flat.json diff --git a/scripts/functional-testing/teardown.sh b/scripts/functional-testing/teardown.sh new file mode 100644 index 00000000..2cc98785 --- /dev/null +++ b/scripts/functional-testing/teardown.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +set -e + +# BIG-IP deployment tool variables stored in GitLab: +# CICD_AUTH_OS_USERNAME - VIO user +# CICD_AUTH_OS_PASSWORD - VIO password +# CICD_AUTH_OS_PROJECT - VIO project +# or +# CICD_AUTH_OS_TOKEN - VIO auth token +# CICD_AUTH_OS_PROJECT - VIO project + +# BIG-IP deployment tool variables: +export CUSTOM_DECLARATION="yes" +export PROJECT_DECLARATION="${CI_PROJECT_DIR}/test/functional/deployment/declaration.yml" +export PROJECT_NAME=$([ "${CICD_PROJECT_NAME}" == "" ] && echo "test_functional_harness" || echo "${CICD_PROJECT_NAME}") +export PROJECT_DIR="/root/deploy-projects/${PROJECT_NAME}" + +echo "CUSTOM_DECLARATION = ${CUSTOM_DECLARATION}" +echo "PROJECT_NAME = ${PROJECT_NAME}" +echo "PROJECT_DIR = ${PROJECT_DIR}" +echo "PROJECT_DECLARATION = ${PROJECT_DECLARATION}" + +declaration=$(sed "s/_DEPLOYMENT_NAME_/${PROJECT_NAME}/g" "${PROJECT_DECLARATION}") +echo "$declaration" > "${PROJECT_DECLARATION}" +cat "${PROJECT_DECLARATION}" + +# ready to start deployment +cd /root/cicd-bigip-deploy +make configure +make printvars +make teardown diff --git a/scripts/schema-build.js b/scripts/schema-build.js index 21979d83..7625df15 100644 --- a/scripts/schema-build.js +++ b/scripts/schema-build.js @@ -8,6 +8,7 @@ 'use strict'; +const assert = require('assert'); const fs = require('fs'); // const base = require('../src/schema/latest/base_schema.json'); @@ -15,7 +16,26 @@ const fs = require('fs'); const SCHEMA_DIR = `${__dirname}/../src/schema/latest`; const outputFile = `${__dirname}/../dist/ts.schema.json`; -const safeTraverse = (p, o) => p.reduce((xs, x) => (xs && xs[x] ? xs[x] : null), o); +const safeTraverse = (pathArray, parentObject) => pathArray.reduce( + (curObj, curPath) => (typeof curObj !== 'undefined' && typeof curObj[curPath] !== 'undefined' ? curObj[curPath] : undefined), + parentObject +); + +const normalizeReference = (ref, schemaId) => { + ref = ref.startsWith('#') ? `${schemaId}${ref}` : ref; + return ref.split('#').join(''); +}; + +const getReferenceValue = (ref, schemaId, schemaMap) => { + const normalizedRef = normalizeReference(ref, schemaId); + const refParts = normalizedRef.split('/'); + const definition = safeTraverse(refParts, schemaMap); + assert.notStrictEqual(definition, undefined, `Unable to dereference '${ref}' from schema with id '${schemaId}'`); + return { + definition, + schemaId: refParts[0] + }; +}; function writeSchema(name, data) { return new Promise((resolve, reject) => { @@ -27,58 +47,65 @@ function writeSchema(name, data) { } function combineSchemas() { - const paths = fs.readdirSync(`${SCHEMA_DIR}/`) + const base = { definitions: {} }; + const schemaMap = {}; + + fs.readdirSync(`${SCHEMA_DIR}/`) .filter(name => !(name.includes('draft')) && name.endsWith('schema.json')) - .map(fileName => `${SCHEMA_DIR}/${fileName}`); + .map(fileName => `${SCHEMA_DIR}/${fileName}`) + .forEach((path) => { + const schema = JSON.parse(fs.readFileSync(path, 'utf8')); + assert.notStrictEqual(schema.$id, undefined, `Schema at path '${path}' should have $id property`); + schemaMap[schema.$id] = schema; + }); - const base = { definitions: {} }; - const contents = []; - const defs = {}; - - paths.forEach((path) => { - const content = JSON.parse(fs.readFileSync(path, 'utf8')); - contents.push(content); - if (content.definitions) { - Object.assign(defs, content.definitions); + Object.keys(schemaMap).forEach((schemaId) => { + const schema = schemaMap[schemaId]; + if (!schema.allOf) { + return; } - }); - contents.forEach((content) => { - if (content.allOf) { - content.allOf.forEach((tsClass) => { - const classType = safeTraverse(['if', 'properties', 'class', 'const'], tsClass); - - if (classType) { - const tmp = {}; - const propKeys = Object.keys(tsClass.then.properties); - propKeys.forEach((propKey) => { - const prop = tsClass.then.properties[propKey]; - - // dereference all values - const ref = prop.$ref; - if (ref) { - const def = ref.split('/').pop(); - tsClass.then.properties[propKey] = defs[def]; - } else if (prop.allOf) { - const def = prop.allOf[0].$ref.split('/').pop(); - tsClass.then.properties[propKey] = defs[def]; - } else if (prop.oneOf) { - const def = prop.oneOf[1].allOf[1].$ref.split('/').pop(); - tsClass.then.properties[propKey] = defs[def]; - } - - // inherit default value on top of the definition - if (prop.$ref || prop.allOf || prop.oneOf) { - tsClass.then.properties[propKey].default = prop.default; - } - }); - - tmp[classType] = tsClass.then; - tmp[classType].description = tsClass.description; - base.definitions = Object.assign(base.definitions, tmp); + schema.allOf.forEach((tsClass) => { + const classType = safeTraverse(['if', 'properties', 'class', 'const'], tsClass); + if (!classType) { + return; + } + + const tmp = {}; + const properties = tsClass.then.properties; + + Object.keys(properties).forEach((propKey) => { + const prop = properties[propKey]; + // dereference all values + if (prop.$ref) { + properties[propKey] = getReferenceValue(prop.$ref, schemaId, schemaMap).definition; + } else if (prop.allOf) { + properties[propKey] = getReferenceValue(prop.allOf[0].$ref, schemaId, schemaMap).definition; + } else if (prop.oneOf) { + let value; + if (propKey === 'systemPoller') { + // Telemetry_System -> systemPoller property -> ref to systemPollerObjectRef -> systemPoller + value = getReferenceValue(prop.oneOf[1].$ref, schemaId, schemaMap); + value = getReferenceValue(value.definition.allOf[1].$ref, value.schemaId, schemaMap).definition; + } else if (propKey === 'iHealthPoller') { + // Telemetry_System -> iHealthPoller property -> ref to iHealthPollerRef -> iHealthPoller + value = getReferenceValue(prop.oneOf[1].$ref, schemaId, schemaMap); + value = getReferenceValue(value.definition.allOf[1].$ref, value.schemaId, schemaMap).definition; + } else { + value = getReferenceValue(prop.oneOf[1].allOf[1].$ref, schemaId, schemaMap).definition; + } + properties[propKey] = value; + } + // inherit default value on top of the definition + if (prop.$ref || prop.allOf || prop.oneOf) { + properties[propKey].default = prop.default; } }); - } + + tmp[classType] = tsClass.then; + tmp[classType].description = tsClass.description; + base.definitions = Object.assign(base.definitions, tmp); + }); }); return writeSchema(outputFile, base); } diff --git a/scripts/schema-to-rst.js b/scripts/schema-to-rst.js index 8a70f80c..da85bb53 100644 --- a/scripts/schema-to-rst.js +++ b/scripts/schema-to-rst.js @@ -242,7 +242,7 @@ function getProperties(definition, props, defName) { function conditionalDescription(definition, props, defName) { if ((definition.if && definition.if.properties) - || (definition.else && definition.else.properties)) { + || (definition.else && definition.else.properties && !definition.if.required)) { const conditionalProps = definition.then; if (conditionalProps.properties) { const conditionalKey = Object.keys(definition.if.properties)[0]; diff --git a/src/lib/config.js b/src/lib/config.js index df19c321..f57a8cdd 100644 --- a/src/lib/config.js +++ b/src/lib/config.js @@ -14,10 +14,10 @@ const setupAsync = require('ajv-async'); const EventEmitter = require('events'); const TeemDevice = require('@f5devcentral/f5-teem').Device; -const logger = require('./logger.js'); -const util = require('./util.js'); -const deviceUtil = require('./deviceUtil.js'); -const persistentStorage = require('./persistentStorage.js').persistentStorage; +const logger = require('./logger'); +const util = require('./util'); +const deviceUtil = require('./deviceUtil'); +const persistentStorage = require('./persistentStorage').persistentStorage; const baseSchema = require('../schema/latest/base_schema.json'); const controlsSchema = require('../schema/latest/controls_schema.json'); @@ -27,11 +27,12 @@ const systemPollerSchema = require('../schema/latest/system_poller_schema.json') const listenerSchema = require('../schema/latest/listener_schema.json'); const consumerSchema = require('../schema/latest/consumer_schema.json'); const iHealthPollerSchema = require('../schema/latest/ihealth_poller_schema.json'); +const endpointsSchema = require('../schema/latest/endpoints_schema.json'); -const customKeywords = require('./customKeywords.js'); -const CONTROLS_CLASS_NAME = require('./constants.js').CONTROLS_CLASS_NAME; -const CONTROLS_PROPERTY_NAME = require('./constants.js').CONTROLS_PROPERTY_NAME; -const VERSION = require('./constants.js').VERSION; +const customKeywords = require('./customKeywords'); +const CONTROLS_CLASS_NAME = require('./constants').CONFIG_CLASSES.CONTROLS_CLASS_NAME; +const CONTROLS_PROPERTY_NAME = require('./constants').CONTROLS_PROPERTY_NAME; +const VERSION = require('./constants').VERSION; const PERSISTENT_STORAGE_KEY = 'config'; const BASE_CONFIG = { @@ -52,7 +53,7 @@ function ConfigWorker() { name: 'Telemetry Streaming', version: VERSION }; - this.teemDevice = new TeemDevice(assetInfo, 'staging'); + this.teemDevice = new TeemDevice(assetInfo); } nodeUtil.inherits(ConfigWorker, EventEmitter); @@ -118,6 +119,7 @@ ConfigWorker.prototype.loadConfig = function () { * @returns {Object} Promise which is resolved once state is saved */ ConfigWorker.prototype.saveConfig = function (config) { + // persistentStorage.set will make copy of data return persistentStorage.set(PERSISTENT_STORAGE_KEY, config) .then(() => logger.debug('Application config saved')) .catch((err) => { @@ -133,13 +135,14 @@ ConfigWorker.prototype.saveConfig = function (config) { * @returns {Promise} Promise resolved with config */ ConfigWorker.prototype.getConfig = function () { + // persistentStorage.get returns data copy return persistentStorage.get(PERSISTENT_STORAGE_KEY) .then((data) => { if (typeof data === 'undefined') { logger.debug(`persistentStorage did not have a value for ${PERSISTENT_STORAGE_KEY}`); } return (typeof data === 'undefined' - || typeof data.parsed === 'undefined') ? BASE_CONFIG : data; + || typeof data.parsed === 'undefined') ? util.deepCopy(BASE_CONFIG) : data; }); }; @@ -158,7 +161,8 @@ ConfigWorker.prototype.compileSchema = function () { systemPoller: systemPollerSchema, listener: listenerSchema, consumer: consumerSchema, - iHealthPoller: iHealthPollerSchema + iHealthPoller: iHealthPollerSchema, + endpoints: endpointsSchema }; const keywords = customKeywords; diff --git a/src/lib/constants.js b/src/lib/constants.js index cb73a566..6d841c83 100644 --- a/src/lib/constants.js +++ b/src/lib/constants.js @@ -8,7 +8,27 @@ 'use strict'; -const VERSION = '1.9.0'; +const packageVersionInfo = (function () { + let packageVersion = '0.0.0-0'; + ['../package.json', '../../package.json'].some((fname) => { + try { + packageVersion = require(fname).version; // eslint-disable-line global-require,import/no-dynamic-require + delete require.cache[require.resolve(fname)]; + } catch (err) { + return false; + } + return true; + }); + packageVersion = packageVersion.split('-'); + if (packageVersion.length === 1) { + // push RELEASE number + packageVersion.push('1'); + } + return packageVersion; +}()); + +const VERSION = packageVersionInfo[0]; +const RELEASE = packageVersionInfo[1]; /** @@ -47,13 +67,19 @@ WEEKDAY_TO_DAY_NAME[7] = 'sunday'; module.exports = { + RELEASE, VERSION, - BIG_IP_DEVICE_TYPE: 'BIG-IP', - CONSUMERS_CLASS_NAME: 'Telemetry_Consumer', + CONFIG_CLASSES: { + CONSUMER_CLASS_NAME: 'Telemetry_Consumer', + CONTROLS_CLASS_NAME: 'Controls', + ENDPOINTS_CLASS_NAME: 'Telemetry_Endpoints', + EVENT_LISTENER_CLASS_NAME: 'Telemetry_Listener', + IHEALTH_POLLER_CLASS_NAME: 'Telemetry_iHealth_Poller', + SYSTEM_CLASS_NAME: 'Telemetry_System', + SYSTEM_POLLER_CLASS_NAME: 'Telemetry_System_Poller' + }, CONSUMERS_DIR: './consumers', - CONTAINER_DEVICE_TYPE: 'Container', - CONTROLS_CLASS_NAME: 'Controls', CONTROLS_PROPERTY_NAME: 'controls', DAY_NAME_TO_WEEKDAY, DEVICE_DEFAULT_PORT: 8100, @@ -64,8 +90,11 @@ module.exports = { DEVICE_REST_MAMD_DIR: '/var/config/rest/madm', DEVICE_REST_MADM_URI: '/mgmt/shared/file-transfer/madm/', DEVICE_TMP_DIR: '/shared/tmp', + DEVICE_TYPE: { + BIG_IP: 'BIG-IP', + CONTAINER: 'Container' + }, DEFAULT_EVENT_LISTENER_PORT: 6514, - EVENT_LISTENER_CLASS_NAME: 'Telemetry_Listener', EVENT_TYPES: { DEFAULT: 'event', AVR_EVENT: 'AVR', @@ -79,20 +108,19 @@ module.exports = { SYSTEM_POLLER: 'systemInfo', IHEALTH_POLLER: 'ihealthInfo' }, + HTTP_REQUEST: { + DEFAULT_PORT: 80, + DEFAULT_PROTOCOL: 'http' + }, IHEALTH_API_LOGIN: 'https://api.f5.com/auth/pub/sso/login/ihealth-api', IHEALTH_API_UPLOAD: 'https://ihealth-api.f5.com/qkview-analyzer/api/qkviews', - IHEALTH_POLLER_CLASS_NAME: 'Telemetry_iHealth_Poller', LOCAL_HOST: 'localhost', PASSPHRASE_CIPHER_TEXT: 'cipherText', PASSPHRASE_ENVIRONMENT_VAR: 'environmentVar', PORT_TO_PROTO, PROTO_TO_PORT, - QKVIEW_CMD_LOCAL_TIMEOUT: 1 * 60 * 60 * 1000, // 1 hour in miliseconds - REQUEST_DEFAULT_PORT: 80, - REQUEST_DEFAULT_PROTOCOL: 'http', + QKVIEW_CMD_LOCAL_TIMEOUT: 1 * 60 * 60 * 1000, // 1 hour in milliseconds STATS_KEY_SEP: '::', - SYSTEM_CLASS_NAME: 'Telemetry_System', - SYSTEM_POLLER_CLASS_NAME: 'Telemetry_System_Poller', STRICT_TLS_REQUIRED: true, TRACER_DIR: '/var/tmp/telemetry', USER_AGENT: `f5-telemetry/${VERSION}`, diff --git a/src/lib/consumers.js b/src/lib/consumers.js index 25b8376a..7c70db7f 100644 --- a/src/lib/consumers.js +++ b/src/lib/consumers.js @@ -9,15 +9,15 @@ 'use strict'; const path = require('path'); -const logger = require('./logger.js'); // eslint-disable-line no-unused-vars -const deepCopy = require('./util.js').deepCopy; -const tracers = require('./util.js').tracer; -const constants = require('./constants.js'); -const configWorker = require('./config.js'); -const DataFilter = require('./dataFilter.js').DataFilter; +const logger = require('./logger'); // eslint-disable-line no-unused-vars +const deepCopy = require('./util').deepCopy; +const tracers = require('./util').tracer; +const constants = require('./constants'); +const configWorker = require('./config'); +const DataFilter = require('./dataFilter').DataFilter; const CONSUMERS_DIR = constants.CONSUMERS_DIR; -const CLASS_NAME = constants.CONSUMERS_CLASS_NAME; +const CLASS_NAME = constants.CONFIG_CLASSES.CONSUMER_CLASS_NAME; let CONSUMERS = null; /** @@ -168,8 +168,8 @@ configWorker.on('change', (config) => { }) .then(() => { unloadUnusedModules(typesBefore); - tracers.remove(null, tracer => tracer.name.startsWith(CLASS_NAME) - && tracer.lastGetTouch < tracersTimestamp); + tracers.remove(tracer => tracer.name.startsWith(CLASS_NAME) + && tracer.lastGetTouch < tracersTimestamp); }); }); diff --git a/src/lib/consumers/Azure_Log_Analytics/index.js b/src/lib/consumers/Azure_Log_Analytics/index.js index 0ffe6cac..b43175e5 100644 --- a/src/lib/consumers/Azure_Log_Analytics/index.js +++ b/src/lib/consumers/Azure_Log_Analytics/index.js @@ -10,7 +10,7 @@ const request = require('request'); const crypto = require('crypto'); -const EVENT_TYPES = require('../../constants.js').EVENT_TYPES; +const EVENT_TYPES = require('../../constants').EVENT_TYPES; function makeRequest(requestOptions) { return new Promise((resolve, reject) => { @@ -84,6 +84,8 @@ module.exports = function (context) { // deep copy and parse body, otherwise it will be stringified again const requestOptionsCopy = JSON.parse(JSON.stringify(requestOptions)); requestOptionsCopy.body = JSON.parse(requestOptionsCopy.body); + // redact secrets in Authorization header + requestOptionsCopy.headers.Authorization = '*****'; tracerMsg.push(requestOptionsCopy); } diff --git a/src/lib/consumers/ElasticSearch/index.js b/src/lib/consumers/ElasticSearch/index.js index 06918f33..554f39a6 100644 --- a/src/lib/consumers/ElasticSearch/index.js +++ b/src/lib/consumers/ElasticSearch/index.js @@ -8,9 +8,9 @@ 'use strict'; -const ESClient = require('elasticsearch').Client; -const util = require('../../util.js'); -const EVENT_TYPES = require('../../constants.js').EVENT_TYPES; +const elasticsearch = require('elasticsearch'); +const util = require('../../util'); +const EVENT_TYPES = require('../../constants').EVENT_TYPES; function elasticLogger(logger, tracer) { @@ -50,7 +50,7 @@ module.exports = function (context) { path: config.path }, ssl: { - rejectUnauthorized: config.allowSelfSignedCert + rejectUnauthorized: !config.allowSelfSignedCert } }; if (config.username) { @@ -80,7 +80,7 @@ module.exports = function (context) { if (context.tracer) { context.tracer.write(JSON.stringify(payload, null, 4)); } - const client = new ESClient(clientConfig); + const client = new elasticsearch.Client(clientConfig); client.index(payload) .then(() => { context.logger.debug('success'); diff --git a/src/lib/consumers/Generic_HTTP/index.js b/src/lib/consumers/Generic_HTTP/index.js index 368e0426..b64e0233 100644 --- a/src/lib/consumers/Generic_HTTP/index.js +++ b/src/lib/consumers/Generic_HTTP/index.js @@ -43,8 +43,15 @@ module.exports = function (context) { strictSSL: !context.config.allowSelfSignedCert }; if (context.tracer) { + let tracedHeaders = httpHeaders; + // redact Basic Auth passphrase, if provided + if (Object.keys(httpHeaders).indexOf('Authorization') > -1) { + tracedHeaders = JSON.parse(JSON.stringify(httpHeaders)); + tracedHeaders.Authorization = '*****'; + } + context.tracer.write(JSON.stringify({ - method, url, headers: httpHeaders, body: JSON.parse(httpBody) + method, url, headers: tracedHeaders, body: JSON.parse(httpBody) }, null, 4)); } diff --git a/src/lib/consumers/README.md b/src/lib/consumers/README.md index d551aa4a..54ab0130 100644 --- a/src/lib/consumers/README.md +++ b/src/lib/consumers/README.md @@ -42,7 +42,7 @@ This describes the structure of the context object. Creating and testing new consumers within TS itself by posting expected declaration and watching logs, etc. is an entirely valid way to add a new consumer. However getting the index.js file right initially might require some iteration, and ideally this can be locally. Below is an example script to call the consumer with a mock event. ```javascript -const index = require('./index.js'); +const index = require('./index'); const mockLogger = { debug: msg => console.log(msg), diff --git a/src/lib/consumers/Splunk/dataMapping.js b/src/lib/consumers/Splunk/dataMapping.js index f6bb76a6..715d6c71 100644 --- a/src/lib/consumers/Splunk/dataMapping.js +++ b/src/lib/consumers/Splunk/dataMapping.js @@ -8,6 +8,11 @@ 'use strict'; +const TMSTATS_PERIOD_PREFIX = RegExp(/\./g); +const IPV4_REGEXP = /FFFF([A-Fa-f0-9].)([A-Fa-f0-9].)([A-Fa-f0-9].)([A-Fa-f0-9].)/; +const IPV6_REGEXP = /([A-Fa-f0-9]{4})([A-Fa-f0-9]{4})([A-Fa-f0-9]{4})([A-Fa-f0-9]{4})([A-Fa-f0-9]{4})([A-Fa-f0-9]{4})([A-Fa-f0-9]{4})([A-Fa-f0-9]{4})/; +const IPV6_V4_PREFIX_REGEXP = RegExp(/::ffff:/ig); + // Canonical format function defaultFormat(globalCtx) { const data = globalCtx.event.data; @@ -48,6 +53,8 @@ const SOURCE_2_TYPES = { 'bigip.objectmodel.cert': 'f5:bigip:config:iapp:json', 'bigip.objectmodel.profile': 'f5:bigip:config:iapp:json', 'bigip.objectmodel.virtual': 'f5:bigip:config:iapp:json', + 'bigip.objectmodel.virtual.pools': 'f5:bigip:config:iapp:json', + 'bigip.objectmodel.virtual.profiles': 'f5:bigip:config:iapp:json', 'bigip.ihealth.diagnostics': 'f5:bigip:ihealth:iapp:json', 'bigip.tmstats': 'f5:bigip:stats:iapp.json' }; @@ -134,6 +141,19 @@ function getData(request, key) { return data; } +function formatHexIP(originData) { + const data = originData.replace(/:/g, '').substring(0, 32); + const matchIpV4 = data.match(IPV4_REGEXP); + if (matchIpV4) { + return `${parseInt(matchIpV4[1], 16)}.${parseInt(matchIpV4[2], 16)}.${parseInt(matchIpV4[3], 16)}.${parseInt(matchIpV4[4], 16)}`; + } + const matchIpV6 = data.match(IPV6_REGEXP); + if (matchIpV6) { + return `${matchIpV6[1]}:${matchIpV6[2]}:${matchIpV6[3]}:${matchIpV6[4]}:${matchIpV6[5]}:${matchIpV6[6]}:${matchIpV6[7]}:${matchIpV6[8]}`; + } + return originData; +} + function overall(request) { const data = request.globalCtx.event.data; const template = getTemplate('bigip.stats.summary', data, request.cache); @@ -306,6 +326,57 @@ const stats = [ }); }, + function (request) { + const vsStats = getData(request, 'virtualServers'); + if (vsStats === undefined) return undefined; + + const template = getTemplate('bigip.objectmodel.virtual.profiles', request.globalCtx.event.data, request.cache); + const ret = []; + Object.keys(vsStats).forEach((vsKey) => { + const vsStat = vsStats[vsKey]; + if (!vsStat.profiles) { + return; + } + const profiles = vsStat.profiles; + Object.keys(profiles).forEach((profKey) => { + const newData = Object.assign({}, template); + newData.event = Object.assign({}, template.event); + newData.event.virtual_name = vsKey; + newData.event.tenant = vsStat.tenant; + newData.event.app = vsStat.application; + newData.event.appComponent = ''; + newData.event.profile_name = profKey; + newData.event.profile_type = 'profile'; + ret.push(newData); + }); + }); + return ret; + }, + + function (request) { + const vsStats = getData(request, 'virtualServers'); + if (vsStats === undefined) return undefined; + + const template = getTemplate('bigip.objectmodel.virtual.pools', request.globalCtx.event.data, request.cache); + const ret = []; + Object.keys(vsStats).forEach((key) => { + const vsStat = vsStats[key]; + if (!vsStat.pool) { + return; + } + + const newData = Object.assign({}, template); + newData.event = Object.assign({}, template.event); + newData.event.virtual_name = key; + newData.event.app = vsStat.application; + newData.event.appComponent = ''; + newData.event.tenant = vsStat.tenant; + newData.event.pool_name = vsStat.pool; + ret.push(newData); + }); + return ret; + }, + function (request) { const poolStats = getData(request, 'pools'); if (poolStats === undefined) return undefined; @@ -338,23 +409,52 @@ const stats = [ const tmstats = getData(request, 'tmstats'); if (tmstats === undefined) return undefined; + const hexIpProps = ['addr', 'source', 'destination']; const template = getTemplate('bigip.tmstats', request.globalCtx.event.data, request.cache); const output = []; - const periodRegex = RegExp(/\./g); Object.keys(tmstats).forEach((key) => { + if (key === 'virtualServerCpuStat') { + const tmstData = tmstats[key]; + // 1) table 'virtualServerStat' should exist + // 2) last_cycle_count was removed starting from 13.1+ + if (tmstats.virtualServerStat && tmstData && tmstData.length + && typeof tmstData[0].last_cycle_count === 'undefined') { + const vsCycleCount = {}; + tmstats.virtualServerStat.forEach((entry) => { + vsCycleCount[entry.name] = entry.cycle_count; + }); + tmstData.forEach((entry) => { + entry.last_cycle_count = vsCycleCount[entry.name]; + }); + } + } + tmstats[key].forEach((entry) => { const newData = Object.assign({}, template); // replace periods in tmstat key names with underscores Object.keys(entry).forEach((entryKey) => { - if (periodRegex.test(entryKey)) { - entry[entryKey.replace(periodRegex, '_')] = entry[entryKey]; + if (TMSTATS_PERIOD_PREFIX.test(entryKey)) { + entry[entryKey.replace(TMSTATS_PERIOD_PREFIX, '_')] = entry[entryKey]; delete entry[entryKey]; } }); newData.source += `.${STAT_2_TMCTL_TABLE[key]}`; newData.event = Object.assign({}, template.event); newData.event = Object.assign(newData.event, entry); + newData.event.app = newData.event.application; + // newData.event.tenant = newData.event.tenant; // just as reminder that tenant is already there + newData.event.appComponent = ''; + + if (key === 'monitorInstanceStat' && newData.event.ip_address) { + newData.event.ip_address = newData.event.ip_address.replace(IPV6_V4_PREFIX_REGEXP, ''); + } + hexIpProps.forEach((hexProp) => { + if (newData.event[hexProp]) { + newData.event[hexProp] = formatHexIP(newData.event[hexProp]); + } + }); + output.push(newData); }); }); diff --git a/src/lib/consumers/Splunk/index.js b/src/lib/consumers/Splunk/index.js index 38af1157..de8b1a9b 100644 --- a/src/lib/consumers/Splunk/index.js +++ b/src/lib/consumers/Splunk/index.js @@ -11,8 +11,8 @@ const request = require('request'); const zlib = require('zlib'); -const dataMapping = require('./dataMapping.js'); -const EVENT_TYPES = require('../../constants.js').EVENT_TYPES; +const dataMapping = require('./dataMapping'); +const EVENT_TYPES = require('../../constants').EVENT_TYPES; const GZIP_DATA = true; const MAX_CHUNK_SIZE = 99000; @@ -230,10 +230,18 @@ function forwardData(dataToSend, globalCtx) { context.request = request.defaults(context.requestOpts); if (globalCtx.tracer) { + // redact passphrase in consumer config + const tracedConsumerCtx = JSON.parse(JSON.stringify(context.consumer)); + tracedConsumerCtx.passphrase = '*****'; + + // redact passphrase in request options + const traceRequestOpts = JSON.parse(JSON.stringify(context.requestOpts)); + traceRequestOpts.headers.Authorization = '*****'; + globalCtx.tracer.write(JSON.stringify({ dataToSend, - consumer: context.consumer, - requestOpts: context.requestOpts + consumer: tracedConsumerCtx, + requestOpts: traceRequestOpts }, null, 2)); } diff --git a/src/lib/consumers/Statsd/index.js b/src/lib/consumers/Statsd/index.js index 92838051..49187238 100644 --- a/src/lib/consumers/Statsd/index.js +++ b/src/lib/consumers/Statsd/index.js @@ -15,7 +15,7 @@ // udp/tcp it might be simpler to just implement net module directly for this use case const StatsD = require('node-statsd'); const deepDiff = require('deep-diff'); -const EVENT_TYPES = require('../../constants.js').EVENT_TYPES; +const EVENT_TYPES = require('../../constants').EVENT_TYPES; const stripMetrics = (data) => { Object.keys(data).forEach((item) => { diff --git a/src/lib/consumers/Sumo_Logic/index.js b/src/lib/consumers/Sumo_Logic/index.js index 8ebf1fa5..6bb709b8 100644 --- a/src/lib/consumers/Sumo_Logic/index.js +++ b/src/lib/consumers/Sumo_Logic/index.js @@ -32,7 +32,14 @@ module.exports = function (context) { strictSSL: !config.allowSelfSignedCert }; if (context.tracer) { - context.tracer.write(JSON.stringify({ url, headers: httpHeaders, body: JSON.parse(httpBody) }, null, 4)); + // redact secret from url + const tracedUrl = (secret === '' ? url : url + .split('/') + .slice(0, -1) + .join('/') + .concat('/*****')); + const traceData = { url: tracedUrl, headers: httpHeaders, body: JSON.parse(httpBody) }; + context.tracer.write(JSON.stringify(traceData, null, 4)); } // eslint-disable-next-line no-unused-vars diff --git a/src/lib/customKeywords.js b/src/lib/customKeywords.js index 462375a5..a60741a6 100644 --- a/src/lib/customKeywords.js +++ b/src/lib/customKeywords.js @@ -10,13 +10,14 @@ const Ajv = require('ajv'); const fs = require('fs'); -const constants = require('./constants.js'); -const util = require('./util.js'); -const deviceUtil = require('./deviceUtil.js'); +const constants = require('./constants'); +const util = require('./util'); +const deviceUtil = require('./deviceUtil'); const textNamedKey = 'plainText'; const base64NamedKey = 'plainBase64'; const secureVaultNamedKey = 'SecureVault'; +const secureVaultCipherPrefix = '$M$'; /** @@ -176,6 +177,61 @@ function expandPointers(str, origin, srcPointer) { return ret; } +/** + * Validate path + * + * @param {Object} origin - origin object + * @param {String} srcPath - path to follow + * @param {Object} options - options + * @param {String} options.path - base path that starts with class name + * @param {Integer} options.partsNum - number of parts the value should consist of. 0 - no limits + */ +function validateDeclarationPath(origin, srcPath, options) { + // Given sample obj + // { + // class: "The_Class", + // collProp: { { key1: val1 }, { key2: val2 } } + // } + + // remove leading and trailing '/' + const trimPath = val => val.substring( + val.startsWith('/') ? 1 : 0, + val.endsWith('/') ? (val.length - 1) : val.length + ); + + // the path defined by the user in their declaration, e.g. The_Class/key_1/.../key_n + const dataParts = trimPath(srcPath).split('/'); + if (options.partsNum && dataParts.length !== options.partsNum) { + let exampleFormat = 'ObjectName'; + if (options.partsNum) { + for (let i = 1; i < options.partsNum; i += 1) { + exampleFormat = `${exampleFormat}/key${i}`; + } + } else { + exampleFormat = `${exampleFormat}/key1/.../keyN`; + } + throw new Error(`"${srcPath}" does not follow format "${exampleFormat}"`); + } + + // the path defined in the schema {class}/{propLevel_1}/../{propLevel_n}, e.g. The_Class/collProp + const schemaParts = trimPath(options.path).split('/'); + const className = schemaParts[0]; + const classInstanceName = dataParts[0]; + let objInstance = origin[classInstanceName]; + + if (typeof objInstance !== 'object' || objInstance.class !== className) { + throw new Error(`"${classInstanceName}" must be of object type and class "${className}"`); + } + + const pathParts = schemaParts.slice(1).concat(dataParts.slice(1)); + /* eslint-disable no-return-assign */ + if (!pathParts.every(key => typeof (objInstance = objInstance[key]) !== 'undefined')) { + const resolvedPath = `${classInstanceName}/${pathParts.join('/')}`; + throw new Error(`Unable to find "${resolvedPath}"`); + } +} + + const keywords = { f5secret: { type: 'object', @@ -189,57 +245,47 @@ const keywords = { compile(schema, parentSchema) { // eslint-disable-next-line no-unused-vars return function (data, dataPath, parentData, propertyName, rootData) { - const ajvErrors = []; - - // we handle a number of passphrase object in this function, the following describes each of them - // 'cipherText': this means we plan to store a plain text secret locally, which requires we encrypt - // it first. This also assumes that we are running on a BIG-IP where we have the means to do so - // 'environmentVar': undefined - - // handle 'environmentVar' passphrase object - if (data[constants.PASSPHRASE_ENVIRONMENT_VAR] !== undefined) { - return Promise.resolve(true); - } - // handle 'cipherText' passphrase object - if (data[constants.PASSPHRASE_CIPHER_TEXT] !== undefined) { - // if data is already encrypted just return - if (data.protected === secureVaultNamedKey) { - return Promise.resolve(true); - } - - // base64 decode before encrypting - if needed - if (data.protected === base64NamedKey) { - data[constants.PASSPHRASE_CIPHER_TEXT] = util.base64('decode', data[constants.PASSPHRASE_CIPHER_TEXT]); - data.protected = textNamedKey; - } - - return deviceUtil.getDeviceType() - .then((deviceType) => { - // check if on a BIG-IP and fail validation if not - if (deviceType !== constants.BIG_IP_DEVICE_TYPE) { - throw new Error(`Specifying '${constants.PASSPHRASE_CIPHER_TEXT}' requires running on ${constants.BIG_IP_DEVICE_TYPE}`); + return Promise.resolve() + .then(() => { + /** + * we handle a number of passphrase object in this function, + * the following describes each of them: + * - 'cipherText': this means we plan to store a plain text secret locally, + * which requires we encrypt it first. This also assumes that we are + * running on a BIG-IP where we have the means to do so. + * - 'environmentVar': undefined + */ + if (typeof data[constants.PASSPHRASE_ENVIRONMENT_VAR] !== 'undefined') { + return Promise.resolve(true); + } + if (typeof data[constants.PASSPHRASE_CIPHER_TEXT] === 'undefined') { + return Promise.reject(new Error(`missing ${constants.PASSPHRASE_CIPHER_TEXT} or ${constants.PASSPHRASE_ENVIRONMENT_VAR}`)); + } + if (data.protected === secureVaultNamedKey) { + if (data[constants.PASSPHRASE_CIPHER_TEXT].startsWith(secureVaultCipherPrefix)) { + return Promise.resolve(true); } - // encrypt secret - return deviceUtil.encryptSecret(data[constants.PASSPHRASE_CIPHER_TEXT]); - }) - .then((secret) => { - // update text field with secret - should we base64 encode? - data[constants.PASSPHRASE_CIPHER_TEXT] = secret; - // set protected key - in case we return validated schema to requestor - data.protected = secureVaultNamedKey; - - // notify success - return true; - }) - .catch((e) => { - ajvErrors.push({ keyword: 'f5secret', message: e.message, params: {} }); - throw new Ajv.ValidationError(ajvErrors); - }); - } + return Promise.reject(new Error(`'${constants.PASSPHRASE_CIPHER_TEXT}' should be encrypted by ${constants.DEVICE_TYPE.BIG_IP} when 'protected' is '${secureVaultNamedKey}'`)); + } + if (data.protected === base64NamedKey) { + data[constants.PASSPHRASE_CIPHER_TEXT] = util.base64('decode', data[constants.PASSPHRASE_CIPHER_TEXT]); + data.protected = textNamedKey; + } - // if we make it here we should reject with a useful message - const message = `missing ${constants.PASSPHRASE_CIPHER_TEXT} or ${constants.PASSPHRASE_ENVIRONMENT_VAR}`; - return Promise.reject(new Ajv.ValidationError([{ keyword: 'f5secret', message, params: {} }])); + return deviceUtil.getDeviceType() + .then((deviceType) => { + if (deviceType !== constants.DEVICE_TYPE.BIG_IP) { + return Promise.reject(new Error(`Specifying '${constants.PASSPHRASE_CIPHER_TEXT}' requires running on ${constants.DEVICE_TYPE.BIG_IP}`)); + } + return deviceUtil.encryptSecret(data[constants.PASSPHRASE_CIPHER_TEXT]); + }) + .then((secret) => { + data[constants.PASSPHRASE_CIPHER_TEXT] = secret; + data.protected = secureVaultNamedKey; + return true; + }); + }) + .catch(e => Promise.reject(new Ajv.ValidationError([{ keyword: 'f5secret', message: e.message || e.toString(), params: {} }]))); }; } }, @@ -329,20 +375,57 @@ const keywords = { compile(schema, parentSchema) { // eslint-disable-next-line no-unused-vars return function (data, dataPath, parentData, propertyName, rootData) { - const ajvErrors = []; - // string passed if (typeof data === 'string') { const declarationClass = schema; - if (!(rootData[data] && rootData[data].class === declarationClass)) { - ajvErrors.push({ - keyword: 'ihealth', + const objectInstance = rootData[data]; + if (typeof objectInstance !== 'object' || objectInstance.class !== declarationClass) { + return Promise.reject(new Ajv.ValidationError([{ + keyword: 'declarationClass', message: `declaration with name "${data}" and class "${declarationClass}" doesn't exist`, params: {} - }); + }])); } } - if (ajvErrors.length) { - return Promise.reject(new Ajv.ValidationError(ajvErrors)); + return Promise.resolve(true); + }; + } + }, + declarationClassProp: { + type: 'string', + errors: true, + modifying: true, + async: true, + metaSchema: { + type: 'object', + description: 'Automatically resolve a path with given {declarationClass}/{propLevel_1}/...{propLevel_n}', + properties: { + partsNum: { + description: 'Expected number of parts the value should consist of. 0 - no limits', + type: 'integer', + minimum: 0, + maximum: 100, + default: 0 + }, + path: { + description: '{declarationClass}/{propLevel_1}/...{propLevel_n}', + type: 'string', + minLength: 1 + } + } + }, + // eslint-disable-next-line no-unused-vars + compile(schema, parentSchema) { + return function (data, dataPath, parentData, propertyName, rootData) { + if (typeof data === 'string') { + try { + validateDeclarationPath(rootData, data, schema); + } catch (err) { + return Promise.reject(new Ajv.ValidationError([{ + keyword: 'declarationClassProp', + message: `${err}`, + params: {} + }])); + } } return Promise.resolve(true); }; @@ -360,7 +443,7 @@ const keywords = { // eslint-disable-next-line no-unused-vars validate(schema, data, parentSchema, dataPath, parentData, propertyName, rootData) { // looks like instance is configured as default - if (data) { + if (typeof data === 'string') { return new Promise((resolve, reject) => { fs.access(data, (fs.constants || fs).R_OK, (accessErr) => { if (accessErr) { diff --git a/src/lib/dataFilter.js b/src/lib/dataFilter.js index 75fba210..40378228 100644 --- a/src/lib/dataFilter.js +++ b/src/lib/dataFilter.js @@ -8,8 +8,8 @@ 'use strict'; -const util = require('./util.js'); -const dataUtil = require('./dataUtil.js'); +const util = require('./util'); +const dataUtil = require('./dataUtil'); /** * Data Filter Class @@ -91,13 +91,14 @@ DataFilter.prototype._applyBlacklist = function (data) { * @param {Object} [actionCtx.excludeData] - 'Exclude' filter definition * @param {Object} [actionCtx.locations] - The locations of data to be filtered * @param {Object} [actionCtx.ifAllMatch] - conditions to check before + * @param {Object} [actionCtx.ifAnyMatch] - conditions to check before * * @returns {void} */ function handleAction(dataCtx, actionCtx) { if ((actionCtx.includeData || actionCtx.excludeData) - && (util.isObjectEmpty(actionCtx.ifAllMatch) - || dataUtil.checkConditions(dataCtx.data, actionCtx.ifAllMatch))) { + && !util.isObjectEmpty(dataCtx.data) // completely short-circuit if dataCtx.data is empty + && dataUtil.checkConditions(dataCtx, actionCtx)) { if (actionCtx.includeData) { dataUtil.preserveStrictMatches(dataCtx.data, actionCtx.locations, true); } else if (actionCtx.excludeData) { diff --git a/src/lib/dataPipeline.js b/src/lib/dataPipeline.js index 94f936f4..58211f2f 100644 --- a/src/lib/dataPipeline.js +++ b/src/lib/dataPipeline.js @@ -8,12 +8,12 @@ 'use strict'; -const logger = require('./logger.js'); -const forwarder = require('./forwarder.js'); -const dataTagging = require('./dataTagging.js'); -const dataFilter = require('./dataFilter.js'); -const util = require('./util.js'); -const EVENT_TYPES = require('./constants.js').EVENT_TYPES; +const logger = require('./logger'); +const forwarder = require('./forwarder'); +const dataTagging = require('./dataTagging'); +const dataFilter = require('./dataFilter'); +const util = require('./util'); +const EVENT_TYPES = require('./constants').EVENT_TYPES; /** diff --git a/src/lib/dataTagging.js b/src/lib/dataTagging.js index 2182d131..a1230f89 100644 --- a/src/lib/dataTagging.js +++ b/src/lib/dataTagging.js @@ -1,5 +1,5 @@ /* - * Copyright 2018. F5 Networks, Inc. See End User License Agreement ("EULA") for + * Copyright 2020. F5 Networks, Inc. See End User License Agreement ("EULA") for * license terms. Notwithstanding anything to the contrary in the EULA, Licensee * may copy and modify this software product for its internal business purposes. * Further, Licensee may upload, publish and distribute the modified version of @@ -10,10 +10,10 @@ const properties = require('./properties.json'); const normalizeUtil = require('./normalizeUtil'); -const dataUtil = require('./dataUtil.js'); -const util = require('./util.js'); +const dataUtil = require('./dataUtil'); +const util = require('./util'); const systemStatsUtil = require('./systemStatsUtil'); -const EVENT_TYPES = require('./constants.js').EVENT_TYPES; +const EVENT_TYPES = require('./constants').EVENT_TYPES; /** * Handle tagging actions on the data. @@ -27,16 +27,17 @@ const EVENT_TYPES = require('./constants.js').EVENT_TYPES; * @param {Object} dataCtx.data - data to process * @param {String} dataCtx.type - type of data to process * @param {Object} actionCtx - 'setTag' action to perfrom on the data + * @param {Object} deviceCtx - device context * @param {Object} [actionCtx.setTag] - tag(s) that will be applied * @param {Object} [actionCtx.locations] - where thae tags should be applied * @param {Object} [actionCtx.ifAllMatch] - conditions to check before + * @param {Object} [actionCtx.ifAnyMatch] - conditions to check before * * @returns {void} */ function handleAction(dataCtx, actionCtx, deviceCtx) { if (!util.isObjectEmpty(actionCtx.setTag) - && (util.isObjectEmpty(actionCtx.ifAllMatch) - || dataUtil.checkConditions(dataCtx.data, actionCtx.ifAllMatch))) { + && dataUtil.checkConditions(dataCtx, actionCtx)) { addTags(dataCtx, actionCtx, deviceCtx); } } @@ -67,22 +68,28 @@ function addTags(dataCtx, actionCtx, deviceCtx) { // properties.json - like old-style tagging if (dataCtx.type === EVENT_TYPES.SYSTEM_POLLER) { // Apply tags to default locations (where addKeysByTag is true) for system info - Object.keys(properties.stats).forEach((statKey) => { - const items = data[statKey]; - const statProp = systemStatsUtil.renderProperty(deviceCtx, properties.stats[statKey]); - // tags can be applied to objects only - usually it is collections of objects - // e.g. Virtual Servers, pools, profiles and etc. - if (typeof items === 'object' - && !util.isObjectEmpty(items) - && statProp.normalization - && statProp.normalization.find(norm => norm.addKeysByTag)) { - Object.keys(items).forEach((itemKey) => { - Object.keys(tags).forEach((tagKey) => { - addTag(items[itemKey], tagKey, tags[tagKey], itemKey, statProp); + if (!dataCtx.isCustom) { + Object.keys(properties.stats).forEach((statKey) => { + const statProp = systemStatsUtil.renderProperty( + deviceCtx, util.deepCopy(properties.stats[statKey]) + ); + const items = statProp.structure && statProp.structure.parentKey + ? (data[statProp.structure.parentKey] || {})[statKey] : data[statKey]; + + // tags can be applied to objects only - usually it is collections of objects + // e.g. Virtual Servers, pools, profiles and etc. + if (typeof items === 'object' + && !util.isObjectEmpty(items) + && statProp.normalization + && statProp.normalization.find(norm => norm.addKeysByTag)) { + Object.keys(items).forEach((itemKey) => { + Object.keys(tags).forEach((tagKey) => { + addTag(items[itemKey], tagKey, tags[tagKey], itemKey, statProp); + }); }); - }); - } - }); + } + }); + } } else { // Apply tags to default locations of events (and not iHealth data) Object.keys(tags).forEach((tagKey) => { diff --git a/src/lib/dataUtil.js b/src/lib/dataUtil.js index 4106ae63..a8c4b1f1 100644 --- a/src/lib/dataUtil.js +++ b/src/lib/dataUtil.js @@ -8,39 +8,108 @@ 'use strict'; -const logger = require('./logger.js'); +const logger = require('./logger'); +const util = require('./util'); /** - * Checks the conditions against the data + * Checks the conditions against the data. * * @private - use for testing only * - * @param {Object} data - data to process - * @param {Object} conditions - conditions to check + * @param {Object} dataCtx - complete data context collected from BIG-IP + * @param {Object} actionCtx - individual action context * - * @returns {Boolean} true when all conditions are met + * @returns {Boolean} True if a condition is met, or if a matching function is not provided. */ -function checkConditions(data, conditions) { - // Array.prototype.every will stop on first 'false' - return Object.keys(conditions).every((condition) => { - const matches = getMatches(data, condition); - const conditionVal = conditions[condition]; +function checkConditions(dataCtx, actionCtx) { + let func; + let condition; + + if (!util.isObjectEmpty(actionCtx.ifAnyMatch)) { + func = checkAnyMatches; + condition = actionCtx.ifAnyMatch; + } + if (!util.isObjectEmpty(actionCtx.ifAllMatch)) { + func = checkAllMatches; + condition = actionCtx.ifAllMatch; + } + if (func) { + return util.isObjectEmpty(dataCtx.data) ? false : func(dataCtx.data, condition); + } + return true; +} + +function checkAnyMatches(data, matchObjects) { + // Use 'Array.prototype.some' to check whether atleast 1 condition is true + return matchObjects.some(conditions => checkAllMatches(data, conditions)); +} + +function checkAllMatches(data, conditions) { + // Use 'Array.prototype.every' to check whether every condition is true + return Object.keys(conditions).every((conditionKey) => { + const matches = getMatches(data, conditionKey); if (matches.length === 0) { - // No matches were found so the condition was not met - logger.debug(`No matches were found for ${condition}`); + logger.debug(`No matches were found for "${conditionKey}"`); return false; } - if (typeof conditionVal !== 'object') { - // The condition to check is not an object and matches have been found in the data - return matches.every(match => data[match] === conditionVal - || data[match].toString().match(conditionVal.toString())); + + const conditionVals = conditions[conditionKey]; + + if (typeof conditionVals !== 'object') { + return matches.every(match => checkScalarValue(data[match], conditionVals)); + } + + if (Array.isArray(conditionVals)) { + return matches.every((match) => { + const dataMatch = data[match]; + // If conditionVals is array, dataMatch must also be an array for matching to occur. + if (!Array.isArray(dataMatch) && dataMatch.length !== conditionVals.length) { + return false; + } + + // Arrays have no order guarantee; sort both, then compare each array item + dataMatch.sort(); + conditionVals.sort(); + return conditionVals.every((conditionVal, index) => checkScalarValue(dataMatch[index], conditionVal)); + }); } - // The next condition is an object so we do recursion and matches for the key have been found - return matches.every(match => checkConditions(data[match], conditionVal)); + + // the next condition is an object (and not array); do recursion and matches for the key have been found + return matches.every(match => checkAllMatches(data[match], conditionVals)); }); } +function checkScalarValue(data, condition) { + // Perform easiest strict equality (===) comparison (before type conversion or regex) + // Perform this check first, since we may have null === null, which should evaluate true + if (data === condition) { + return true; + } + // Function only performs matching on scalar values against scalar values + if (typeof data === 'object' || typeof condition === 'object') { + return false; + } + + // 'data' and 'condition' are simple scalars - but not strictly equal (different type, or requires regex) + try { + // Force each to be strings for later comparison + condition = condition.toString(); + data = data.toString(); + + // Perform another strict equality (which will be more performant than a regex match), + // with types converted to strings, before attempting regex match. + return data === condition || data.match(condition); + } catch (err) { + // Possible to have invalid regex - catch and log error. Return false. Matching unsuccessful. + if (err instanceof SyntaxError) { + logger.exception(`checkScalarValue error (data = "${data}" condition = "${condition}"): ${err.message || err}`, err); + return false; + } + throw err; + } +} + /** * Check to see if a property can be found inside the data. Checks by using property as a literal string and * then checks as a regular expression if no results are found from the literal string search. @@ -123,19 +192,23 @@ function getDeepMatches(data, matchObj) { * @returns {void} */ function searchAnyMatches(data, matchObj, cb) { - Object.keys(matchObj).forEach((matchKey) => { - getMatches(data, matchKey).forEach((key) => { - let item = data[key]; - const nextMatchObj = matchObj[matchKey]; - const nestedKey = cb(key, item); - if (nestedKey) { - item = item[nestedKey]; - } - if (typeof item === 'object' && typeof nextMatchObj === 'object') { - searchAnyMatches(item, nextMatchObj, cb); - } + if (Array.isArray(matchObj)) { + matchObj.forEach(matchItem => searchAnyMatches(data, matchItem, cb)); + } else { + Object.keys(matchObj).forEach((matchKey) => { + getMatches(data, matchKey).forEach((key) => { + let item = data[key]; + const nextMatchObj = matchObj[matchKey]; + const nestedKey = cb(key, item); + if (nestedKey) { + item = item[nestedKey]; + } + if (typeof item === 'object' && typeof nextMatchObj === 'object') { + searchAnyMatches(item, nextMatchObj, cb); + } + }); }); - }); + } } /** diff --git a/src/lib/datetimeUtil.js b/src/lib/datetimeUtil.js index 84e6ebdd..311b1a3d 100644 --- a/src/lib/datetimeUtil.js +++ b/src/lib/datetimeUtil.js @@ -8,8 +8,8 @@ 'use strict'; -const constants = require('./constants.js'); -const util = require('./util.js'); +const constants = require('./constants'); +const util = require('./util'); /** diff --git a/src/lib/deviceUtil.js b/src/lib/deviceUtil.js index 7d8a665a..a2995026 100644 --- a/src/lib/deviceUtil.js +++ b/src/lib/deviceUtil.js @@ -13,11 +13,26 @@ const fs = require('fs'); const crypto = require('crypto'); const diff = require('deep-diff'); -const constants = require('./constants.js'); -const logger = require('./logger.js'); -const util = require('./util.js'); +const constants = require('./constants'); +const logger = require('./logger'); +const util = require('./util'); +/** + * Cache for info about the device TS is running on + * + * @property {String} TYPE - device's type - BIG-IP or Container + * @property {Object} VERSION - version information + * @property {Boolean} RETRIEVE_SECRETS_FROM_TMSH - true when device is affected by bug and you should run + * TMSH command to retrieve secret (BZ745423) + */ +const HOST_DEVICE_CACHE = {}; +const HDC_KEYS = { + TYPE: 'TYPE', + VERSION: 'VERSION', + RETRIEVE_SECRETS_FROM_TMSH: 'RETRIEVE_SECRETS_FROM_TMSH' +}; + /** * F5 Device async CLI class definition starts here */ @@ -427,13 +442,14 @@ DeviceAsyncCLI.prototype._removeAsyncTaskFromDevice = function (taskID, errOk) { * Helper function for the encryptSecret function * * @private - * @param {Array} splitData - the secret that has been split up - * @param {Array} dataArray - the array that the encrypted data will be put - * @param {Integer} index - the endex value used to go through the split data + * @param {Array} splitData - the secret that has been split up + * @param {Array} dataArray - the array that the encrypted data will be put + * @param {Integer} index - the index value used to go through the split data + * @param {Boolean} secretsFromTMSH - fetch secrets from TMSH * * @returns {Promise} Promise resolved with the encrypted data */ -function encryptSecretHelper(splitData, dataArray, index) { +function encryptSecretHelper(splitData, dataArray, index, secretsFromTMSH) { let encryptedData = null; let error = null; // can't have a + or / in the radius object name, so replace those if they exist @@ -459,70 +475,139 @@ function encryptSecretHelper(splitData, dataArray, index) { } // update text field with Secure Vault cryptogram - should we base64 encode? encryptedData = res.secret; - return module.exports.getDeviceVersion(constants.LOCAL_HOST); - }) - .then((deviceVersion) => { - let promise; - if (util.compareVersionStrings(deviceVersion.version, '>=', '14.1') - && util.compareVersionStrings(deviceVersion.version, '<', '15.0')) { - // TMOS 14.1.x fix for 745423 - const tmshCmd = `tmsh -a list auth radius-server ${radiusObjectName} secret`; - promise = module.exports.executeShellCommandOnDevice(constants.LOCAL_HOST, tmshCmd) - .then((res) => { - /** - * auth radius-server telemetry_delete_me { - * secret - * } - */ - encryptedData = res.split('\n')[1].trim().split(' ', 2)[1]; - }); + + if (!secretsFromTMSH) { + return Promise.resolve(); } - return promise || Promise.resolve(); + + // TMOS 14.1.x fix for 745423 + const tmshCmd = `tmsh -a list auth radius-server ${radiusObjectName} secret`; + return module.exports.executeShellCommandOnDevice(constants.LOCAL_HOST, tmshCmd) + .then((tmosOutput) => { + /** + * auth radius-server telemetry_delete_me { + * secret + * } + */ + encryptedData = tmosOutput.split('\n')[1].trim().split(' ', 2)[1]; + }); }) .catch((e) => { error = e; }) .then(() => { + // remove TMOS object at first to keep BIG-IP clean and then throw error if needed const httpDeleteOptions = { method: 'DELETE', - continueOnErrorCode: true + continueOnErrorCode: true // ignore error to avoid UnhandledPromiseRejection error }; module.exports.makeDeviceRequest(constants.LOCAL_HOST, `${uri}/${radiusObjectName}`, httpDeleteOptions); if (error) { throw error; } - }) - .then(() => { if (encryptedData.indexOf(',') !== -1) { throw new Error('Encrypted data should not have a comma in it'); } dataArray.push(encryptedData); index += 1; if (index < splitData.length) { - return encryptSecretHelper(splitData, dataArray, index); + return encryptSecretHelper(splitData, dataArray, index, secretsFromTMSH); } return Promise.resolve(dataArray); }); } +/** + * Check if TMOS version affected by bug when secrets should be fetched from TMSH only (BZ745423) + * + * @param {Object} version - TMOS version info + * @param {String} version.version - TMOS version string + * + * @returns {Boolean} true if TMOS version affected by bug + */ +function isVersionAffectedBySecretsBug(version) { + return util.compareVersionStrings(version.version, '>=', '14.1') + && util.compareVersionStrings(version.version, '<', '15.0'); +} + + module.exports = { + /** + * Gather Host Device Info + * + * @returns {Promise} resolved once info about Host Device was gathered + */ + gatherHostDeviceInfo() { + return this.getDeviceType() + .then((deviceType) => { + this.setHostDeviceInfo(HDC_KEYS.TYPE, deviceType); + return this.getDeviceVersion(constants.LOCAL_HOST); + }) + .then((deviceVersion) => { + this.setHostDeviceInfo(HDC_KEYS.VERSION, deviceVersion); + this.setHostDeviceInfo(HDC_KEYS.RETRIEVE_SECRETS_FROM_TMSH, + isVersionAffectedBySecretsBug(deviceVersion)); + }); + }, + + /** + * Clear Host Device Info + * + * @param {...String} [key] - key(s) to remove, if absent then all keys will be removed + */ + clearHostDeviceInfo() { + const keysToRemove = arguments.length ? arguments : Object.keys(HOST_DEVICE_CACHE); + Array.prototype.forEach.call(keysToRemove, (toRemove) => { + delete HOST_DEVICE_CACHE[toRemove]; + }); + }, + + /** + * Get Host Device info + * + * @param {String} [key] - key, if omitted then copy of cache will be returned + * + * @returns {Object|Any} value from cache for the key or copy of cache if no arguments were passed to function + */ + getHostDeviceInfo(key) { + if (arguments.length === 0) { + return util.deepCopy(HOST_DEVICE_CACHE); + } + return HOST_DEVICE_CACHE[key]; + }, + + /** + * Set Host Device Info + * @param {String} key - key + * @param {Any} value - value + */ + setHostDeviceInfo(key, value) { + HOST_DEVICE_CACHE[key] = value; + }, + /** * Performs a check of the local environment and returns device type * * @returns {Promise} A promise which is resolved with the device type. - * */ getDeviceType() { - // eslint-disable-next-line no-unused-vars - return new Promise((resolve, reject) => { - // eslint-disable-next-line no-unused-vars - childProcess.exec('/usr/bin/tmsh -a show sys version', (error, stdout, stderr) => { - if (error) { - // don't reject, just assume we are running on a container - resolve(constants.CONTAINER_DEVICE_TYPE); + const deviceType = this.getHostDeviceInfo(HDC_KEYS.TYPE); + if (typeof deviceType !== 'undefined') { + return Promise.resolve(deviceType); + } + + return new Promise((resolve) => { + const versionFile = '/VERSION'; + fs.readFile(versionFile, (err, data) => { + // .toString() in case if data is Buffer + if (!err && (new RegExp('product:\\s+big-ip', 'i')).test(data.toString())) { + resolve(constants.DEVICE_TYPE.BIG_IP); } else { - // command did not error so we must be a BIG-IP - resolve(constants.BIG_IP_DEVICE_TYPE); + // don't reject, just assume we are running on a container + if (err) { + logger.debug(`Unable to read '${versionFile}': ${err}`); + } + resolve(constants.DEVICE_TYPE.CONTAINER); } }); }); @@ -600,8 +685,9 @@ module.exports = { // should have content-range header if (!crange) { const msg = `${respObj.statusCode} ${respObj.statusMessage} ${JSON.stringify(respBody)}`; - throw new Error(`HTTP Error: ${msg}`); - } else if (respObj.statusCode >= 200 && respObj.statusCode < 300) { + return Promise.reject(new Error(`HTTP Error: ${msg}`)); + } + if (respObj.statusCode >= 200 && respObj.statusCode < 300) { // handle it in async way, waiting for callabck from write promise = new Promise((resolve, reject) => { currentBytes += parseInt(respObj.headers['content-length'], 10); @@ -616,7 +702,7 @@ module.exports = { } else { attempt += 1; if (attempt >= attemptsOnHTTPerror) { - error = new Error('Exceeded number of attempts on HTTP error'); + return Promise.reject(new Error('Exceeded number of attempts on HTTP error')); } } @@ -865,8 +951,19 @@ module.exports = { * @returns {Object} Returns promise resolved with encrypted secret */ encryptSecret(data) { - const splitData = data.match(/(.|\n).{1,500}/g); - return encryptSecretHelper(splitData, [], 0).then(result => result.join(',')); + let affectedByBug = this.getHostDeviceInfo(HDC_KEYS.RETRIEVE_SECRETS_FROM_TMSH); + let promise = Promise.resolve(); + + if (typeof affectedByBug === 'undefined') { + promise = promise.then(() => this.getDeviceVersion(constants.LOCAL_HOST)) + .then((deviceVersion) => { + affectedByBug = isVersionAffectedBySecretsBug(deviceVersion); + }); + } + return promise.then(() => { + const splitData = data.match(/(.|\n){1,500}/g); + return encryptSecretHelper(splitData, [], 0, affectedByBug).then(result => result.join(',')); + }); }, /** @@ -978,8 +1075,9 @@ module.exports = { // return (modified) data return data; }) - .catch((e) => { - throw e; + .catch((err) => { + const msg = `decryptAllSecrets: ${err}`; + throw new Error(msg); }); }, diff --git a/src/lib/endpointLoader.js b/src/lib/endpointLoader.js index f39a4f88..79253370 100644 --- a/src/lib/endpointLoader.js +++ b/src/lib/endpointLoader.js @@ -8,19 +8,61 @@ 'use strict'; -const deviceUtil = require('./deviceUtil.js'); -const constants = require('./constants.js'); -const util = require('./util.js'); -const logger = require('./logger.js'); +const deviceUtil = require('./deviceUtil'); +const constants = require('./constants'); +const util = require('./util'); +const logger = require('./logger'); +/** @module EndpointLoader */ + +/** + * Options to use to expand reference + * + * @typedef {Object} ExpandReferencesOpts + * @property {String} [endpointSuffix] - URI suffix to use to modify link + * @property {Boolean} [includeStats] - include response from /stats + */ +/** + * References to expand + * + * @typedef {Object.} newEndpoints - list of endpoints to add */ EndpointLoader.prototype.setEndpoints = function (newEndpoints) { - this.endpoints = {}; - newEndpoints.forEach((endpoint) => { - // if 'name' presented then use it as unique ID - // otherwise using 'endpoint' prop - this.endpoints[endpoint.name || endpoint.endpoint] = endpoint; - }); + if (Array.isArray(newEndpoints)) { + this.endpoints = {}; + newEndpoints.forEach((endpoint) => { + // if 'name' presented then use it as unique ID + // otherwise use path prop + this.endpoints[endpoint.name || endpoint.path] = endpoint; + }); + } else { + this.endpoints = newEndpoints; + } }; /** * Authenticate on target device * - * @returns {Object} Promise which is resolved when successfully authenticated + * @returns {Promise} Promise which is resolved when successfully authenticated */ EndpointLoader.prototype.auth = function () { if (this.options.credentials.token) { return Promise.resolve(); } - // in case of optimization, replace with Object.assign - const options = util.deepCopy(this.options.connection); + const options = Object.assign({}, this.options.connection); return deviceUtil.getAuthToken( this.host, this.options.credentials.username, this.options.credentials.passphrase, options ) .then((token) => { this.options.credentials.token = token.token; - }) - .catch((err) => { - throw err; }); }; - /** * Load data from endpoint * @@ -82,59 +134,158 @@ EndpointLoader.prototype.auth = function () { * @param {Object} [options] - function options * @param {Object} [options.replaceStrings] - key/value pairs that replace matching strings in request body * - * @returns {Object} Promise resolved with fetched data + * @returns {Promise} Promise resolved with FetchedData */ EndpointLoader.prototype.loadEndpoint = function (endpoint, options) { - const opts = options || {}; - const endpointObj = this.endpoints[endpoint]; - + let endpointObj = this.endpoints[endpoint]; if (endpointObj === undefined) { return Promise.reject(new Error(`Endpoint not defined in file: ${endpoint}`)); } - - let dataIsEmpty = false; - if (this.cachedResponse[endpoint] === undefined) { - dataIsEmpty = true; + // TODO: fix it later, right now it doesn't work with multiple concurrent connections + if (!endpointObj.ignoreCached && typeof this.cachedResponse[endpoint] !== 'undefined') { + return Promise.resolve(this.cachedResponse[endpoint]); } - - if ((endpointObj || {}).ignoreCached) { - dataIsEmpty = true; + if ((options || {}).replaceStrings) { + endpointObj = Object.assign({}, endpointObj); + endpointObj.body = this.replaceBodyVars(endpointObj.body, options.replaceStrings); } - - return Promise.resolve() - .then(() => { - if (dataIsEmpty) { - return this._getAndExpandData(endpointObj, { replaceStrings: opts.replaceStrings }); - } - return Promise.resolve(this.cachedResponse[endpoint]); - }) + return this.getAndExpandData(endpointObj) .then((response) => { - if (dataIsEmpty) { - // Cache data for later calls - this.cachedResponse[endpoint] = response; - } + this.cachedResponse[endpoint] = response; return Promise.resolve(response); }) .catch((err) => { - logger.error(`Error: EndpointLoader.loadEndpoint: ${endpoint}: ${err}`); + this.logger.error(`Error: EndpointLoader.loadEndpoint: ${endpoint}: ${err}`); return Promise.reject(err); }); }; + +/** + * Expand references + * + * @param {module:EndpointLoader~Endpoint} endpointObj - endpoint object + * @param {module:EndpointLoader~FetchedData} data - fetched data + * + * @returns {Promise>} resolved with array of FetchedData + */ +EndpointLoader.prototype.expandReferences = function (endpointObj, data) { + const promises = []; + const dataItems = data.data.items; + if (endpointObj.expandReferences && dataItems && Array.isArray(dataItems) && dataItems.length) { + // for now let's just support a single reference + const referenceKey = Object.keys(endpointObj.expandReferences)[0]; + const referenceObj = endpointObj.expandReferences[referenceKey]; + for (let i = 0; i < dataItems.length; i += 1) { + const item = dataItems[i][referenceKey]; + if (item && item.link) { + let referenceEndpoint = this.getURIPath(item.link); + if (referenceObj.endpointSuffix) { + referenceEndpoint = `${referenceEndpoint}${referenceObj.endpointSuffix}`; + } + if (referenceObj.includeStats) { + promises.push(this.getData(`${referenceEndpoint}/stats`, { name: i, refKey: referenceKey })); + } + promises.push(this.getData(referenceEndpoint, { name: i, refKey: referenceKey })); + } + } + } + return Promise.all(promises); +}; +/** + * Fetch stats for each item + * + * @param {module:EndpointLoader~Endpoint} endpointObj - endpoint object + * @param {Object} data - data + * @param {String} data.name - name + * @param {Object} data.data - data to process + * + * @returns {Promise>}} resolved with array of FetchedData + */ +EndpointLoader.prototype.fetchStats = function (endpointObj, data) { + const promises = []; + const dataItems = data.data.items; + if (endpointObj.includeStats && dataItems && Array.isArray(dataItems) && dataItems.length) { + for (let i = 0; i < dataItems.length; i += 1) { + const item = dataItems[i]; + // check for selfLink property + if (item.selfLink) { + promises.push(this.getData(`${this.getURIPath(item.selfLink)}/stats`, { name: i })); + } + } + } + return Promise.all(promises); +}; +/** + * Substitute data + * + * @param {module:EndpointLoader~FetchedData} baseData - base data + * @param {Array} dataArray - array of data to use for substitution + * @param {Boolean} shallowCopy - true if shallow copy required else + * original object will be used + */ +EndpointLoader.prototype.substituteData = function (baseData, dataArray, shallowCopy) { + if (!dataArray.length) { + return; + } + const baseDataItems = baseData.data.items; + dataArray.forEach((data) => { + try { + let dataToSubstitute; + if (shallowCopy === true) { + dataToSubstitute = Object.assign(data.data, baseDataItems[data.name]); + } else { + dataToSubstitute = data.data; + } + if (data.refKey) { + // if this is the first time substituting data, overwrite the containing object with data + // e.g. + // itemsRef: { + // link: 'http://objLink/objItems', + // isSubcollection: true + // } + // will become: + // itemsRef: { + // objItemProp1: 123 //data from link + // } + if (baseDataItems[data.name][data.refKey].link) { + baseDataItems[data.name][data.refKey] = dataToSubstitute; + } else { + // otherwise if same object has been previously substituted + // and we're merging new set of props from a different link (e.g. objItems/stats) + // then copy over the properties of the new dataToSubstitute + // e.g. + // itemsRef: { + // objItemProp1: 123 + // objItemProp2: true + // } + Object.assign(baseDataItems[data.name][data.refKey], dataToSubstitute); + } + } else { + baseDataItems[data.name] = dataToSubstitute; + } + } catch (e) { + // just continue + } + }); +}; /** * Get data for specific endpoint * * @param {String} uri - uri where data resides - * @param {Object} options - function options + * @param {Object} [options] - function options * @param {String} [options.name] - name of key to store as, will override default of uri * @param {String} [options.body] - body to send, sent via POST request + * @param {String} [options.refKey] - reference key * @param {String[]} [options.endpointFields] - restrict collection to these fields * - * @returns {Object} Promise which is resolved with data + * @returns {Promise} resolved with FetchedData */ -EndpointLoader.prototype._getData = function (uri, options) { - logger.debug(`EndpointLoader._getData: loading data from URI = ${uri}`); - // remove parse-stringify in case of optimizations - const httpOptions = Object.assign({}, util.deepCopy(this.options.connection)); +EndpointLoader.prototype.getData = function (uri, options) { + this.logger.debug(`EndpointLoader.getData: loading data from URI = ${uri}`); + + options = options || {}; + const httpOptions = Object.assign({}, this.options.connection); + httpOptions.credentials = { username: this.options.credentials.username, token: this.options.credentials.token @@ -147,165 +298,86 @@ EndpointLoader.prototype._getData = function (uri, options) { maxTries: 3, backoff: 100 }; - - let fullUri = uri; - if (options.endpointFields) { - fullUri = `${fullUri}?$select=${options.endpointFields.join(',')}`; - } - + const fullUri = options.endpointFields ? `${uri}?$select=${options.endpointFields.join(',')}` : uri; return util.retryPromise(() => deviceUtil.makeDeviceRequest(this.host, fullUri, httpOptions), retryOpts) .then((data) => { - // use uri unless name is explicitly provided - const nameToUse = options.name !== undefined ? options.name : uri; - const ret = { name: nameToUse, data }; + const ret = { + name: options.name !== undefined ? options.name : uri, + data + }; + if (options.refKey) { + ret.refKey = options.refKey; + } return ret; - }) - .catch((err) => { - throw err; }); }; /** * Get data for specific endpoint (with some extra logic) * - * @param {Object} endpointProperties - endpoint properties - * @param {Object} [options] - function options - * @param {Object} [options.replaceStrings] - key/value pairs that replace matching strings in request body - * - * @returns {Object} Promise which is resolved with data + * @param {module:EndpointLoader~Endpoint} endpointObj - endpoint object + * @param {String} endpointObj.path - URI path to get data from + * @returns {Promise} resolved with FetchedData */ -EndpointLoader.prototype._getAndExpandData = function (endpointProperties, options) { - const opts = options || {}; - const p = endpointProperties; - let completeData; - let referenceKey; - const childItemKey = 'items'; // assume we are looking inside of 'items' - - // remote protocol, host and query params - const fixEndpoint = i => i.replace('https://localhost', '').split('?')[0]; - - const substituteData = (data, childKey, assign) => { - // this tells us we need to modify the data - if (completeData) { - data.forEach((i) => { - try { - let dataToSubstitute; - if (assign === true) { - dataToSubstitute = Object.assign(i.data, completeData.data[childItemKey][i.name]); - } else { - dataToSubstitute = i.data; - } - - if (childKey) { - // if this is the first time substituting data, overwrite the containing object with data - // e.g. - // itemsRef: { - // link: 'http://objLink/objItems', - // isSubcollection: true - // } - // will become: - // itemsRef: { - // objItemProp1: 123 //data from link - // } - if (completeData.data[childItemKey][i.name][childKey].link) { - completeData.data[childItemKey][i.name][childKey] = dataToSubstitute; - } else { - // otherwise if same object has been previously substituted - // and we're merging new set of props from a different link (e.g. objItems/stats) - // then copy over the properties of the new dataToSubstitute - // e.g. - // itemsRef: { - // objItemProp1: 123 - // objItemProp2: true - // } - Object.assign(completeData.data[childItemKey][i.name][childKey], dataToSubstitute); - } - } else { - completeData.data[childItemKey][i.name] = dataToSubstitute; - } - } catch (e) { - // just continue - } - }); - return Promise.resolve(completeData); // return substituted data - } - return Promise.resolve(data); // return data - }; - - const replaceBodyVars = (body, replaceStrings) => { - let bodyStr = JSON.stringify(body); - - Object.keys(replaceStrings).forEach((key) => { - bodyStr = bodyStr.replace(new RegExp(key), replaceStrings[key]); +EndpointLoader.prototype.getAndExpandData = function (endpointObj) { + // baseData in this method is the data fetched from endpointObj.path + return this.getData(endpointObj.path, endpointObj) + // Promise below will be resolved with array of 2 elements: + // [ baseData, [refData, refData] ] + .then(baseData => Promise.all([ + Promise.resolve(baseData), + this.expandReferences(endpointObj, baseData) + ])) + .then((dataArray) => { + // dataArray === [ baseData, [refData, refData] ] + const baseData = dataArray[0]; + this.substituteData(baseData, dataArray[1], false); + return Promise.all([ + Promise.resolve(baseData), + this.fetchStats(endpointObj, baseData) + ]); + }) + // Promise below will be resolved with array of 2 elements: + // [ baseData, [statsData, statsData] ] + .then((dataArray) => { + // dataArray === [ baseData, [statsData, statsData] ] + const baseData = dataArray[0]; + this.substituteData(baseData, dataArray[1], true); + return baseData; }); +}; - return JSON.parse(bodyStr); - }; - - const body = opts.replaceStrings ? replaceBodyVars(p.body, opts.replaceStrings) : p.body; - - return this._getData( - p.endpoint, - { name: p.name, body, endpointFields: p.endpointFields } - ) - .then((data) => { - // data: { name: foo, data: bar } - // check if expandReferences property was specified - if (p.expandReferences) { - completeData = data; - const actualData = data.data; - // set default value if not exists - actualData[childItemKey] = actualData[childItemKey] === undefined ? [] : actualData[childItemKey]; - // for now let's just support a single reference - referenceKey = Object.keys(p.expandReferences)[0]; - const referenceObj = p.expandReferences[Object.keys(p.expandReferences)[0]]; - - const promises = []; - if (typeof actualData === 'object' && Array.isArray(actualData[childItemKey])) { - for (let i = 0; i < actualData[childItemKey].length; i += 1) { - const item = actualData[childItemKey][i]; - // first check for reference and then link property - if (item[referenceKey] && item[referenceKey].link) { - let referenceEndpoint = fixEndpoint(item[referenceKey].link); - if (referenceObj.endpointSuffix) { - referenceEndpoint = `${referenceEndpoint}${referenceObj.endpointSuffix}`; - } - if (referenceObj.includeStats) { - promises.push(this._getData(`${referenceEndpoint}/stats`, { name: i })); - } - promises.push(this._getData(referenceEndpoint, { name: i })); - } - } - } - return Promise.all(promises); - } - return Promise.resolve(data); // just return the data - }) - .then(data => substituteData(data, referenceKey, false)) - .then((data) => { - completeData = null; - // check if includeStats property was specified - if (p.includeStats) { - completeData = data; - const actualData = data.data; +/** + * Replace variables in body with values + * + * @param {Object|String} body - request body + * @param {Object} keys - keys/vars to replace + * + * @returns {Object|String} + */ +EndpointLoader.prototype.replaceBodyVars = function (body, keys) { + let isObject = false; + if (typeof body !== 'string') { + isObject = true; + body = JSON.stringify(body); + } + Object.keys(keys).forEach((key) => { + body = body.replace(new RegExp(key), keys[key]); + }); + if (isObject) { + body = JSON.parse(body); + } + return body; +}; - const promises = []; - if (typeof actualData === 'object' && Array.isArray(actualData[childItemKey])) { - for (let i = 0; i < actualData[childItemKey].length; i += 1) { - const item = actualData[childItemKey][i]; - // check for selfLink property - if (item.selfLink) { - promises.push(this._getData(`${fixEndpoint(item.selfLink)}/stats`, { name: i })); - } - } - } - return Promise.all(promises); - } - return Promise.resolve(data); // just return the data - }) - .then(data => substituteData(data, null, true)) - .catch((err) => { - throw err; - }); +/** + * Get URI path + * + * @param {String} uri - URI + * + * @returns {String} URI path + */ +EndpointLoader.prototype.getURIPath = function (uri) { + return uri.replace('https://localhost', '').split('?')[0]; }; module.exports = EndpointLoader; diff --git a/src/lib/eventListener.js b/src/lib/eventListener.js index 9e913c86..2cf65e20 100644 --- a/src/lib/eventListener.js +++ b/src/lib/eventListener.js @@ -11,23 +11,23 @@ const net = require('net'); const dgram = require('dgram'); -const logger = require('./logger.js'); -const constants = require('./constants.js'); -const normalize = require('./normalize.js'); -const dataPipeline = require('./dataPipeline.js'); -const configWorker = require('./config.js'); +const logger = require('./logger'); +const constants = require('./constants'); +const normalize = require('./normalize'); +const dataPipeline = require('./dataPipeline'); +const configWorker = require('./config'); const properties = require('./properties.json'); -const tracers = require('./util.js').tracer; -const stringify = require('./util.js').stringify; -const isObjectEmpty = require('./util.js').isObjectEmpty; +const tracers = require('./util').tracer; +const stringify = require('./util').stringify; +const isObjectEmpty = require('./util').isObjectEmpty; const global = properties.global; const events = properties.events; const definitions = properties.definitions; const DEFAULT_PORT = constants.DEFAULT_EVENT_LISTENER_PORT; -const CLASS_NAME = constants.EVENT_LISTENER_CLASS_NAME; +const CLASS_NAME = constants.CONFIG_CLASSES.EVENT_LISTENER_CLASS_NAME; const MAX_BUFFER_SIZE = 16 * 1024; // 16k chars const MAX_BUFFER_TIMEOUTS = 5; @@ -454,6 +454,6 @@ configWorker.on('change', (config) => { }); logger.debug(`${Object.keys(listeners).length} event listener(s) listening`); - tracers.remove(null, tracer => tracer.name.startsWith(CLASS_NAME) - && tracer.lastGetTouch < tracersTimestamp); + tracers.remove(tracer => tracer.name.startsWith(CLASS_NAME) + && tracer.lastGetTouch < tracersTimestamp); }); diff --git a/src/lib/forwarder.js b/src/lib/forwarder.js index 95d671da..e7b9259a 100644 --- a/src/lib/forwarder.js +++ b/src/lib/forwarder.js @@ -8,8 +8,8 @@ 'use strict'; -const logger = require('./logger.js'); // eslint-disable-line no-unused-vars -const consumersHndlr = require('./consumers.js'); +const logger = require('./logger'); // eslint-disable-line no-unused-vars +const consumersHndlr = require('./consumers'); /** * Forward data to consumer diff --git a/src/lib/ihealth.js b/src/lib/ihealth.js index c04491d8..203261b3 100644 --- a/src/lib/ihealth.js +++ b/src/lib/ihealth.js @@ -8,17 +8,17 @@ 'use strict'; -const logger = require('./logger.js'); // eslint-disable-line no-unused-vars -const constants = require('./constants.js'); -const util = require('./util.js'); -const configWorker = require('./config.js'); -const iHealthPoller = require('./ihealthPoller.js'); -const dataPipeline = require('./dataPipeline.js'); -const normalize = require('./normalize.js'); +const logger = require('./logger'); // eslint-disable-line no-unused-vars +const constants = require('./constants'); +const util = require('./util'); +const configWorker = require('./config'); +const iHealthPoller = require('./ihealthPoller'); +const dataPipeline = require('./dataPipeline'); +const normalize = require('./normalize'); const properties = require('./properties.json').ihealth; -const SYSTEM_CLASS_NAME = constants.SYSTEM_CLASS_NAME; -const IHEALTH_POLLER_CLASS_NAME = constants.IHEALTH_POLLER_CLASS_NAME; +const SYSTEM_CLASS_NAME = constants.CONFIG_CLASSES.SYSTEM_CLASS_NAME; +const IHEALTH_POLLER_CLASS_NAME = constants.CONFIG_CLASSES.IHEALTH_POLLER_CLASS_NAME; /** @module ihealth */ @@ -241,8 +241,8 @@ configWorker.on('change', (config) => { } }); - util.tracer.remove(null, tracer => tracer.name.startsWith(IHEALTH_POLLER_CLASS_NAME) - && tracer.lastGetTouch < tracersTimestamp); + util.tracer.remove(tracer => tracer.name.startsWith(IHEALTH_POLLER_CLASS_NAME) + && tracer.lastGetTouch < tracersTimestamp); logger.debug(`${Object.keys(pollers).length} iHealth poller(s) running`); }); diff --git a/src/lib/ihealthPoller.js b/src/lib/ihealthPoller.js index 2e334f4f..e4745d78 100644 --- a/src/lib/ihealthPoller.js +++ b/src/lib/ihealthPoller.js @@ -8,17 +8,17 @@ 'use strict'; -const logger = require('./logger.js'); // eslint-disable-line no-unused-vars -const constants = require('./constants.js'); -const datetimeUtil = require('./datetimeUtil.js'); -const util = require('./util.js'); -const deviceUtil = require('./deviceUtil.js'); -const ihUtil = require('./ihealthUtil.js'); -const persistentStorage = require('./persistentStorage.js').persistentStorage; -const configWorker = require('./config.js'); - -const SYSTEM_CLASS_NAME = constants.SYSTEM_CLASS_NAME; -const IHEALTH_POLLER_CLASS_NAME = constants.IHEALTH_POLLER_CLASS_NAME; +const logger = require('./logger'); // eslint-disable-line no-unused-vars +const constants = require('./constants'); +const datetimeUtil = require('./datetimeUtil'); +const util = require('./util'); +const deviceUtil = require('./deviceUtil'); +const ihUtil = require('./ihealthUtil'); +const persistentStorage = require('./persistentStorage').persistentStorage; +const configWorker = require('./config'); + +const SYSTEM_CLASS_NAME = constants.CONFIG_CLASSES.SYSTEM_CLASS_NAME; +const IHEALTH_POLLER_CLASS_NAME = constants.CONFIG_CLASSES.IHEALTH_POLLER_CLASS_NAME; const PERSISTENT_STORAGE_KEY = 'ihealth'; const IHEALTH_POLL_MAX_TIMEOUT = 60 * 60 * 1000; // 1 h. diff --git a/src/lib/ihealthUtil.js b/src/lib/ihealthUtil.js index 43639ff3..1a7a6d11 100644 --- a/src/lib/ihealthUtil.js +++ b/src/lib/ihealthUtil.js @@ -13,10 +13,10 @@ const fs = require('fs'); const path = require('path'); const request = require('request'); -const logger = require('./logger.js'); -const constants = require('./constants.js'); -const util = require('./util.js'); -const deviceUtil = require('./deviceUtil.js'); +const logger = require('./logger'); +const constants = require('./constants'); +const util = require('./util'); +const deviceUtil = require('./deviceUtil'); /** @module ihealthUtil */ @@ -816,7 +816,7 @@ QkviewManager.prototype.prepare = function () { .then(() => deviceUtil.getDeviceType()) .then((deviceType) => { this.deviceType = deviceType; - if (this.deviceType === constants.BIG_IP_DEVICE_TYPE) { + if (this.deviceType === constants.DEVICE_TYPE.BIG_IP) { return this.checkIsItLocalDevice(); } return Promise.resolve(); diff --git a/src/lib/logger.js b/src/lib/logger.js index e9c8bfaa..e044a96e 100644 --- a/src/lib/logger.js +++ b/src/lib/logger.js @@ -8,6 +8,8 @@ 'use strict'; +/** @module logger */ + let logger; try { // eslint-disable-next-line global-require, import/no-unresolved diff --git a/src/lib/normalize.js b/src/lib/normalize.js index b86e9539..1f8e4cba 100644 --- a/src/lib/normalize.js +++ b/src/lib/normalize.js @@ -8,9 +8,9 @@ 'use strict'; -const logger = require('./logger.js'); // eslint-disable-line no-unused-vars -const constants = require('./constants.js'); -const normalizeUtil = require('./normalizeUtil.js'); +const logger = require('./logger'); // eslint-disable-line no-unused-vars +const constants = require('./constants'); +const normalizeUtil = require('./normalizeUtil'); /** @@ -164,8 +164,10 @@ module.exports = { try { return normalizeUtil[options.func](args); } catch (e) { - logger.exception(`runCustomFunction failed: ${e}`, e); - throw new Error(`runCustomFunction failed: ${e}`); + const errMsg = `runCustomFunction '${options.func}' failed: ${e}`; + logger.exception(errMsg, e); + e.message = errMsg; + throw e; } }, @@ -208,7 +210,9 @@ module.exports = { const keysToReduce = ['nestedStats', 'value', 'description', 'color']; for (let i = 0; i < keysToReduce.length; i += 1) { const item = data[keysToReduce[i]]; - if (item !== undefined && Object.keys(data).length === 1) return this._reduceData(item, options); + if (item !== undefined && Object.keys(data).length === 1) { + return this._reduceData(item, options); + } } // .entries evaluates to true if data is an array @@ -285,10 +289,19 @@ module.exports = { * @param {Object} options - options * @param {Array} [options.skip] - array of child object keys to skip * @param {Array} [options.classifyByKeys] - classify by specific keys (used by events) + * @param {Array} [options.tags] - tags to apply in addition to "tags" * * @returns {Object} Returns data with added tags */ _addKeysByTag(data, tags, definitions, options) { + tags = Object.assign({}, tags); + if (options && options.tags) { + Object.keys(options.tags).forEach((key) => { + if (typeof tags[key] === 'undefined') { + tags[key] = options.tags[key]; + } + }); + } const tagKeys = Object.keys(tags); const skip = options.skip || []; const def = definitions || {}; @@ -301,15 +314,20 @@ module.exports = { // then check if the tag value contains 'pattern' // otherwise assume the tag value is a 'constant' let tagValue = tags[t]; - if (tagValue in def) tagValue = def[tagValue]; // overwrite with def value - + if (tagValue in def) { + tagValue = def[tagValue]; // overwrite with def value + } if (tagValue.pattern) { const match = normalizeUtil._checkForMatch(key, tagValue.pattern, tagValue.group); - if (match) val = match; + if (match) { + val = match; + } } else { val = tagValue; } - thisData[t] = val; + if (val) { + thisData[t] = val; + } }); return thisData; }; @@ -549,11 +567,11 @@ module.exports = { includeFirstEntry: (options.normalization.find(n => n.includeFirstEntry) || {}).includeFirstEntry }; - ret = this._reduceData(data, reduceDataOptions); + ret = this._reduceData(norm.useCurrentData ? ret : data, reduceDataOptions); setReduced = true; // get data by key - ret = options.key ? this._getDataByKey(ret, options.key) : ret; + ret = options.key && !norm.keepKey ? this._getDataByKey(ret, options.key) : ret; } if (norm.filterKeys) { diff --git a/src/lib/normalizeConfig.js b/src/lib/normalizeConfig.js new file mode 100644 index 00000000..686e6109 --- /dev/null +++ b/src/lib/normalizeConfig.js @@ -0,0 +1,285 @@ +/* + * Copyright 2020. F5 Networks, Inc. See End User License Agreement ("EULA") for + * license terms. Notwithstanding anything to the contrary in the EULA, Licensee + * may copy and modify this software product for its internal business purposes. + * Further, Licensee may upload, publish and distribute the modified version of + * the software product on devcentral.f5.com. + */ + +'use strict'; + +const constants = require('./constants'); +const logger = require('./logger'); +const util = require('./util'); + +const CONFIG_CLASSES = constants.CONFIG_CLASSES; + +/** @module normalizeConfig */ + +function getTelemetryObjects(originalConfig, className) { + return originalConfig[className] || {}; +} + +function getTelemetrySystems(originalConfig) { + return getTelemetryObjects(originalConfig, CONFIG_CLASSES.SYSTEM_CLASS_NAME); +} + +function getTelemetrySystemPollers(originalConfig) { + return getTelemetryObjects(originalConfig, CONFIG_CLASSES.SYSTEM_POLLER_CLASS_NAME); +} + +function getTelemetryEndpoints(originalConfig) { + return getTelemetryObjects(originalConfig, CONFIG_CLASSES.ENDPOINTS_CLASS_NAME); +} + +/** + * Force allowSelfSignedCert to default value if not specified + * + * @param {Object} originalConfig - origin config + */ +function verifyAllowSelfSignedCert(originalConfig) { + const telemetrySystems = getTelemetrySystems(originalConfig); + Object.keys(telemetrySystems).forEach((systemName) => { + const system = telemetrySystems[systemName]; + if (typeof system.allowSelfSignedCert === 'undefined') { + system.allowSelfSignedCert = !constants.STRICT_TLS_REQUIRED; + } + }); +} + +/** + * Expand endpoints references in Telemetry_System objects + * + * @param {Object} originalConfig - origin config + */ +function normalizeTelemetryEndpoints(originalConfig) { + const telemetryEndpoints = getTelemetryEndpoints(originalConfig); + const telemetrySystems = getTelemetrySystems(originalConfig); + + function computeBasePath(endpoint) { + let basePath = ''; + if (endpoint.basePath && endpoint.basePath.length > 0) { + const pathPrefix = endpoint.basePath.startsWith('/') ? '' : '/'; + if (endpoint.basePath.endsWith('/')) { + basePath = endpoint.basePath.substring(0, endpoint.basePath.length - 1); + } else { + basePath = endpoint.basePath; + } + basePath = `${pathPrefix}${basePath}`; + } + endpoint.basePath = basePath; + } + + function fixEndpointPath(endpoint) { + endpoint.path = endpoint.path.startsWith('/') ? endpoint.path : `/${endpoint.path}`; + } + + function parseEndpointItem(endpoint, key) { + const innerEndpoint = util.deepCopy(endpoint.items[key]); + fixEndpointPath(innerEndpoint); + innerEndpoint.enable = endpoint.enable && innerEndpoint.enable; + innerEndpoint.path = `${endpoint.basePath}${innerEndpoint.path}`; + innerEndpoint.name = innerEndpoint.name || key; + return innerEndpoint; + } + + function processEndpoint(endpoint, cb) { + if (typeof endpoint === 'object') { + // array of definitions - can be all of the following + if (Array.isArray(endpoint)) { + endpoint.forEach(innerEndpoint => processEndpoint(innerEndpoint, cb)); + // endpoint is Telemetry_Endpoints + } else if (endpoint.class === CONFIG_CLASSES.ENDPOINTS_CLASS_NAME || endpoint.items) { + // don't need to copy 'endpoint' because it was either reference or inline config + computeBasePath(endpoint); + Object.keys(endpoint.items).forEach(key => cb(parseEndpointItem(endpoint, key))); + // endpoint is Telemetry_Endpoint + } else if (typeof endpoint.path === 'string') { + fixEndpointPath(endpoint); + cb(endpoint); + } + } else if (typeof endpoint === 'string') { + const refs = endpoint.split('/'); + // reference to a Telemetry_Endpoints object + // format is ObjectName/pathName + endpoint = telemetryEndpoints[refs[0]]; + if (refs.length > 1) { + // reference to a child of Telemetry_Endpoints.items + const item = endpoint.items[refs[1]]; + endpoint = { + items: { [refs[1]]: item }, + basePath: endpoint.basePath, + enable: endpoint.enable + }; + } + processEndpoint(endpoint, cb); + } + } + + Object.keys(telemetrySystems).forEach((systemName) => { + const system = telemetrySystems[systemName]; + system.systemPollers.forEach((poller) => { + if (Object.prototype.hasOwnProperty.call(poller, 'endpointList')) { + const endpointList = {}; + processEndpoint(poller.endpointList, (endpoint) => { + if (endpoint.enable) { + endpointList[endpoint.name] = endpoint; + } else { + logger.debug(`${systemName}: ignoring disabled endpoint '${endpoint.name}' ('${endpoint.path}')`); + } + }); + poller.endpointList = endpointList; + } + }); + }); +} + +/** + * Expand references in Telemetry_System objects + * Note: as result each System and its System Pollers will have 'name' property with actual name + * + * @param {Object} originalConfig - origin config + */ +function normalizeTelemetrySystems(originalConfig) { + const sysPollersToDelete = {}; + const telemetrySystems = getTelemetrySystems(originalConfig); + const telemetrySystemPollers = getTelemetrySystemPollers(originalConfig); + const keysToCopy = [ + 'actions', 'enable', 'endpointList', + 'interval', 'tag', 'trace' + ]; + + const copySystemPoller = (systemPoller) => { + const newSystemPoller = {}; + systemPoller = util.deepCopy(systemPoller); + keysToCopy.forEach((key) => { + if (Object.prototype.hasOwnProperty.call(systemPoller, key)) { + newSystemPoller[key] = systemPoller[key]; + } + }); + return newSystemPoller; + }; + + const createSystemPollerName = id => `SystemPoller_${id}`; + + Object.keys(telemetrySystems).forEach((systemName) => { + const system = telemetrySystems[systemName]; + system.name = systemName; + + system.systemPollers = system.systemPoller; + delete system.systemPoller; + + if (!Array.isArray(system.systemPollers)) { + system.systemPollers = system.systemPollers ? [system.systemPollers] : []; + } + // existing Telemetry_System_Poller names + const existingNames = []; + system.systemPollers.forEach((systemPoller, index, pollers) => { + // systemPoller can be either string or object + if (typeof systemPoller === 'string') { + // expand reference and replace it with existing configuration + sysPollersToDelete[systemPoller] = true; + pollers[index] = copySystemPoller(telemetrySystemPollers[systemPoller]); + pollers[index].name = systemPoller; + existingNames.push(systemPoller); + } + }); + // time to assign name to pollers without name + let nameID = 0; + system.systemPollers.forEach((systemPoller) => { + if (typeof systemPoller.name === 'undefined') { + do { + nameID += 1; + systemPoller.name = createSystemPollerName(nameID); + } while (existingNames.indexOf(systemPoller.name) !== -1); + } + }); + }); + // remove System Pollers that were used as references + Object.keys(sysPollersToDelete).forEach((key) => { + delete telemetrySystemPollers[key]; + }); +} + +/** + * Convert Telemetry_System_Poller to Telemetry_System + * Note: as result each System and its System Pollers will have 'name' property with actual name + * + * @param {Object} originalConfig - origin config + */ +function normalizeTelemetrySystemPollers(originalConfig) { + const telemetrySystems = getTelemetrySystems(originalConfig); + const telemetrySystemPollers = getTelemetrySystemPollers(originalConfig); + const keysToCopy = [ + 'allowSelfSignedCert', 'enable', 'enableHostConnectivityCheck', 'host', + 'port', 'protocol', 'passphrase', 'trace', 'username' + ]; + const skipDelete = ['enable', 'trace']; + delete originalConfig[CONFIG_CLASSES.SYSTEM_POLLER_CLASS_NAME]; + + function createSystemFromSystemPoller(systemPollerName, systemPoller) { + /** + * if Telemetry_System_Poller is not referenced by any of Telemetry_System + * then it should be converted to Telemetry_System. + * Don't need to make copy of origin object. + */ + delete systemPoller.class; + const newSystem = { + class: CONFIG_CLASSES.SYSTEM_CLASS_NAME + }; + keysToCopy.forEach((key) => { + if (Object.prototype.hasOwnProperty.call(systemPoller, key)) { + newSystem[key] = systemPoller[key]; + if (skipDelete.indexOf(key) === -1) { + delete systemPoller[key]; + } + } + }); + + systemPoller.name = systemPollerName; + newSystem.name = systemPollerName; + newSystem.systemPollers = [systemPoller]; + return newSystem; + } + + Object.keys(telemetrySystemPollers).forEach((systemPollerName) => { + logger.debug(`Converting ${CONFIG_CLASSES.SYSTEM_POLLER_CLASS_NAME} '${systemPollerName}' to ${CONFIG_CLASSES.SYSTEM_CLASS_NAME}`); + telemetrySystems[systemPollerName] = createSystemFromSystemPoller( + systemPollerName, telemetrySystemPollers[systemPollerName] + ); + }); + if (Object.keys(telemetrySystems).length) { + originalConfig[CONFIG_CLASSES.SYSTEM_CLASS_NAME] = telemetrySystems; + } +} + +/** + * Normalize configuration and expand all references. + * + * @param {object} originalConfig - original config, should be copied by caller + * + * @returns {Object} normalized configuration with expanded references + */ +module.exports = function (originalConfig) { + /** + * Assume that originalConfig is valid declaration. + * originalConfig should look like following: + * { + * 'classNameA': { + * 'objectA': {}, + * 'objectB': {} + * }, + * ... + * 'classNameZ': { + * 'objectY': {}, + * 'objectZ': {} + * } + * } + */ + // TODO: add normalization for Telemetry_iHealth_Poller and Telemetry_Listener classes + normalizeTelemetrySystems(originalConfig); + normalizeTelemetrySystemPollers(originalConfig); + verifyAllowSelfSignedCert(originalConfig); + normalizeTelemetryEndpoints(originalConfig); + return originalConfig; +}; diff --git a/src/lib/normalizeUtil.js b/src/lib/normalizeUtil.js index c46284ee..40a7f1f6 100644 --- a/src/lib/normalizeUtil.js +++ b/src/lib/normalizeUtil.js @@ -8,11 +8,32 @@ 'use strict'; -const logger = require('./logger.js'); // eslint-disable-line no-unused-vars -const util = require('./util.js'); +const logger = require('./logger'); // eslint-disable-line no-unused-vars +const util = require('./util'); +const constants = require('./constants'); module.exports = { + /** + * Format MAC address + * + * @param {String} mac - MAC address + * + * @returns {String} formatted MAC address + */ + _formatMACAddress(mac) { + // expect ':' in mac addr - aa:b:cc:d:ee:f + if (mac.indexOf(':') === -1) { + return mac; + } + return mac.split(':').map((item) => { + item = item.toUpperCase(); + if (item.length === 1) { + item = `0${item}`; + } + return item; + }).join(':'); + }, /** * Convert array to map using provided options @@ -186,6 +207,48 @@ module.exports = { return data; }, + /** + * Restructure host-info to collect CPU statistics for the host(s) and cpu(s) + * that match the provided key pattern. + * This function depends upon the exact output from the host-info endpoint, and will requires + * that every object key is unique. + * + * @param {Object} args - args object + * @param {Object} [args.data] - data to process (always included) + * @param {Object} [args.keyPattern] - pattern used to traverse object keys + * + * @returns {Object} Returns matching sub-properties + */ + restructureHostCpuInfo(args) { + if (!args.keyPattern) { + throw new Error('Argument keyPattern required'); + } + const data = args.data; + if (typeof data !== 'object') { + return data; + } + const keys = args.keyPattern.split(constants.STATS_KEY_SEP); + + const findMatches = (inputData) => { + if (keys.length === 0) { + return inputData; + } + const keyExp = new RegExp(keys.splice(0, 1)); + const matchedData = {}; + + Object.keys(inputData).forEach((dataItem) => { + if (keyExp.test(dataItem)) { + // Capture ALL sub-properties if property matches, instead of iterating over object keys + // Will overwrite matching keys in 'matchedData' - assumption is that *EVERY* key is unique + Object.assign(matchedData, inputData[dataItem]); + } + }); + return findMatches(matchedData); + }; + const result = findMatches(data); + return Object.keys(result).length === 0 ? 'missing data' : result; + }, + /** * Average values * @@ -196,15 +259,22 @@ module.exports = { * @returns {Object} Returns averaged value */ getAverage(args) { - if (!args.keyWithValue) { throw new Error('Argument keyWithValue required'); } + if (!args.keyWithValue) { + throw new Error('Argument keyWithValue required'); + } const data = args.data; + if (typeof data !== 'object') { + return data; + } const values = []; // for now assume in object, could also be provided an array and just average that Object.keys(data).forEach((k) => { const key = args.keyWithValue; // throw error if key is missing - if (!(key in data[k])) { throw new Error(`Expecting key: ${key} in object: ${util.stringify(data[k])}`); } + if (!(key in data[k])) { + throw new Error(`Expecting key: ${key} in object: ${util.stringify(data[k])}`); + } values.push(data[k][key]); }); const averageFunc = arr => Math.round(arr.reduce((a, b) => a + b, 0) / arr.length); @@ -267,17 +337,30 @@ module.exports = { /** * getPercentFromKeys * - * @param {Object} args - args object - * @param {Object} [args.data] - data to process (always included) - * @param {Object} [args.totalKey] - key containing total (max) value - * @param {Object} [args.partialKey] - key containing partial value, such as free or used - * @param {Object} [args.inverse] - inverse percentage + * @param {Object} args - args object + * @param {Object} [args.data] - data to process (always included) + * @param {Object} [args.totalKey] - key containing total (max) value + * @param {Object} [args.partialKey] - key containing partial value, such as free or used + * @param {Object} [args.inverse] - inverse percentage + * @param {Object} [args.nestedObjects] - whether or not to traverse sub-objects for keys * * @returns {Object} Returns calculated percentage */ getPercentFromKeys(args) { const data = args.data; + const accumulateSubKeys = (arg, dataKeys) => dataKeys + .map(key => data[key][arg]) + .reduce((acc, val) => acc + val); + + if (args.nestedObjects && typeof data === 'object') { + // Get object keys before modifying the data object + const dataKeys = Object.keys(data); + [args.partialKey, args.totalKey].forEach((arg) => { + data[arg] = accumulateSubKeys(arg, dataKeys); + }); + } + // this should result in a number between 0 and 100 (percentage) let ret = Math.round(data[args.partialKey] / data[args.totalKey] * 100); ret = args.inverse ? 100 - ret : ret; @@ -354,6 +437,25 @@ module.exports = { return newRules; }, + /** + * Convert map to array using provided options + * + * @param {Object} data - data + * + * @returns {Object} Converted data + */ + convertMapToArray(data) { + const ret = []; + data = data.data; + + if (typeof data !== 'object') { + throw new Error(`convertMapToArray() object required: ${util.stringify(data)}`); + } + + Object.keys(data).forEach(key => ret.push(data[key])); + return ret; + }, + /** * restructureGslbPool * @@ -415,6 +517,114 @@ module.exports = { delete item.poolsCname; }); + return data; + }, + + /** + * Normalize MAC Address - upper case and etc. + * + * @param {Object} args - args object + * @param {Object} [args.data] - data to process (always included) + * @param {Array.} [args.properties] - list of properties to format + * + * @returns {Object} Returns formatted data + */ + normalizeMACAddress(args) { + let data = args.data; + if (data) { + if (typeof args.properties === 'undefined') { + data = this._formatMACAddress(data); + } else { + const properties = args.properties; + const stack = [data]; + let obj; + + const forKey = (key) => { + const val = obj[key]; + if (typeof val === 'object') { + if (val !== null) { + stack.push(val); + } + } else if (properties.indexOf(key) !== -1 && typeof val === 'string') { + obj[key] = this._formatMACAddress(val); + } + }; + + while (stack.length) { + obj = stack[0]; + Object.keys(obj).forEach(forKey); + stack.shift(); + } + } + } + return data; + }, + + /** + * Restructure Virtual Server Profiles + * + * @param {Object} args - args object + * @param {Object} [args.data] - data to process (always included) + * + * @returns {Object} Returns formatted data + */ + restructureVirtualServerProfiles(args) { + /** + * Possible issues: + * profiles: { + * name: 'profiles', <---- should be removed + * items: { <---- should be removed + * name: 'items', <---- should be removed + * profile1: { <---- should be moved one level up + * name: 'profile1', + * ..... + * } + * } + * } + */ + const data = args.data; + if (data) { + Object.keys(data).forEach((vsName) => { + const vsObj = data[vsName]; + if (vsObj.profiles) { + const profiles = vsObj.profiles; + delete profiles.name; + + if (profiles.items) { + delete profiles.items.name; + + Object.keys(profiles.items).forEach((profileName) => { + profiles[profileName] = profiles.items[profileName]; + }); + delete profiles.items; + } + } + }); + } + return data; + }, + + /** + * Get value by key/path + * + * @param {Object} args - args object + * @param {Object} [args.data] - data to process (always included) + * @param {Array} [args.path] - path to fetch data from + * + * @returns {Object} Returns value that belongs to key/path + */ + getValue(args) { + let data = args.data; + if (data && args.path) { + args.path.every((key) => { + data = data[key]; + if (typeof data === 'undefined') { + data = 'missing data'; + return false; + } + return true; + }); + } return data; } }; diff --git a/src/lib/paths.json b/src/lib/paths.json index cb1ef2ec..bb783af2 100644 --- a/src/lib/paths.json +++ b/src/lib/paths.json @@ -1,209 +1,226 @@ { "endpoints": [ { - "endpoint": "/mgmt/tm/sys/global-settings" + "path": "/mgmt/tm/sys/global-settings" }, { - "endpoint": "/mgmt/tm/cm/device" + "path": "/mgmt/tm/cm/device" }, { - "endpoint": "/mgmt/tm/sys/hardware" + "path": "/mgmt/tm/sys/hardware" }, { - "endpoint": "/mgmt/tm/sys/version" + "path": "/mgmt/tm/sys/version" }, { - "endpoint": "/mgmt/tm/sys/ready" + "path": "/mgmt/tm/sys/ready" }, { - "endpoint": "/mgmt/tm/cm/sync-status" + "path": "/mgmt/tm/cm/sync-status" }, { - "endpoint": "/mgmt/tm/cm/failover-status" + "path": "/mgmt/tm/cm/failover-status" }, { - "endpoint": "/mgmt/tm/sys/clock" + "path": "/mgmt/tm/sys/clock" }, { - "endpoint": "/mgmt/tm/sys/host-info" + "path": "/mgmt/tm/sys/host-info" }, { - "endpoint": "/mgmt/tm/sys/memory" + "path": "/mgmt/tm/sys/memory" }, { - "endpoint": "/mgmt/tm/sys/management-ip" + "path": "/mgmt/tm/sys/management-ip" }, { "name": "provisioning", - "endpoint": "/mgmt/tm/sys/provision" + "path": "/mgmt/tm/sys/provision" }, { "name": "networkInterfaces", - "endpoint": "/mgmt/tm/net/interface/stats" + "path": "/mgmt/tm/net/interface/stats" }, { "name": "networkTunnels", - "endpoint": "/mgmt/tm/net/tunnels/tunnel/stats" + "path": "/mgmt/tm/net/tunnels/tunnel/stats" }, { "name": "tmmInfo", - "endpoint": "/mgmt/tm/sys/tmm-info" + "path": "/mgmt/tm/sys/tmm-info" }, { "name": "tmmTraffic", - "endpoint": "/mgmt/tm/sys/tmm-traffic" + "path": "/mgmt/tm/sys/tmm-traffic" }, { "name": "aWideIps", - "endpoint": "/mgmt/tm/gtm/wideip/a", + "path": "/mgmt/tm/gtm/wideip/a", "includeStats": true }, { "name": "aaaaWideIps", - "endpoint": "/mgmt/tm/gtm/wideip/aaaa", + "path": "/mgmt/tm/gtm/wideip/aaaa", "includeStats": true }, { "name": "cnameWideIps", - "endpoint": "/mgmt/tm/gtm/wideip/cname", + "path": "/mgmt/tm/gtm/wideip/cname", "includeStats": true }, { "name": "mxWideIps", - "endpoint": "/mgmt/tm/gtm/wideip/mx", + "path": "/mgmt/tm/gtm/wideip/mx", "includeStats": true }, { "name": "naptrWideIps", - "endpoint": "/mgmt/tm/gtm/wideip/naptr", + "path": "/mgmt/tm/gtm/wideip/naptr", "includeStats": true }, { "name": "srvWideIps", - "endpoint": "/mgmt/tm/gtm/wideip/srv", + "path": "/mgmt/tm/gtm/wideip/srv", "includeStats": true }, { "name": "aPools", - "endpoint": "/mgmt/tm/gtm/pool/a", + "path": "/mgmt/tm/gtm/pool/a", "includeStats": true, "expandReferences": { "membersReference": { "includeStats": true } } }, { "name": "aaaaPools", - "endpoint": "/mgmt/tm/gtm/pool/aaaa", + "path": "/mgmt/tm/gtm/pool/aaaa", "includeStats": true, "expandReferences": { "membersReference": { "includeStats": true } } }, { "name": "cnamePools", - "endpoint": "/mgmt/tm/gtm/pool/cname", + "path": "/mgmt/tm/gtm/pool/cname", "includeStats": true, "expandReferences": { "membersReference": { "includeStats": true } } }, { "name": "mxPools", - "endpoint": "/mgmt/tm/gtm/pool/mx", + "path": "/mgmt/tm/gtm/pool/mx", "includeStats": true, "expandReferences": { "membersReference": { "includeStats": true } } }, { "name": "naptrPools", - "endpoint": "/mgmt/tm/gtm/pool/naptr", + "path": "/mgmt/tm/gtm/pool/naptr", "includeStats": true, "expandReferences": { "membersReference": { "includeStats": true } } }, { "name": "srvPools", - "endpoint": "/mgmt/tm/gtm/pool/srv", + "path": "/mgmt/tm/gtm/pool/srv", "includeStats": true, "expandReferences": { "membersReference": { "includeStats": true } } }, { "name": "virtualServers", - "endpoint": "/mgmt/tm/ltm/virtual", + "path": "/mgmt/tm/ltm/virtual", "includeStats": true, - "endpointFields": [ "name", "fullPath", "selfLink", "appService", "ipProtocol", "mask", "pool" ] + "endpointFields": [ "name", "fullPath", "selfLink", "appService", "ipProtocol", "mask", "pool", "profilesReference" ], + "expandReferences": { "profilesReference": { "endpointSuffix": "?$select=name,fullPath" } } }, { "name": "pools", - "endpoint": "/mgmt/tm/ltm/pool", + "path": "/mgmt/tm/ltm/pool", "includeStats": true, "expandReferences": { "membersReference": { "endpointSuffix": "/stats" } } }, { "name": "ltmPolicies", - "endpoint": "/mgmt/tm/ltm/policy/stats" + "path": "/mgmt/tm/ltm/policy/stats" }, { "name": "sslCerts", - "endpoint": "/mgmt/tm/sys/file/ssl-cert" + "path": "/mgmt/tm/sys/file/ssl-cert" }, { "name": "diskStorage", - "endpoint": "/mgmt/tm/util/bash", - "body": "{ \"command\": \"run\", \"utilCmdArgs\": \"-c \\\"/bin/df -P | /usr/bin/tr -s ' ' ','\\\"\" }" + "path": "/mgmt/tm/util/bash", + "ignoreCached": true, + "body": { + "command": "run", + "utilCmdArgs": "-c \"/bin/df -P | /usr/bin/tr -s ' ' ','\"" + } }, { "name": "diskLatency", - "endpoint": "/mgmt/tm/util/bash", - "body": "{ \"command\": \"run\", \"utilCmdArgs\": \"-c \\\"/usr/bin/iostat -x -d | /usr/bin/tail -n +3 | /usr/bin/tr -s ' ' ','\\\"\" }" + "path": "/mgmt/tm/util/bash", + "ignoreCached": true, + "body": { + "command": "run", + "utilCmdArgs": "-c \"/usr/bin/iostat -x -d | /usr/bin/tail -n +3 | /usr/bin/tr -s ' ' ','\"" + } }, { "name": "httpProfiles", - "endpoint": "/mgmt/tm/ltm/profile/http/stats" + "path": "/mgmt/tm/ltm/profile/http/stats" }, { "name": "clientSslProfiles", - "endpoint": "/mgmt/tm/ltm/profile/client-ssl/stats" + "path": "/mgmt/tm/ltm/profile/client-ssl/stats" }, { "name": "serverSslProfiles", - "endpoint": "/mgmt/tm/ltm/profile/server-ssl/stats" + "path": "/mgmt/tm/ltm/profile/server-ssl/stats" }, { "name": "deviceGroups", - "endpoint": "/mgmt/tm/cm/device-group", + "path": "/mgmt/tm/cm/device-group", "includeStats": true }, { "name": "asmQuery", - "endpoint": "/mgmt/tm/util/bash", - "body": "{ \"command\": \"run\", \"utilCmdArgs\": \"-c \\\"/bin/mysql -uroot -p$(/bin/perl -MPassCrypt -nle 'print PassCrypt::decrypt_password($_)' /var/db/mysqlpw) PLC -B -e 'select CASE WHEN max(event_time) IS NOT NULL THEN \\\\\\\"Pending Policy Changes\\\\\\\" ELSE \\\\\\\"Policies Consistent\\\\\\\" END as asm_state, max(event_time) as last_asm_change from PL_CONFIG_LOG where event_type <> 2 and element_type <> 18 and event_time > (select max(from_date) as asm_last_changed from PL_POLICY_HISTORY)' | sed 's/\\t/,/'\\\"\"}" + "path": "/mgmt/tm/util/bash", + "ignoreCached": true, + "body": { + "command": "run", + "utilCmdArgs": "-c \"/bin/mysql -uroot -p$(/bin/perl -MPassCrypt -nle 'print PassCrypt::decrypt_password($_)' /var/db/mysqlpw) PLC -B -e 'select CASE WHEN max(event_time) IS NOT NULL THEN \\\"Pending Policy Changes\\\" ELSE \\\"Policies Consistent\\\" END as asm_state, max(event_time) as last_asm_change from PL_CONFIG_LOG where event_type <> 2 and element_type <> 18 and event_time > (select max(from_date) as asm_last_changed from PL_POLICY_HISTORY)' | sed 's/\\t/,/'\"" + } }, { "name": "apmState", - "endpoint": "/mgmt/tm/util/bash", - "body": "{ \"command\": \"run\", \"utilCmdArgs\": \"-c \\\"/bin/unbuffer /usr/bin/guishell -c \\\\\\\"select case when max(config_sync_state) >= 0 then case when max(config_sync_state) > 0 then 'Pending Policy Changes' else 'Policies Consistent' end end from profile_access_misc_stat;\\\\\\\" | tr '\\n' ' ' | sed -r 's/.*\\\\|\\\\s*\\\\|.*\\\\| ([^|]*) \\\\|.*/apm_state\\\\n\\\\1/'\\\"\"}" + "path": "/mgmt/tm/util/bash", + "ignoreCached": true, + "body": { + "command": "run", + "utilCmdArgs": "-c \"/bin/unbuffer /usr/bin/guishell -c \\\"select max(config_sync_state) from profile_access_misc_stat;\\\" | tr '\\n' ' ' | sed -r 's/.*\\\\|\\\\s*\\\\|.*\\\\|\\\\s*([^|]*)\\\\s*\\\\|.*/apm_state\\n\\\\1/'\"" + } }, { "name": "firewallCurrentState", - "endpoint": "/mgmt/tm/security/firewall/current-state/stats" + "path": "/mgmt/tm/security/firewall/current-state/stats" }, { "name": "ltmConfigTime", - "endpoint": "/mgmt/tm/sys/db/ltm.configtime" + "path": "/mgmt/tm/sys/db/ltm.configtime" }, { "name": "gtmConfigTime", - "endpoint": "/mgmt/tm/sys/db/gtm.configtime" + "path": "/mgmt/tm/sys/db/gtm.configtime" }, { "name": "iRules", - "endpoint": "/mgmt/tm/ltm/rule/stats" + "path": "/mgmt/tm/ltm/rule/stats" }, { "name": "tmctl", - "endpoint": "/mgmt/tm/util/bash", + "path": "/mgmt/tm/util/bash", "ignoreCached": true, "body": { "command": "run", - "utilCmdArgs": "-c '/bin/tmctl $tmctlArgs'" + "utilCmdArgs": "-c 'tmctl $tmctlArgs'" } }, { "name": "deviceInfo", - "endpoint": "/mgmt/shared/identified-devices/config/device-info" + "path": "/mgmt/shared/identified-devices/config/device-info" } ] } diff --git a/src/lib/persistentStorage.js b/src/lib/persistentStorage.js index abb9e067..e50cec0b 100644 --- a/src/lib/persistentStorage.js +++ b/src/lib/persistentStorage.js @@ -8,7 +8,8 @@ 'use strict'; -const logger = require('./logger.js'); +const logger = require('./logger'); +const util = require('./util'); /** @module persistentStorage */ @@ -26,7 +27,7 @@ const logger = require('./logger.js'); * * @param {String} key - key to be searched in the storage * - * @returns {Promise.} Promise resolved with data + * @returns {Promise.} Promise resolved with copy data */ /** * Set data to the specified key @@ -38,7 +39,7 @@ const logger = require('./logger.js'); * @param {String} key - key to be used * @param {} data - data to be set to the key * - * @returns {Promise} Promise resolved when data saved to the storage + * @returns {Promise} Promise resolved when copy data saved to the storage */ /** * Remove data by the specified key @@ -87,11 +88,20 @@ function PersistentStorageProxy(storage) { /** @inheritdoc */ PersistentStorageProxy.prototype.get = function (key) { - return this.storage.get(key); + return this.storage.get(key) + .then((value) => { + if (typeof value === 'object') { + value = util.deepCopy(value); + } + return Promise.resolve(value); + }); }; /** @inheritdoc */ PersistentStorageProxy.prototype.set = function (key, data) { + if (typeof data === 'object') { + data = util.deepCopy(data); + } return this.storage.set(key, data); }; @@ -186,7 +196,7 @@ RestStorage.prototype._load = function () { loadPromise = loadPromise.then(() => this._unsafeLoad()) .then((state) => { this._loadPromise = null; - this._cache = this._validateLoadedState(state || {}); + this._cache = this._validateLoadedState(state || this._getBaseState()); loadPromise.loadResults = this._cache._data_; logger.debug('RestStorage.load: application state loaded'); }) diff --git a/src/lib/properties.json b/src/lib/properties.json index 561cfc34..7f71343a 100644 --- a/src/lib/properties.json +++ b/src/lib/properties.json @@ -31,61 +31,63 @@ }, "version": { "structure": { "parentKey": "system" }, - "key": "/mgmt/tm/sys/version::sys/version/0::Version" + "key": "deviceInfo::version" }, "versionBuild": { "structure": { "parentKey": "system" }, - "key": "/mgmt/tm/sys/version::sys/version/0::Build" + "key": "deviceInfo::build" }, "location": { "structure": { "parentKey": "system" }, - "key": "/mgmt/tm/cm/device::items::{{HOSTNAME}}::location", + "key": "/mgmt/tm/cm/device::items", "normalization": [ { - "convertArrayToMap": { "keyName": "name" } + "runFunctions": [{ "name": "normalizeMACAddress", "args": { "properties": ["baseMac"] } } ] + }, + { + "convertArrayToMap": { "keyName": "baseMac" }, "useCurrentData": true, "keepKey": true + }, + { + "runFunctions": [{ "name": "getValue", "args": { "path": ["{{ BASE_MAC_ADDR }}", "location" ] } } ] } ] }, "description": { "structure": { "parentKey": "system" }, - "key": "/mgmt/tm/cm/device::items::{{HOSTNAME}}::description", + "key": "/mgmt/tm/cm/device::items", "normalization": [ { - "convertArrayToMap": { "keyName": "name" } + "runFunctions": [{ "name": "normalizeMACAddress", "args": { "properties": ["baseMac"] } } ] + }, + { + "convertArrayToMap": { "keyName": "baseMac" }, "useCurrentData": true, "keepKey": true + }, + { + "runFunctions": [{ "name": "getValue", "args": { "path": ["{{ BASE_MAC_ADDR }}", "description" ] } } ] } ] }, "marketingName": { "structure": { "parentKey": "system" }, - "key": "/mgmt/tm/cm/device::items::{{HOSTNAME}}::marketingName", - "normalization": [ - { - "convertArrayToMap": { "keyName": "name" } - } - ] + "key": "deviceInfo::platformMarketingName" }, "platformId": { "structure": { "parentKey": "system" }, - "key": "/mgmt/tm/cm/device::items::{{HOSTNAME}}::platformId", - "normalization": [ - { - "convertArrayToMap": { "keyName": "name" } - } - ] + "key": "deviceInfo::platform" }, "chassisId": { "structure": { "parentKey": "system" }, - "key": "/mgmt/tm/cm/device::items::{{HOSTNAME}}::chassisId", + "key": "deviceInfo::chassisSerialNumber" + }, + "baseMac": { + "structure": { "parentKey": "system" }, + "key": "deviceInfo::baseMac", "normalization": [ { - "convertArrayToMap": { "keyName": "name" } + "runFunctions": [{ "name": "normalizeMACAddress" }] } ] }, - "baseMac": { - "structure": { "parentKey": "system" }, - "key": "/mgmt/tm/sys/hardware::sys/hardware/platform::sys/hardware/platform/0::baseMac" - }, "callBackUrl": { "structure": { "parentKey": "system" }, "key": "/mgmt/tm/sys/management-ip::items", @@ -164,20 +166,25 @@ }, "cpu": { "structure": { "parentKey": "system" }, - "key": "/mgmt/tm/sys/host-info::sys/host-info/0::sys/hostInfo/0/cpuInfo", + "key": "/mgmt/tm/sys/host-info", "normalization": [ { - "runFunctions": [{ "name": "getAverage", "args": { "keyWithValue": "oneMinAvgSystem" } }] + "runFunctions": [ + { "name": "restructureHostCpuInfo", "args": { "keyPattern": "^sys/host-info/\\d+::^sys/hostInfo/\\d+/cpuInfo" } }, + { "name": "getAverage", "args": { "keyWithValue": "oneMinAvgSystem" } } + ] } ], "comment": "also oneMinAvgUser, need to determine how that should be factored in" }, "memory": { "structure": { "parentKey": "system" }, - "key": "/mgmt/tm/sys/memory::sys/memory/memory-host::sys/memory/memory-host/0", + "key": "/mgmt/tm/sys/memory::sys/memory/memory-host", "normalization": [ { - "runFunctions": [{ "name": "getPercentFromKeys", "args": { "totalKey": "memoryTotal", "partialKey": "memoryUsed" } }] + "runFunctions": [ + { "name": "getPercentFromKeys", "args": { "totalKey": "memoryTotal", "partialKey": "memoryUsed", "nestedObjects": true } } + ] } ] }, @@ -192,10 +199,12 @@ }, "tmmMemory": { "structure": { "parentKey": "system" }, - "key": "/mgmt/tm/sys/memory::sys/memory/memory-host::sys/memory/memory-host/0", + "key": "/mgmt/tm/sys/memory::sys/memory/memory-host", "normalization": [ { - "runFunctions": [{ "name": "getPercentFromKeys", "args": { "totalKey": "tmmMemoryTotal", "partialKey": "tmmMemoryUsed" } }] + "runFunctions": [ + { "name": "getPercentFromKeys", "args": { "totalKey": "tmmMemoryTotal", "partialKey": "tmmMemoryUsed", "nestedObjects": true } } + ] } ] }, @@ -260,6 +269,124 @@ } ] }, + "asmState": { + "structure": { "parentKey": "system" }, + "if": { + "isModuleProvisioned": "asm" + }, + "then": { + "key": "asmQuery::commandResult", + "normalization": [ + { + "runFunctions": [ + { "name": "formatAsJson", "args": { "type": "csv", "mapKey": "asm_state" } }, + { "name": "getFirstKey" } + ] + } + ] + }, + "else": { + "disabled": true + } + }, + "lastAsmChange": { + "structure": { "parentKey": "system" }, + "if": { + "isModuleProvisioned": "asm" + }, + "then": { + "key": "asmQuery::commandResult", + "normalization": [ + { + "runFunctions": [ + { "name": "formatAsJson", "args": { "type": "csv", "mapKey": "last_asm_change", "renameKeys": { "patterns": { "NULL": { "constant": "" } } } } }, + { "name": "getFirstKey" } + ] + } + ] + }, + "else": { + "disabled": true + } + }, + "apmState": { + "structure": { "parentKey": "system" }, + "if": { + "isModuleProvisioned": "apm" + }, + "then": { + "key": "apmState::commandResult", + "normalization": [ + { + "runFunctions": [ + { "name": "formatAsJson", "args": { "type": "csv", "mapKey": "apm_state", "renameKeys": { "patterns": { "NULL": { "constant": "" } } } } }, + { "name": "getFirstKey" } + ] + } + ] + }, + "else": { + "disabled": true + } + }, + "afmState": { + "structure": { "parentKey": "system" }, + "if": { + "isModuleProvisioned": "afm" + }, + "then": { + "key": "firewallCurrentState::pccdStatus", + "normalization": [ + { + "includeFirstEntry": { "pattern": "/stats" } + } + ] + }, + "else": { + "disabled": true + } + }, + "lastAfmDeploy": { + "structure": { "parentKey": "system" }, + "if": { + "isModuleProvisioned": "afm" + }, + "then": { + "key": "firewallCurrentState::ruleDeployEndTimeFmt", + "normalization": [ + { + "includeFirstEntry": { "pattern": "/stats" } + } + ] + }, + "else": { + "disabled": true + } + }, + "ltmConfigTime": { + "structure": { "parentKey": "system" }, + "if": { + "isModuleProvisioned": "ltm" + }, + "then": { + "key": "ltmConfigTime::value" + }, + "else": { + "disabled": true + } + }, + "gtmConfigTime": { + "structure": { "parentKey": "system" }, + "if": { + "isModuleProvisioned": "gtm" + }, + "then": { + "key": "gtmConfigTime::value" + }, + "else": { + "disabled": true + } + }, "aWideIps": { "if": { "isModuleProvisioned": "gtm" @@ -609,10 +736,15 @@ "filterKeys": { "exclude": [ "fullPath", "generation", "appServiceReference", "tmName", "status.statusReason", "cmpEnabled", "cmpEnableMode", "csMaxConnDur", "csMeanConnDur", "csMinConnDur", "totRequests", "oneMinAvgUsageRatio", "clientside.pktsIn", "clientside.pktsOut", "clientside.evictedConns", "clientside.slowKilled", "clientside.maxConns", "clientside.totConns", "ephemeral.bitsIn", "ephemeral.bitsOut", "ephemeral.curConns", "ephemeral.evictedConns", "ephemeral.maxConns", "ephemeral.pktsIn", "ephemeral.pktsOut", "ephemeral.slowKilled", "ephemeral.totConns", "fiveSecAvgUsageRatio", "fiveMinAvgUsageRatio", "syncookieStatus", "syncookie.accepts", "syncookie.hwAccepts", "syncookie.hwSyncookies", "syncookie.rejects", "syncookie.hwsyncookieInstance", "syncookie.swsyncookieInstance", "syncookie.syncacheCurr", "syncookie.syncacheOver", "syncookie.syncookies", "syncookie.syncacheOver", "poolReference" ] } }, { - "renameKeys": { "patterns": { "name/": { "pattern": "name\/(.*)", "group": 1 }, "ltm/virtual": { "pattern": "virtual\/(.*)\/", "group": 1 } } } + "renameKeys": { "patterns": { "name/": { "pattern": "name\/(.*)", "group": 1 }, "ltm/virtual": { "pattern": "virtual\/(.*)\/", "group": 1 }, "profilesReference": "profiles" } } }, { "addKeysByTag": true + }, + { + "runFunctions": [ + { "name": "restructureVirtualServerProfiles" } + ] } ] }, @@ -740,124 +872,6 @@ } ] }, - "asmState": { - "structure": { "parentKey": "system" }, - "if": { - "isModuleProvisioned": "asm" - }, - "then": { - "key": "asmQuery::commandResult", - "normalization": [ - { - "runFunctions": [ - { "name": "formatAsJson", "args": { "type": "csv", "mapKey": "asm_state" } }, - { "name": "getFirstKey" } - ] - } - ] - }, - "else": { - "disabled": true - } - }, - "lastAsmChange": { - "structure": { "parentKey": "system" }, - "if": { - "isModuleProvisioned": "asm" - }, - "then": { - "key": "asmQuery::commandResult", - "normalization": [ - { - "runFunctions": [ - { "name": "formatAsJson", "args": { "type": "csv", "mapKey": "last_asm_change", "renameKeys": { "patterns": { "NULL": { "constant": "" } } } } }, - { "name": "getFirstKey" } - ] - } - ] - }, - "else": { - "disabled": true - } - }, - "apmState": { - "structure": { "parentKey": "system" }, - "if": { - "isModuleProvisioned": "apm" - }, - "then": { - "key": "apmState::commandResult", - "normalization": [ - { - "runFunctions": [ - { "name": "formatAsJson", "args": { "type": "csv", "mapKey": "apm_state", "renameKeys": { "patterns": { "NULL": { "constant": "" } } } } }, - { "name": "getFirstKey" } - ] - } - ] - }, - "else": { - "disabled": true - } - }, - "afmState": { - "structure": { "parentKey": "system" }, - "if": { - "isModuleProvisioned": "afm" - }, - "then": { - "key": "firewallCurrentState::pccdStatus", - "normalization": [ - { - "includeFirstEntry": { "pattern": "/stats" } - } - ] - }, - "else": { - "disabled": true - } - }, - "lastAfmDeploy": { - "structure": { "parentKey": "system" }, - "if": { - "isModuleProvisioned": "afm" - }, - "then": { - "key": "firewallCurrentState::ruleDeployEndTimeFmt", - "normalization": [ - { - "includeFirstEntry": { "pattern": "/stats" } - } - ] - }, - "else": { - "disabled": true - } - }, - "ltmConfigTime": { - "structure": { "parentKey": "system" }, - "if": { - "isModuleProvisioned": "ltm" - }, - "then": { - "key": "ltmConfigTime::value" - }, - "else": { - "disabled": true - } - }, - "gtmConfigTime": { - "structure": { "parentKey": "system" }, - "if": { - "isModuleProvisioned": "gtm" - }, - "then": { - "key": "gtmConfigTime::value" - }, - "else": { - "disabled": true - } - }, "iRules": { "key": "iRules", "normalization": [ @@ -886,7 +900,13 @@ "keyArgs": { "replaceStrings": { "\\$tmctlArgs": "-c asm_cpu_util_stats" } }, "normalization": [ { - "runFunctions": [{ "name": "formatAsJson", "args": { "type": "csv" } }] + "runFunctions": [{ "name": "formatAsJson", "args": { "type": "csv", "mapKey": "vs_name" } }] + }, + { + "addKeysByTag": { "tags": { "tenant": "`T`", "application": "`A`" } } + }, + { + "runFunctions": [{ "name": "convertMapToArray" }] } ] }, @@ -936,7 +956,13 @@ "keyArgs": { "replaceStrings": { "\\$tmctlArgs": "-c dos_stat" } }, "normalization": [ { - "runFunctions": [{ "name": "formatAsJson", "args": { "type": "csv" } }] + "runFunctions": [{ "name": "formatAsJson", "args": { "type": "csv", "mapKey": "vs_name" } }] + }, + { + "addKeysByTag": { "tags": { "tenant": "`T`", "application": "`A`" } } + }, + { + "runFunctions": [{ "name": "convertMapToArray" }] } ] }, @@ -966,7 +992,13 @@ "keyArgs": { "replaceStrings": { "\\$tmctlArgs": "-c flow_eviction_policy_stat" } }, "normalization": [ { - "runFunctions": [{ "name": "formatAsJson", "args": { "type": "csv" } }] + "runFunctions": [{ "name": "formatAsJson", "args": { "type": "csv", "mapKey": "context_name" } }] + }, + { + "addKeysByTag": { "tags": { "tenant": "`T`", "application": "`A`" } } + }, + { + "runFunctions": [{ "name": "convertMapToArray" }] } ] }, @@ -1126,7 +1158,13 @@ "keyArgs": { "replaceStrings": { "\\$tmctlArgs": "-c pool_member_stat" } }, "normalization": [ { - "runFunctions": [{ "name": "formatAsJson", "args": { "type": "csv" } }] + "runFunctions": [{ "name": "formatAsJson", "args": { "type": "csv", "mapKey": "pool_name" } }] + }, + { + "addKeysByTag": { "tags": { "tenant": "`T`", "application": "`A`" } } + }, + { + "runFunctions": [{ "name": "convertMapToArray" }] } ] }, @@ -1156,7 +1194,13 @@ "keyArgs": { "replaceStrings": { "\\$tmctlArgs": "-c profile_bigproto_stat" } }, "normalization": [ { - "runFunctions": [{ "name": "formatAsJson", "args": { "type": "csv" } }] + "runFunctions": [{ "name": "formatAsJson", "args": { "type": "csv", "mapKey": "vs_name" } }] + }, + { + "addKeysByTag": { "tags": { "tenant": "`T`", "application": "`A`" } } + }, + { + "runFunctions": [{ "name": "convertMapToArray" }] } ] }, @@ -1166,7 +1210,13 @@ "keyArgs": { "replaceStrings": { "\\$tmctlArgs": "-c profile_clientssl_stat" } }, "normalization": [ { - "runFunctions": [{ "name": "formatAsJson", "args": { "type": "csv" } }] + "runFunctions": [{ "name": "formatAsJson", "args": { "type": "csv", "mapKey": "vs_name" } }] + }, + { + "addKeysByTag": { "tags": { "tenant": "`T`", "application": "`A`" } } + }, + { + "runFunctions": [{ "name": "convertMapToArray" }] } ] }, @@ -1176,7 +1226,13 @@ "keyArgs": { "replaceStrings": { "\\$tmctlArgs": "-c profile_connpool_stat" } }, "normalization": [ { - "runFunctions": [{ "name": "formatAsJson", "args": { "type": "csv" } }] + "runFunctions": [{ "name": "formatAsJson", "args": { "type": "csv", "mapKey": "vs_name" } }] + }, + { + "addKeysByTag": { "tags": { "tenant": "`T`", "application": "`A`" } } + }, + { + "runFunctions": [{ "name": "convertMapToArray" }] } ] }, @@ -1186,7 +1242,13 @@ "keyArgs": { "replaceStrings": { "\\$tmctlArgs": "-c profile_dns_stat" } }, "normalization": [ { - "runFunctions": [{ "name": "formatAsJson", "args": { "type": "csv" } }] + "runFunctions": [{ "name": "formatAsJson", "args": { "type": "csv", "mapKey": "vs_name" } }] + }, + { + "addKeysByTag": { "tags": { "tenant": "`T`", "application": "`A`" } } + }, + { + "runFunctions": [{ "name": "convertMapToArray" }] } ] }, @@ -1196,7 +1258,13 @@ "keyArgs": { "replaceStrings": { "\\$tmctlArgs": "-c profile_ftp_stat" } }, "normalization": [ { - "runFunctions": [{ "name": "formatAsJson", "args": { "type": "csv" } }] + "runFunctions": [{ "name": "formatAsJson", "args": { "type": "csv", "mapKey": "vs_name" } }] + }, + { + "addKeysByTag": { "tags": { "tenant": "`T`", "application": "`A`" } } + }, + { + "runFunctions": [{ "name": "convertMapToArray" }] } ] }, @@ -1206,7 +1274,13 @@ "keyArgs": { "replaceStrings": { "\\$tmctlArgs": "-c profile_http_stat" } }, "normalization": [ { - "runFunctions": [{ "name": "formatAsJson", "args": { "type": "csv" } }] + "runFunctions": [{ "name": "formatAsJson", "args": { "type": "csv", "mapKey": "vs_name" } }] + }, + { + "addKeysByTag": { "tags": { "tenant": "`T`", "application": "`A`" } } + }, + { + "runFunctions": [{ "name": "convertMapToArray" }] } ] }, @@ -1216,7 +1290,13 @@ "keyArgs": { "replaceStrings": { "\\$tmctlArgs": "-c profile_httpcompression_stat" } }, "normalization": [ { - "runFunctions": [{ "name": "formatAsJson", "args": { "type": "csv" } }] + "runFunctions": [{ "name": "formatAsJson", "args": { "type": "csv", "mapKey": "vs_name" } }] + }, + { + "addKeysByTag": { "tags": { "tenant": "`T`", "application": "`A`" } } + }, + { + "runFunctions": [{ "name": "convertMapToArray" }] } ] }, @@ -1226,7 +1306,13 @@ "keyArgs": { "replaceStrings": { "\\$tmctlArgs": "-c profile_serverssl_stat" } }, "normalization": [ { - "runFunctions": [{ "name": "formatAsJson", "args": { "type": "csv" } }] + "runFunctions": [{ "name": "formatAsJson", "args": { "type": "csv", "mapKey": "vs_name" } }] + }, + { + "addKeysByTag": { "tags": { "tenant": "`T`", "application": "`A`" } } + }, + { + "runFunctions": [{ "name": "convertMapToArray" }] } ] }, @@ -1236,7 +1322,13 @@ "keyArgs": { "replaceStrings": { "\\$tmctlArgs": "-c profile_tcp_stat" } }, "normalization": [ { - "runFunctions": [{ "name": "formatAsJson", "args": { "type": "csv" } }] + "runFunctions": [{ "name": "formatAsJson", "args": { "type": "csv", "mapKey": "vs_name" } }] + }, + { + "addKeysByTag": { "tags": { "tenant": "`T`", "application": "`A`" } } + }, + { + "runFunctions": [{ "name": "convertMapToArray" }] } ] }, @@ -1246,7 +1338,13 @@ "keyArgs": { "replaceStrings": { "\\$tmctlArgs": "-c profile_udp_stat" } }, "normalization": [ { - "runFunctions": [{ "name": "formatAsJson", "args": { "type": "csv" } }] + "runFunctions": [{ "name": "formatAsJson", "args": { "type": "csv", "mapKey": "vs_name" } }] + }, + { + "addKeysByTag": { "tags": { "tenant": "`T`", "application": "`A`" } } + }, + { + "runFunctions": [{ "name": "convertMapToArray" }] } ] }, @@ -1256,7 +1354,13 @@ "keyArgs": { "replaceStrings": { "\\$tmctlArgs": "-c profile_webacceleration_stat" } }, "normalization": [ { - "runFunctions": [{ "name": "formatAsJson", "args": { "type": "csv" } }] + "runFunctions": [{ "name": "formatAsJson", "args": { "type": "csv", "mapKey": "vs_name" } }] + }, + { + "addKeysByTag": { "tags": { "tenant": "`T`", "application": "`A`" } } + }, + { + "runFunctions": [{ "name": "convertMapToArray" }] } ] }, @@ -1326,7 +1430,7 @@ "keyArgs": { "replaceStrings": { "\\$tmctlArgs": "-c virtual_server_conn_stat" } }, "normalization": [ { - "runFunctions": [{ "name": "formatAsJson", "args": { "type": "csv" } }] + "runFunctions": [{ "name": "formatAsJson", "args": { "type": "csv", "$comment": "can't dump table on 12.0+" } }] } ] }, @@ -1336,7 +1440,13 @@ "keyArgs": { "replaceStrings": { "\\$tmctlArgs": "-c virtual_server_cpu_stat" } }, "normalization": [ { - "runFunctions": [{ "name": "formatAsJson", "args": { "type": "csv" } }] + "runFunctions": [{ "name": "formatAsJson", "args": { "type": "csv", "mapKey": "name" } }] + }, + { + "addKeysByTag": { "tags": { "tenant": "`T`", "application": "`A`" } } + }, + { + "runFunctions": [{ "name": "convertMapToArray" }] } ] }, @@ -1346,7 +1456,13 @@ "keyArgs": { "replaceStrings": { "\\$tmctlArgs": "-c virtual_server_stat" } }, "normalization": [ { - "runFunctions": [{ "name": "formatAsJson", "args": { "type": "csv" } }] + "runFunctions": [{ "name": "formatAsJson", "args": { "type": "csv", "mapKey": "name" } }] + }, + { + "addKeysByTag": { "tags": { "tenant": "`T`", "application": "`A`" } } + }, + { + "runFunctions": [{ "name": "convertMapToArray" }] } ] } @@ -1357,7 +1473,10 @@ "AVR": { "keys": [ { "required": [ "EOCTimestamp" ], "optional": [ "AggrInterval", "Microtimestamp", "STAT_SRC", "Entity", "errdefs_msgno" ] } ] }, "ASM": { "keys": [ { "required": [ "policy_name" ], "optional": [ "policy_apply_date", "request_status" ] } ] }, "APM": { "keys": [ { "required": [ "Access_Profile" ] } ] }, - "AFM": { "keys": [ { "required": [ "acl_policy_name" ], "optional": [ "acl_policy_type", "acl_rule_name" ] } ] }, + "AFM": { "keys": [ + { "required": [ "acl_policy_name" ], "optional": [ "acl_policy_type", "acl_rule_name" ] }, + { "required": [ "dos_attack_id" ], "optional": [ "flow_id", "action", "errdefs_msg_name" ] } + ]}, "CGNAT": { "keys": [ { "required": [ "lsn_event" ], "optional": [ "lsn_client", "lsn_pb", "start" ] } ] } } }, @@ -1379,13 +1498,20 @@ }, "context": { "HOSTNAME": { - "key": "/mgmt/tm/sys/global-settings::hostname" + "key": "deviceInfo::hostname" + }, + "BASE_MAC_ADDR": { + "key": "deviceInfo::baseMac", + "normalization": [ + { + "runFunctions": [{ "name": "normalizeMACAddress" }] + } + ] }, "deviceVersion": { - "key": "/mgmt/tm/sys/version::sys/version/0::Version" + "key": "deviceInfo::version" }, "provisioning": { - "structure": { "parentKey": "system" }, "key": "provisioning::items", "normalization": [ { diff --git a/src/lib/systemPoller.js b/src/lib/systemPoller.js index df940db8..8f3b5246 100644 --- a/src/lib/systemPoller.js +++ b/src/lib/systemPoller.js @@ -8,20 +8,215 @@ 'use strict'; -const logger = require('./logger.js'); // eslint-disable-line no-unused-vars -const constants = require('./constants.js'); -const util = require('./util.js'); -const deviceUtil = require('./deviceUtil.js'); -const configWorker = require('./config.js'); -const SystemStats = require('./systemStats.js'); -const dataPipeline = require('./dataPipeline.js'); - -const SYSTEM_CLASS_NAME = constants.SYSTEM_CLASS_NAME; -const SYSTEM_POLLER_CLASS_NAME = constants.SYSTEM_POLLER_CLASS_NAME; -const pollerIDs = {}; +const constants = require('./constants'); +const configWorker = require('./config'); +const dataPipeline = require('./dataPipeline'); +const deviceUtil = require('./deviceUtil'); +const logger = require('./logger'); // eslint-disable-line no-unused-vars +const normalizeConfig = require('./normalizeConfig'); +const SystemStats = require('./systemStats'); +const util = require('./util'); /** @module systemPoller */ +const CONFIG_CLASSES = constants.CONFIG_CLASSES; +// use SYSTEM_POLLER_CLASS_NAME to keep compatibility with previous versions +// but it is possible use SYSTEM_CLASS_NAME instead too +const TRACER_CLASS_NAME = CONFIG_CLASSES.SYSTEM_POLLER_CLASS_NAME; +// key - poller name, value - timer ID +const POLLER_TIMERS = {}; + +function getPollerTimers() { + return POLLER_TIMERS; +} + +function getTelemetryObjects(originalConfig, className) { + return originalConfig[className] || {}; +} + +function getTelemetrySystems(originalConfig) { + return getTelemetryObjects(originalConfig, CONFIG_CLASSES.SYSTEM_CLASS_NAME); +} + +function getTelemetrySystemPollers(originalConfig) { + return getTelemetryObjects(originalConfig, CONFIG_CLASSES.SYSTEM_POLLER_CLASS_NAME); +} + +function getTelemetryConsumers(originalConfig) { + return getTelemetryObjects(originalConfig, CONFIG_CLASSES.CONSUMER_CLASS_NAME); +} + +function createCustomConfig(originalConfig, sysOrPollerName, pollerName) { + // originalConfig is not normalized yet + let systems = getTelemetrySystems(originalConfig); + let pollers = getTelemetrySystemPollers(originalConfig); + let system; + let poller; + + if (sysOrPollerName && pollerName) { + system = systems[sysOrPollerName]; + poller = pollers[pollerName]; + } else { + // each object has unique name across the entire declaration. + // so, one of them will be 'undefined' + system = systems[sysOrPollerName]; + poller = pollers[sysOrPollerName]; + } + + const systemFound = !util.isObjectEmpty(system); + const pollerFound = !util.isObjectEmpty(poller); + // check for errors at first + if (!systemFound || !pollerFound) { + if (pollerName) { + // sysOrPollerName and pollerName both passed to the function + if (!systemFound) { + throw new Error(`System with name '${sysOrPollerName}' doesn't exist`); + } + if (!pollerFound) { + throw new Error(`System Poller with name '${pollerName}' doesn't exist`); + } + } + if (!(systemFound || pollerFound)) { + throw new Error(`System or System Poller with name '${sysOrPollerName}' doesn't exist`); + } + if (systemFound && util.isObjectEmpty(system.systemPoller)) { + throw new Error(`System with name '${sysOrPollerName}' has no System Poller configured`); + } + } + // error check passed and now we have valid objects to continue with + if (systemFound && pollerFound) { + systems = { [sysOrPollerName]: system }; + pollers = { [pollerName]: poller }; + system.systemPoller = pollerName; + } else if (pollerFound) { + systems = {}; + pollers = { [sysOrPollerName]: poller }; + } else { + const newPollers = {}; + systems = { [sysOrPollerName]: system }; + + system.systemPoller = Array.isArray(system.systemPoller) ? system.systemPoller + : [system.systemPoller]; + + system.systemPoller.forEach((pollerVal) => { + if (typeof pollerVal === 'string') { + newPollers[pollerVal] = pollers[pollerVal]; + } + }); + pollers = newPollers; + } + originalConfig[CONFIG_CLASSES.SYSTEM_CLASS_NAME] = systems; + originalConfig[CONFIG_CLASSES.SYSTEM_POLLER_CLASS_NAME] = pollers; + return originalConfig; +} + +/** + * Compute trace's value from System and System Poller config + * + * @param {Boolean|String} [systemTrace] - system's trace config + * @param {Boolean|String} [pollerTrace] - poller's trace config + * + * @returns {Boolean|String} trace's value + */ +function getTraceValue(systemTrace, pollerTrace) { + if (typeof systemTrace === 'undefined' && typeof pollerTrace === 'undefined') { + pollerTrace = false; + } else { + // we know that one of the values is defined (or both) + // set default value to true to do not block tracer usage + systemTrace = typeof systemTrace === 'undefined' ? true : systemTrace; + pollerTrace = typeof pollerTrace === 'undefined' ? true : pollerTrace; + if (typeof pollerTrace === 'string') { + // preserve poller's value + pollerTrace = systemTrace && pollerTrace; + } else if (pollerTrace === true) { + // preserve system's value + pollerTrace = systemTrace; + } + } + return pollerTrace; +} + +function createPollerConfig(systemConfig, pollerConfig, fetchTMStats) { + return { + name: `${systemConfig.name}::${pollerConfig.name}`, + enable: Boolean(systemConfig.enable && pollerConfig.enable), + trace: module.exports.getTraceValue(systemConfig.trace, pollerConfig.trace), + interval: pollerConfig.interval, + connection: { + host: systemConfig.host, + port: systemConfig.port, + protocol: systemConfig.protocol, + allowSelfSignedCert: systemConfig.allowSelfSignedCert + }, + credentials: { + username: systemConfig.username, + passphrase: systemConfig.passphrase + }, + dataOpts: { + tags: pollerConfig.tag, + actions: pollerConfig.actions, + noTMStats: !fetchTMStats + }, + endpointList: pollerConfig.endpointList + }; +} + +function getEnabledPollerConfigs(systemObj, fetchTMStats, includeDisabled) { + const pollers = []; + if (systemObj.enable || includeDisabled) { + systemObj.systemPollers.forEach((pollerConfig) => { + if (pollerConfig.enable || includeDisabled) { + const newPollerConfig = module.exports.createPollerConfig(systemObj, pollerConfig, fetchTMStats); + pollers.push(newPollerConfig); + } + }); + } + return pollers; +} + +function hasSplunkLegacy(originalConfig) { + const consumers = getTelemetryConsumers(originalConfig); + return Object.keys(consumers).some(consumerKey => consumers[consumerKey].type === 'Splunk' + && consumers[consumerKey].format === 'legacy'); +} + +function applyConfig(originalConfig) { + const systems = getTelemetrySystems(originalConfig); + const fetchTMStats = hasSplunkLegacy(originalConfig); + const newPollerIDs = []; + const currPollerIDs = module.exports.getPollerTimers(); + + Object.keys(systems).forEach((systemName) => { + module.exports.getEnabledPollerConfigs(systems[systemName], fetchTMStats).forEach((pollerConfig) => { + newPollerIDs.push(pollerConfig.name); + pollerConfig.tracer = util.tracer.createFromConfig( + TRACER_CLASS_NAME, pollerConfig.name, pollerConfig + ); + const baseMsg = `system poller ${pollerConfig.name}. Interval = ${pollerConfig.interval} sec.`; + if (currPollerIDs[pollerConfig.name]) { + logger.info(`Updating ${baseMsg}`); + currPollerIDs[pollerConfig.name] = util.update( + currPollerIDs[pollerConfig.name], module.exports.safeProcess, pollerConfig, pollerConfig.interval + ); + } else { + logger.info(`Starting ${baseMsg}`); + currPollerIDs[pollerConfig.name] = util.start( + module.exports.safeProcess, pollerConfig, pollerConfig.interval + ); + } + }); + }); + + Object.keys(currPollerIDs).forEach((key) => { + if (newPollerIDs.indexOf(key) === -1) { + logger.info(`Disabling system poller ${key}`); + util.stop(currPollerIDs[key]); + delete currPollerIDs[key]; + } + }); +} + /** * Process system(s) stats * @@ -34,33 +229,34 @@ const pollerIDs = {}; * @returns {Promise} Promise which is resolved with data sent */ function process() { - const args = arguments[0]; + const config = arguments[0]; const options = arguments.length > 1 ? arguments[1] : {}; - - const config = args.config; - const tracer = args.tracer; + const tracer = config.tracer; const startTimestamp = new Date().toISOString(); logger.debug('System poller cycle started'); - const systemStats = new SystemStats(config.host, config.options); + const systemStats = new SystemStats(config); return systemStats.collect() .then((normalizedData) => { - const endTimeStamp = new Date().toISOString(); // inject service data const telemetryServiceInfo = { - pollingInterval: args.interval, + pollingInterval: config.interval, cycleStart: startTimestamp, - cycleEnd: endTimeStamp + cycleEnd: (new Date()).toISOString() }; normalizedData.telemetryServiceInfo = telemetryServiceInfo; normalizedData.telemetryEventCategory = constants.EVENT_TYPES.SYSTEM_POLLER; // end inject service data - const dataCtx = { data: normalizedData, type: constants.EVENT_TYPES.SYSTEM_POLLER }; + const dataCtx = { + data: normalizedData, + type: constants.EVENT_TYPES.SYSTEM_POLLER, + isCustom: systemStats.isCustom + }; return dataPipeline.process(dataCtx, { noConsumers: options.requestFromUser, tracer, - actions: config.options.actions, + actions: config.dataOpts.actions, deviceContext: systemStats.contextData }); }) @@ -82,16 +278,24 @@ function process() { * @returns {Promise.} Promise resolved with data from System Poller */ function safeProcess() { + const requestFromUser = (arguments.length > 1 ? arguments[1] : {}).requestFromUser; try { // eslint-disable-next-line - return process.apply(null, arguments) + return module.exports.process.apply(null, arguments) .catch((err) => { logger.exception('systemPoller:safeProcess unhandled exception in promise-chain', err); + if (requestFromUser) { + return Promise.reject(err); + } + return Promise.resolve(); }); } catch (err) { logger.exception('systemPoller:safeProcess unhandled exception', err); - return Promise.reject(new Error(`systemPoller:safeProcess unhandled exception: ${err}`)); + if (requestFromUser) { + return Promise.reject(new Error(`systemPoller:safeProcess unhandled exception: ${err}`)); + } } + return Promise.resolve(); } /** @@ -100,74 +304,52 @@ function safeProcess() { * @param {Object} restOperation - request object */ function processClientRequest(restOperation) { + // only GET requests allowed // allowed URIs: // - shared/telemetry/systempoller/systemName // - shared/telemetry/systempoller/systemPollerName // - shared/telemetry/systempoller/systemName/systemPollerName const parts = restOperation.getUri().pathname.split('/'); - const objName = parts[4]; - const subObjName = parts[5]; - - if (!objName) { - util.restOperationResponder(restOperation, 400, - { code: 400, message: 'Bad Request. System\'s or System Poller\'s name not specified.' }); - return; - } - configWorker.getConfig() + const objName = (parts[4] || '').trim(); + const subObjName = (parts[5] || '').trim(); + + (new Promise((resolve, reject) => { + if (objName === '') { + const err = new Error('Bad Request. Name for System or System Poller was not specified.'); + err.responseCode = 400; + reject(err); + } else { + resolve(); + } + })) + .then(() => configWorker.getConfig()) .then((config) => { - config = config.parsed; - const systems = config[SYSTEM_CLASS_NAME] || {}; - const systemPollers = config[SYSTEM_POLLER_CLASS_NAME] || {}; - - let system; - let systemPoller; - - if (objName && subObjName) { - system = systems[objName]; - systemPoller = systemPollers[subObjName]; - } else if (!util.isObjectEmpty(systemPollers[objName])) { - systemPoller = systemPollers[objName]; - system = systemPoller; - } else if (!util.isObjectEmpty(systems[objName])) { - system = systems[objName]; - if (typeof system.systemPoller === 'string') { - systemPoller = systemPollers[system.systemPoller]; - } else if (system.systemPoller) { - systemPoller = system.systemPoller; - } - } - if (!(system && systemPoller)) { - const error = new Error('System Poller declaration not found.'); - error.responseCode = 404; - return Promise.reject(error); - } - - if (!JSON.stringify(config).includes('format":"legacy')) { - systemPoller.noTmstats = true; + // config was copied by getConfig already + // before calling normalizeConfig we have to create custom config + try { + return module.exports.createCustomConfig(config.parsed, objName, subObjName); + } catch (err) { + err.responseCode = 404; + return Promise.reject(err); } - - return Promise.resolve([system, systemPoller]); }) - .then((configs) => { - const system = configs[0]; - const systemPoller = configs[1]; - - if (system.class === SYSTEM_POLLER_CLASS_NAME) { - return Promise.all([ - deviceUtil.decryptAllSecrets(system), - Promise.resolve(systemPoller) - ]); + .then(config => deviceUtil.decryptAllSecrets(config)) + .then((config) => { + const system = getTelemetrySystems(normalizeConfig(config))[objName]; + if (util.isObjectEmpty(system.systemPollers)) { + // unexpected, something went wrong + const err = new Error(`System '${objName}' has no System Poller(s) configured`); + err.responseCode = 404; + return Promise.reject(err); } - return Promise.all([ - deviceUtil.decryptAllSecrets(system), - deviceUtil.decryptAllSecrets(systemPoller) - ]); + + const pollers = module.exports.getEnabledPollerConfigs(system, false, true) + .map(pollerConfig => module.exports.safeProcess(pollerConfig, { requestFromUser: true })); + return Promise.all(pollers); }) - .then((configs) => { - const config = mergeConfigs(configs[0], configs[1]); - return safeProcess(config, { requestFromUser: true }); + .then((dataCtx) => { + util.restOperationResponder(restOperation, 200, dataCtx.map(d => d.data)); }) - .then(dataCtx => util.restOperationResponder(restOperation, 200, dataCtx.data)) .catch((error) => { let message; let code; @@ -176,165 +358,37 @@ function processClientRequest(restOperation) { code = error.responseCode; message = `${error}`; } else { - logger.error(`poller request ended up with error: ${error}`); + message = `${error}`; code = 500; - message = `systemPoller.process error: ${error}`; + logger.exception(`poller request ended up with error: ${message}`, error); } util.restOperationResponder(restOperation, code, { code, message }); }); } -/** - * Merge configs - * - * @private - * - * @param {Object} system - System declaration - * @param {Object} systemPoller - System Poller declaration - * - * @returns {Object} config - */ -function mergeConfigs(system, systemPoller) { - return { - enable: Boolean(system.enable && systemPoller.enable), - trace: Boolean(system.trace && systemPoller.trace), - interval: systemPoller.interval, - config: { - host: system.host, - options: { - connection: { - port: system.port, - protocol: system.protocol, - allowSelfSignedCert: system.allowSelfSignedCert - }, - credentials: { - username: system.username, - passphrase: system.passphrase - }, - tags: systemPoller.tag, - actions: systemPoller.actions, - noTmstats: systemPoller.noTmstats - } - } - }; -} - -/** - * Build config for provided key - * - * @private - * - * @param {Object} systems - System declarations - * @param {Object} systemPollers - System Poller declarations - * @param {String} key - key to build config for - * - * @returns {Object} config - */ -function buildConfig(systems, systemPollers, key) { - let systemPoller; - let name = key; - let system = systems[key]; - - if (!util.isObjectEmpty(system)) { - if (typeof system.systemPoller === 'string') { - name = `${name}_${system.systemPoller}`; - systemPoller = systemPollers[system.systemPoller]; - } else { - name = `${name}_System_Poller`; - systemPoller = system.systemPoller; - } - } else { - systemPoller = systemPollers[key]; - system = systemPoller; - } - if (!(system && systemPoller)) { - // somehow it happen - return null; - } - const config = mergeConfigs(system, systemPoller); - config.name = name; - return config; -} - - // config worker change event configWorker.on('change', (config) => { - logger.debug('configWorker change event in systemPoller'); // helpful debug - - config = config || {}; - const systems = config[SYSTEM_CLASS_NAME] || {}; - const systemPollers = config[SYSTEM_POLLER_CLASS_NAME] || {}; - const validPollerIDs = []; - const objectsToIter = {}; - const usedSystemPollers = []; - - Object.keys(systems).forEach((sysKey) => { - const system = systems[sysKey]; - objectsToIter[sysKey] = system; - - if (typeof system.systemPoller === 'string') { - usedSystemPollers.push(system.systemPoller); - } - }); - Object.keys(systemPollers).forEach((sysKey) => { - objectsToIter[sysKey] = systemPollers[sysKey]; - }); - + logger.debug('configWorker change event in systemPoller'); // timestamp to find out-dated tracers const tracersTimestamp = new Date().getTime(); + // copy, normalize and apply config + module.exports.applyConfig(normalizeConfig(util.deepCopy(config || {}))); + // remove tracers that were not touched + util.tracer.remove(tracer => tracer.name.startsWith(TRACER_CLASS_NAME) + && tracer.lastGetTouch < tracersTimestamp); - Object.keys(objectsToIter).forEach((key) => { - // silently skip System Pollers - // that are in use by System already - if (usedSystemPollers.indexOf(key) !== -1) { - return; - } - - const spConfig = buildConfig(systems, systemPollers, key); - if (util.isObjectEmpty(spConfig) || !spConfig.enable) { - return; - } - - validPollerIDs.push(spConfig.name); - spConfig.tracer = util.tracer.createFromConfig( - SYSTEM_POLLER_CLASS_NAME, spConfig.name, spConfig - ); - // Only collect tmstats if Splunk consumer is using legacy format - if (!JSON.stringify(config).includes('format":"legacy')) { - spConfig.config.options.noTmstats = true; - } - - const baseMsg = `system poller ${spConfig.name}. Interval = ${spConfig.interval} sec.`; - if (pollerIDs[spConfig.name]) { - logger.info(`Updating ${baseMsg}`); - pollerIDs[spConfig.name] = util.update( - pollerIDs[spConfig.name], safeProcess, spConfig, spConfig.interval - ); - } else { - logger.info(`Starting ${baseMsg}`); - pollerIDs[spConfig.name] = util.start( - safeProcess, spConfig, spConfig.interval - ); - } - }); - - Object.keys(pollerIDs).forEach((key) => { - if (validPollerIDs.indexOf(key) === -1) { - logger.info(`Disabling system poller ${key}`); - util.stop(pollerIDs[key]); - delete pollerIDs[key]; - } - }); - - util.tracer.remove(null, tracer => tracer.name.startsWith(SYSTEM_POLLER_CLASS_NAME) - && tracer.lastGetTouch < tracersTimestamp); - - logger.debug(`${Object.keys(pollerIDs).length} system poller(s) running`); + logger.debug(`${Object.keys(module.exports.getPollerTimers()).length} system poller(s) running`); }); module.exports = { + applyConfig, + createCustomConfig, + createPollerConfig, + getEnabledPollerConfigs, + getPollerTimers, + getTraceValue, process, - safeProcess, - processClientRequest + processClientRequest, + safeProcess }; diff --git a/src/lib/systemStats.js b/src/lib/systemStats.js index 043601ed..8e3fc571 100644 --- a/src/lib/systemStats.js +++ b/src/lib/systemStats.js @@ -1,5 +1,5 @@ /* - * Copyright 2018. F5 Networks, Inc. See End User License Agreement ("EULA") for + * Copyright 2020. F5 Networks, Inc. See End User License Agreement ("EULA") for * license terms. Notwithstanding anything to the contrary in the EULA, Licensee * may copy and modify this software product for its internal business purposes. * Further, Licensee may upload, publish and distribute the modified version of @@ -9,50 +9,84 @@ 'use strict'; -const constants = require('./constants.js'); -const util = require('./util.js'); -const normalize = require('./normalize.js'); -const properties = require('./properties.json'); -const paths = require('./paths.json'); -const logger = require('./logger.js'); +const constants = require('./constants'); +const util = require('./util'); +const normalize = require('./normalize'); +const defaultProperties = require('./properties.json'); +const defaultPaths = require('./paths.json'); +const logger = require('./logger'); const EndpointLoader = require('./endpointLoader'); const dataUtil = require('./dataUtil'); const systemStatsUtil = require('./systemStatsUtil'); +/** @module systemStats */ /** * System Stats Class - * @param {String} host - host - * @param {Object} [options] - options - * @param {Object} [options.tags] - tags to add to the data (each key) - * @param {String} [options.credentials.username] - username for host - * @param {String} [options.credentials.passphrase] - password for host - * @param {String} [options.connection.protocol] - protocol for host - * @param {Integer} [options.connection.port] - port for host - * @param {Boolean} [options.connection.allowSelfSignedCert] - false - requires SSL certificates be valid, - * true - allows self-signed certs + * @param {Object} config - config object + * @param {Object} config.connection - connection info + * @param {String} config.connection.host - host to connect to + * @param {Integer} [config.connection.port] - port to use + * @param {String} [config.connection.protocol] - protocol to use to connect + * @param {Boolean} [config.connection.allowSelfSignedCert] - false - requires SSL certificates be valid, + * true - allows self-signed certs + * @param {String} [config.credentials.username] - username for host + * @param {String} [config.credentials.passphrase] - password for host + * @param {Object} [config.dataOpts] - data options + * @param {Object} [config.dataOpts.tags] - tags to add to the data (each key) + * @param {Object} [config.dataOpts.actions] - actions to apply to the data (each key) + * @param {Boolean} [config.dataOpts.noTMStats] - true if don't need to fetch TMSTAT data + * @param {Object} [config.endpoints] - endpoints to use to fetch data + * @param {String} [config.name] - name */ -function SystemStats(host, options) { - options = options || {}; - - const _paths = options.paths || paths; - const _properties = options.properties || properties; +function SystemStats(config) { + config = util.assignDefaults( + config, + { + connection: {}, + credentials: {}, + dataOpts: {}, + name: 'UnknownPoller' + } + ); - this.noTmstats = options.noTmstats; - this.tags = options.tags || {}; - this.loader = new EndpointLoader(host, options); - this.loader.setEndpoints(_paths.endpoints); + config.dataOpts = util.assignDefaults( + config.dataOpts, + { + tags: {}, + noTMStats: false, + actions: [] + } + ); + this.logger = logger.getChild(`${config.name}`); + this.noTMStats = config.dataOpts.noTMStats; + this.tags = config.dataOpts.tags; + this.actions = config.dataOpts.actions; + this.collectedData = {}; - this.actions = options.actions || []; + this.loader = new EndpointLoader( + config.connection.host, + { + credentials: util.deepCopy(config.credentials), + connection: util.deepCopy(config.connection), + logger: this.logger + } + ); - // deep copy this.stats so that new instances of SystemStats get their own version of this.stats - this.stats = util.deepCopy(_properties.stats); - this.context = _properties.context; - this.definitions = _properties.definitions; - this.global = _properties.global; + const paths = config.paths || defaultPaths; + const properties = config.properties || defaultProperties; + this.global = properties.global; - this.contextData = {}; - this.collectedData = {}; + if (typeof config.endpointList === 'undefined') { + this.stats = properties.stats; + this.definitions = properties.definitions; + this.endpoints = paths.endpoints; + this.contextProps = properties.context; + this.contextData = {}; + } else { + this.endpoints = config.endpointList; + this.isCustom = true; + } } /** * Split key @@ -83,6 +117,7 @@ SystemStats.prototype._processData = function (property, data, key) { const defaultTags = { name: { pattern: '(.*)', group: 1 } }; const addKeysByTagIsObject = property.normalization && property.normalization.find(n => n.addKeysByTag && typeof n.addKeysByTag === 'object'); + const options = { key: this._splitKey(property.key).childKey, propertyKey: key @@ -162,13 +197,15 @@ SystemStats.prototype._loadData = function (property) { return this.loader.loadEndpoint(endpoint, property.keyArgs) .then((data) => { - if (!data.data.items) { - data.data.items = []; + data = data.data; + if (data && typeof data === 'object' && typeof data.items === 'undefined' + && Object.keys(data).length === 2 && data.kind.endsWith('state')) { + data.items = []; } - return Promise.resolve(data.data); + return Promise.resolve(data); }) .catch((err) => { - logger.error(`Error: SystemStats._loadData: ${endpoint} (${property.keyArgs}): ${err}`); + this.logger.error(`Error: SystemStats._loadData: ${endpoint} (${property.keyArgs}): ${err}`); return Promise.reject(err); }); }; @@ -181,11 +218,11 @@ SystemStats.prototype._loadData = function (property) { * @returns {Object} Promise resolved when data was successfully colleted */ SystemStats.prototype._processProperty = function (key, property) { - if (this.noTmstats && property.structure && property.structure.parentKey === 'tmstats') { + if (this.noTMStats && property.structure && property.structure.parentKey === 'tmstats') { return Promise.resolve(); } - property = systemStatsUtil.renderProperty(this.contextData, property); + property = systemStatsUtil.renderProperty(this.contextData, util.deepCopy(property)); /** * if endpoints will have their own 'disabled' flag * we will need to add additional check here or simply return empty value. @@ -210,7 +247,7 @@ SystemStats.prototype._processProperty = function (key, property) { } }) .catch((err) => { - logger.error(`Error: SystemStats._processProperty: ${key} (${property.key}): ${err}`); + this.logger.error(`Error: SystemStats._processProperty: ${key} (${property.key}): ${err}`); return Promise.reject(err); }); }; @@ -242,15 +279,15 @@ SystemStats.prototype._processContext = function (contextData) { SystemStats.prototype._computeContextData = function () { let promise; - if (Array.isArray(this.context)) { - if (this.context.length) { - promise = this._processContext(this.context[0]); - for (let i = 1; i < this.context.length; i += 1) { - promise.then(this._processContext(this.context[i])); + if (Array.isArray(this.contextProps)) { + if (this.contextProps.length) { + promise = this._processContext(this.contextProps[0]); + for (let i = 1; i < this.contextProps.length; i += 1) { + promise.then(this._processContext(this.contextProps[i])); } } - } else if (this.context) { - promise = this._processContext(this.context); + } else if (this.contextProps) { + promise = this._processContext(this.contextProps); } if (!promise) { promise = Promise.resolve(); @@ -278,7 +315,7 @@ SystemStats.prototype._filterStats = function () { * to avoid memory usage. */ // early return - if (util.isObjectEmpty(this.actions)) { + if (util.isObjectEmpty(this.actions) || this.isStatsFilterApplied) { return; } const FLAGS = { @@ -310,17 +347,17 @@ SystemStats.prototype._filterStats = function () { if (!actionCtx.enable) { return; } - if (actionCtx.ifAllMatch) { - // if ifAllMatch points to nonexisting data - VS name, tag or what ever else + if (actionCtx.ifAllMatch || actionCtx.ifAnyMatch) { + // if ifAllMatch or ifAnyMatch points to nonexisting data - VS name, tag or what ever else // we have to mark all existing paths with PRESERVE flag - dataUtil.searchAnyMatches(statsSkeleton, actionCtx.ifAllMatch, (key, item) => { + dataUtil.searchAnyMatches(statsSkeleton, actionCtx.ifAllMatch || actionCtx.ifAnyMatch, (key, item) => { item.flag = FLAGS.PRESERVE; return nestedKey; }); } - // if includeData/excludeData paired with ifAllMatch then we can simply ignore it + // if includeData/excludeData paired with ifAllMatch or ifAnyMatch then we can simply ignore it // because we can't include/exclude data without conditional check - if (actionCtx.excludeData && !actionCtx.ifAllMatch) { + if (actionCtx.excludeData && !(actionCtx.ifAllMatch || actionCtx.ifAnyMatch)) { dataUtil.removeStrictMatches(statsSkeleton, actionCtx.locations, (key, item, getNestedKey) => { if (getNestedKey) { return nestedKey; @@ -328,7 +365,7 @@ SystemStats.prototype._filterStats = function () { return item.flag !== FLAGS.PRESERVE; }); } - if (actionCtx.includeData && !actionCtx.ifAllMatch) { + if (actionCtx.includeData && !(actionCtx.ifAllMatch || actionCtx.ifAnyMatch)) { // strict is false - it is okay to have partial matches because we can't be sure // for 100% that such data was not added by previous action dataUtil.preserveStrictMatches(statsSkeleton, actionCtx.locations, false, (key, item, getNestedKey) => { @@ -339,6 +376,8 @@ SystemStats.prototype._filterStats = function () { }); } }); + + let statsCopy; Object.keys(this.stats).forEach((statKey) => { let skeleton = statsSkeleton; // path to stat should exists otherwise we can delete it @@ -350,10 +389,50 @@ SystemStats.prototype._filterStats = function () { return skeleton; }); if (!exists) { - delete this.stats[statKey]; + if (!statsCopy) { + statsCopy = util.deepCopy(this.stats); + } + delete statsCopy[statKey]; } }); + if (statsCopy) { + this.stats = statsCopy; + } + this.isStatsFilterApplied = true; +}; + +/** + * Converts a telemetry_endpoint to a standard property. + * Only BIG-IP paths currently supported, + * For e.g. /mgmt/tm/subPath?$select=prop1,prop2 + * (Note that we don't guarantee behavior for all types of query params) + * + * @param {String} keyName - property key + * @param {Object} endpoint - object to convert + * + * @returns {Object} Converted property + */ + +SystemStats.prototype._convertToProperty = function (keyName, endpoint) { + let normalization; + const statsIndex = endpoint.path.indexOf('/stats'); + const bigipBasePath = 'mgmt/tm/'; + if (statsIndex > -1) { + const mgmtTmIndex = endpoint.path.indexOf(bigipBasePath) + bigipBasePath.length; + const renameKeys = { patterns: {} }; + const pathMatch = endpoint.path.substring(mgmtTmIndex, statsIndex); + + // eslint-disable-next-line no-useless-escape + renameKeys.patterns[pathMatch] = { pattern: `${pathMatch}\/(.*)`, group: 1 }; + normalization = [{ renameKeys }]; + } + return { + key: keyName, + normalization + }; }; + + /** * Compute properties * @@ -365,14 +444,14 @@ SystemStats.prototype._computePropertiesData = function () { return Promise.all(Object.keys(this.stats) .map(key => this._processProperty(key, this.stats[key]))); }; + /** - * Collect info based on object provided in properties + * Collect info based on object provided in paths and properties files (builtin/ defaults) * * @returns {Object} Promise which is resolved with a map of stats */ -SystemStats.prototype.collect = function () { - return this.loader.auth() - .then(() => this._computeContextData()) +SystemStats.prototype.collectDefaultPathsProps = function () { + return this._computeContextData() .then(() => { this._filterStats(); return Promise.resolve(); @@ -394,10 +473,72 @@ SystemStats.prototype.collect = function () { } }); return Promise.resolve(data); + }); +}; + +/** + * Collect info based on object provided in declaration (config from user input) + * Currently customEndpoints supported are only BIG-IP endpoints + * + * @returns {Object} Promise + */ +SystemStats.prototype.collectCustomEndpoints = function () { + return new Promise((resolve, reject) => { + const endpKeys = Object.keys(this.endpoints); + + const processEndpoint = (idx) => { + if (idx >= endpKeys.length) { + return resolve(this.collectedData); + } + const endpointKey = endpKeys[idx]; + const endpoint = this.endpoints[endpointKey]; + const keyName = endpoint.name || endpointKey; + + return Promise.resolve() + .then(() => this._processProperty(keyName, this._convertToProperty(keyName, endpoint))) + .then(() => { + processEndpoint(idx + 1); + }) + .catch((err) => { + const msg = `Error on attempt to load data from endpoint '${endpoint.name}[${endpoint.path}]': ${err}`; + err.message = msg; + reject(err); + }); + }; + + processEndpoint(0); + }); +}; + +/** + * Collect info + * + * @returns {Object} Promise which is resolved with a map of stats + */ +SystemStats.prototype.collect = function () { + let collectedData; + let caughtErr; + this.logger.debug('Starting stats collection'); + return this.loader.auth() + .then(() => { + this.loader.setEndpoints(this.endpoints); + return this.isCustom ? this.collectCustomEndpoints() : this.collectDefaultPathsProps(); + }) + .then((data) => { + collectedData = data; }) .catch((err) => { - logger.error(`Error: SystemStats.collect: ${err}`); - return Promise.reject(err); + caughtErr = err; + }) + .then(() => { + // erase cached data + this.loader.eraseCache(); + if (caughtErr) { + const message = caughtErr.message || `Error: SystemStats.collect: ${caughtErr}`; + this.logger.error(message); + return Promise.reject(caughtErr); + } + return Promise.resolve(collectedData); }); }; diff --git a/src/lib/systemStatsUtil.js b/src/lib/systemStatsUtil.js index 57065736..4c8e0431 100644 --- a/src/lib/systemStatsUtil.js +++ b/src/lib/systemStatsUtil.js @@ -9,12 +9,7 @@ 'use strict'; const mustache = require('mustache'); -const util = require('./util.js'); - -const CONDITIONAL_FUNCS = { - deviceVersionGreaterOrEqual, - isModuleProvisioned -}; +const util = require('./util'); /** * Comparison functions @@ -43,7 +38,7 @@ function deviceVersionGreaterOrEqual(contextData, versionToCompare) { * * @param {Object} contextData - context data * @param {Object} contextData.provisioning - provision state of modules to compare - * @param {String} moduletoCompare - module to compare against + * @param {String} moduleToCompare - module to compare against * * @returns {boolean} true when device's module is provisioned */ @@ -57,41 +52,63 @@ function isModuleProvisioned(contextData, moduleToCompare) { module.exports = { + CONDITIONAL_FUNCS: { + deviceVersionGreaterOrEqual, + isModuleProvisioned + }, + /** * Evaluate conditional block * - * @param {Object} contextData - contextData object + * @param {Object} contextData - contextData object * @param {Object} conditionalBlock - block to evaluate, where object's key - conditional operator * object's value - params for that operator * * @returns {boolean} conditional result */ _resolveConditional(contextData, conditionalBlock) { - let ret = true; - Object.keys(conditionalBlock).forEach((key) => { - const func = CONDITIONAL_FUNCS[key]; + return Object.keys(conditionalBlock).every((key) => { + const func = this.CONDITIONAL_FUNCS[key]; if (func === undefined) { - throw new Error(`Unknown property in conditional block ${key}`); + throw new Error(`Unknown property '${key}' in conditional block`); } - ret = ret && func(contextData, conditionalBlock[key]); + return func(contextData, conditionalBlock[key]); }); - return ret; }, /** - * Render key using mustache template system + * Render property using mustache template system. * * @param {Object} contextData - contextData object - * @param {Object} property - property object + * @param {Object} property - property object * * @returns {Object} rendered property object */ - _renderPropTemplate(contextData, property) { - // should be easy to add support for more complex templates like {{ #something }} - // but not sure we are really need it now. - // For now just supporting simple templates which - // generates single string only - if (property.key) property.key = mustache.render(property.key, contextData); + _renderTemplate(contextData, property) { + // traverse object without recursion + const stack = [property]; + const forKey = (key) => { + const val = stack[0][key]; + switch (typeof val) { + case 'object': + if (val !== null) { + stack.push(val); + } + break; + case 'string': + if (val.indexOf('{{') !== -1) { + stack[0][key] = mustache.render(val, contextData); + } + break; + default: + break; + } + }; + while (stack.length) { + // expecting objects only to be in stack var + Object.keys(stack[0]).forEach(forKey); + stack.shift(); + } return property; }, @@ -99,48 +116,57 @@ module.exports = { * Property pre-processing to resolve conditionals * * @param {Object} contextData - contextData object - * @param {Object} property - property object + * @param {Object} property - property object * * @returns {Object} pre-processed deep copy of property object */ _preprocessProperty(contextData, property) { - if (property.if) { - const newObj = {}; - // property can result in 'false' when - // 'else' or 'then' were not defined. - while (property) { - // copy all non-conditional data on same level to new object - // eslint-disable-next-line no-loop-func - Object.keys(property).forEach((key) => { - if (!(key === 'if' || key === 'then' || key === 'else')) { - newObj[key] = property[key]; + // traverse object without recursion + const stack = [property]; + let obj; + + const forKey = (key) => { + if (typeof obj[key] === 'object' && obj[key] !== null) { + stack.push(obj[key]); + } + }; + + while (stack.length) { + obj = stack[0]; + if (obj.if) { + const ifBlock = obj.if; + const thenBlock = obj.then; + const elseBlock = obj.else; + delete obj.if; + delete obj.then; + delete obj.else; + if (this._resolveConditional(contextData, ifBlock)) { + if (thenBlock) { + Object.assign(obj, thenBlock); } - }); - // so, we copied everything we needed. - // break in case there is no nested 'if' block - if (!property.if) { - break; + } else if (elseBlock) { + Object.assign(obj, elseBlock); } - // trying to resolve conditional - property = this._resolveConditional(contextData, property.if) - ? property.then : property.else; + } else { + Object.keys(obj).forEach(forKey); + stack.shift(); } - property = newObj; } - // deep copy - return util.deepCopy(property); + return property; }, /** * Render property based on template and conditionals * * @param {Object} contextData - contextData object - * @param {Object} property - property object + * @param {Object} property - property object * * @returns {Object} rendered property */ renderProperty(contextData, property) { - return this._renderPropTemplate(contextData, this._preprocessProperty(contextData, property)); + return this._preprocessProperty( + contextData, + this._renderTemplate(contextData, property) + ); } - }; diff --git a/src/lib/util.js b/src/lib/util.js index 7a64c758..e36666ec 100644 --- a/src/lib/util.js +++ b/src/lib/util.js @@ -13,8 +13,8 @@ const net = require('net'); const path = require('path'); const request = require('request'); -const constants = require('./constants.js'); -const logger = require('./logger.js'); +const constants = require('./constants'); +const logger = require('./logger'); /** @module util */ @@ -24,8 +24,8 @@ const logger = require('./logger.js'); * * @class * - * @param {string} name - tracer's name - * @param {string} tracerPath - path to file + * @param {String} name - tracer's name + * @param {String} tracerPath - path to file * * @property {String} name - tracer's name * @property {String} path - path to file @@ -88,7 +88,7 @@ Tracer.prototype.removeScheduledReopen = function () { * Set state * * @private - * @param {string} newState - tracer's new state + * @param {String} newState - tracer's new state */ Tracer.prototype._setState = function (newState) { // reject any state if current is STOP @@ -251,8 +251,11 @@ Tracer.prototype._mkdir = function () { return new Promise((resolve, reject) => { logger.info(`Creating dir '${baseDir}' for tracer '${self.name}'`); fs.mkdir(baseDir, { recursive: true }, (mkdirErr) => { - if (mkdirErr) reject(mkdirErr); - else resolve(); + if (mkdirErr) { + reject(mkdirErr); + } else { + resolve(); + } }); }); }); @@ -342,7 +345,7 @@ Tracer.prototype._truncate = function () { * @async * @private * - * @param {string} data - data to write to stream + * @param {String} data - data to write to stream * * @returns {Promise} Promise resolved when data was written */ @@ -354,8 +357,11 @@ Tracer.prototype._write = function (data) { resolve(); } else { self.stream.write(data, (err) => { - if (err) reject(err); - else resolve(); + if (err) { + reject(err); + } else { + resolve(); + } }); } }); @@ -364,7 +370,7 @@ Tracer.prototype._write = function (data) { /** * Check if tracer ready to process data * - * @returns {boolean} true if ready else false + * @returns {Boolean} true if ready else false */ Tracer.prototype.isReady = function () { return this.state === Tracer.STATE.READY; @@ -373,7 +379,7 @@ Tracer.prototype.isReady = function () { /** * Check if tracer is able to process/buffer data * - * @returns {boolean} true if ready else false + * @returns {Boolean} true if ready else false */ Tracer.prototype.isAvailable = function () { return this.isReady() @@ -384,7 +390,7 @@ Tracer.prototype.isAvailable = function () { /** * Check if tracer should be initialized * - * @returns {boolean} true if not initialized else false + * @returns {Boolean} true if not initialized else false */ Tracer.prototype.isNew = function () { return this.state === Tracer.STATE.NEW @@ -401,7 +407,7 @@ Tracer.prototype.touch = function () { /** * Reopen stream if needed * - * @param {boolean} schedule - true when need to schedule function + * @param {Boolean} schedule - true when need to schedule function */ Tracer.prototype.reopenIfNeeded = function (schedule) { this.removeScheduledReopen(); @@ -436,7 +442,7 @@ Tracer.prototype.stop = function () { * Write data to tracer * * @async - * @param {string} data - data to write to tracer + * @param {String} data - data to write to tracer */ Tracer.prototype.write = function (data) { if (!data) { @@ -454,15 +460,15 @@ Tracer.prototype.write = function (data) { /** * Instances cache. * - * @member {Object.} + * @member {Object.} */ Tracer.instances = {}; /** * Get Tracer instance or create new one * - * @param {string} name - tracer name - * @param {string} tracerPath - destination path + * @param {String} name - tracer name + * @param {String} tracerPath - destination path * * @returns {Tracer} Tracer instance */ @@ -487,9 +493,10 @@ Tracer.get = function (name, tracerPath) { /** * Create tracer from config * - * @param {string} className - object's class name - * @param {string} objName - object's name - * @param {Object} config - object's config + * @param {String} className - object's class name + * @param {String} objName - object's name + * @param {Object} config - object's config + * @param {String|Boolean} [config.trace] - path to file, if 'true' then default path will be used * * @returns {Tracer} Tracer object */ @@ -530,23 +537,22 @@ Tracer.removeTracer = function (tracer) { /** * Remove Tracer instance * - * @param {string | Tracer} toRemove - tracer or tracer's name to remove - * @param {Tracer~filterCallback} filter - filter function + * @param {String | Tracer | Tracer~filterCallback} toRemove - tracer or tracer's name to remove + * or filter function */ -Tracer.remove = function (toRemove, filter) { - if (toRemove) { - if (typeof toRemove !== 'string') { - toRemove = toRemove.name; - } - Tracer.removeTracer(Tracer.instances[toRemove]); - } - if (filter) { +Tracer.remove = function (toRemove) { + if (typeof toRemove === 'function') { Object.keys(Tracer.instances).forEach((tracerName) => { const tracer = Tracer.instances[tracerName]; - if (filter(tracer)) { + if (toRemove(tracer)) { Tracer.removeTracer(tracer); } }); + } else { + if (typeof toRemove !== 'string') { + toRemove = toRemove.name; + } + Tracer.removeTracer(Tracer.instances[toRemove]); } }; @@ -594,8 +600,44 @@ function retryPromise(fn, opts) { }); } +// cleanup options. Update tests (test/unit/utilTests.js) when adding new value +const MAKE_REQUEST_OPTS_TO_REMOVE = [ + 'allowSelfSignedCert', + 'continueOnErrorCode', + 'expectedResponseCode', + 'fullURI', + 'includeResponseObject', + 'json', + 'port', + 'protocol', + 'rawResponseBody' +]; + +const VERSION_COMPARATORS = ['==', '===', '<', '<=', '>', '>=', '!=', '!==']; + module.exports = { + /** + * Assign defaults to object + * + * @param {Object} obj - object to assign defaults to + * @param {Object} defaults - defaults to assign to object + * + * @returns {Object} + */ + assignDefaults(obj, defaults) { + // from docs: if the value is null or undefined, it will create and return an empty object + // otherwise, it will return an object of a Type that corresponds to the given value. + // If the value is an object already, it will return the value. + obj = Object(obj); + Object.keys(defaults).forEach((key) => { + if (!Object.prototype.hasOwnProperty.call(obj, key)) { + obj[key] = defaults[key]; + } + }); + return obj; + }, + /** * Check if object has any data or not * @@ -604,10 +646,18 @@ module.exports = { * @returns {Boolean} 'true' if empty else 'false' */ isObjectEmpty(obj) { - if (obj === undefined || obj === null) return true; - if (Array.isArray(obj) || typeof obj === 'string') return obj.length === 0; + if (obj === undefined || obj === null) { + return true; + } + if (Array.isArray(obj) || typeof obj === 'string') { + return obj.length === 0; + } /* eslint-disable no-restricted-syntax */ - for (const key in obj) if (Object.prototype.hasOwnProperty.call(obj, key)) return false; + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + return false; + } + } return true; }, /** @@ -673,7 +723,7 @@ module.exports = { */ compareVersionStrings(version1, comparator, version2) { comparator = comparator === '=' ? '==' : comparator; - if (['==', '===', '<', '<=', '>', '>=', '!=', '!=='].indexOf(comparator) === -1) { + if (VERSION_COMPARATORS.indexOf(comparator) === -1) { throw new Error(`Invalid comparator '${comparator}'`); } const v1parts = version1.split('.'); @@ -729,7 +779,9 @@ module.exports = { const v = data[k]; // check if value for v is an object that contains a class if (typeof v === 'object' && v.class) { - if (!ret[v.class]) { ret[v.class] = {}; } + if (!ret[v.class]) { + ret[v.class] = {}; + } ret[v.class][k] = v; } }); @@ -765,27 +817,49 @@ module.exports = { /** * Perform HTTP request * + * @example + * // host only + * makeRequest(hostStr) + * @example + * // options only + * makeRequest(optionsObj) + * @example + * // host and options + * makeRequest(hostStr, optionsObj) + * @example + * // host and uri and options + * makeRequest(hostStr, uriStr, optionsObj) + * @example + * // host and uri + * makeRequest(hostStr, uriStr) + * * @param {String} [host] - HTTP host * @param {String} [uri] - HTTP uri * @param {Object} [options] - function options. Copy it before pass to function. * @param {String} [options.fullURI] - full HTTP URI - * @param {String} [options.protocol] - HTTP protocol - * @param {Integer} [options.port] - HTTP port - * @param {String} [options.method] - HTTP method - * @param {String} [options.body] - HTTP body + * @param {String} [options.protocol] - HTTP protocol, by default http + * @param {Integer} [options.port] - HTTP port, by default 80 + * @param {String} [options.method] - HTTP method, by default GET + * @param {Any} [options.body] - HTTP body, must be a Buffer, String or ReadStream or + * JSON-serializable object + * @param {Boolean} [options.json] - sets HTTP body to JSON representation of value and adds + * Content-type: application/json header, by default true * @param {Object} [options.headers] - HTTP headers - * @param {Object} [options.continueOnErrorCode] - resolve promise even on non-successful response code - * @param {Boolean} [options.allowSelfSignedCert] - false - requires SSL certificates be valid, - * true - allows self-signed certs - * @param {Object} [options.rawResponseBody] - true - Buffer object with binary data will be returned as body - * @param {Integer} [options.expectedResponseCode] - expected response code - * @param {Boolean} [options.includeResponseObject] - false - only body object/string will be returned - * true - array with [body, responseObject] will be returned + * @param {Object} [options.continueOnErrorCode] - continue on non-successful response code, by default false + * @param {Boolean} [options.allowSelfSignedCert] - do not require SSL certificates be valid, by default false + * @param {Object} [options.rawResponseBody] - return response as Buffer object with binary data, + * by default false + * @param {Boolean} [options.includeResponseObject] - return [body, responseObject], by default false + * @param {Array|Integer} [options.expectedResponseCode] - expected response code, by default 200 * * @returns {Promise.} Returns promise resolved with response */ makeRequest() { - // rest params syntax supported only fron node 6+ + if (arguments.length === 0) { + throw new Error('makeRequest: no arguments were passed to function'); + } + + // rest params syntax supported by node 6+ only let host; let uri; let options; @@ -801,42 +875,42 @@ module.exports = { options = arguments[2]; } - options = options || {}; - options.method = options.method || 'GET'; - options.protocol = options.protocol || constants.REQUEST_DEFAULT_PROTOCOL; - options.port = options.port || constants.REQUEST_DEFAULT_PORT; - options.body = options.body ? this.stringify(options.body) : undefined; + options = this.assignDefaults(options, { + continueOnErrorCode: false, + expectedResponseCode: [200], + headers: {}, + includeResponseObject: false, + json: true, + method: 'GET', + port: constants.HTTP_REQUEST.DEFAULT_PORT, + protocol: constants.HTTP_REQUEST.DEFAULT_PROTOCOL, + rawResponseBody: false + }); + options.headers['User-Agent'] = options.headers['User-Agent'] || constants.USER_AGENT; options.strictSSL = options.allowSelfSignedCert === undefined ? constants.STRICT_TLS_REQUIRED : !options.allowSelfSignedCert; - options.headers = options.headers || {}; - options.headers['User-Agent'] = options.headers['User-Agent'] || constants.USER_AGENT; - if (options.rawResponseBody) { options.encoding = null; } - if (host) { - options.uri = `${options.protocol}://${host}:${options.port}${uri || ''}`; - } else { - options.uri = options.fullURI; + if (options.json && typeof options.body !== 'undefined') { + options.body = JSON.stringify(options.body); } - if (!options.uri) { - throw new Error('makeRequest: No fullURI or host provided'); + uri = host ? `${options.protocol}://${host}:${options.port}${uri || ''}` : options.fullURI; + if (!uri) { + throw new Error('makeRequest: no fullURI or host provided'); } + options.uri = uri; - const rawResponseBody = options.rawResponseBody; const continueOnErrorCode = options.continueOnErrorCode; + const expectedResponseCode = Array.isArray(options.expectedResponseCode) + ? options.expectedResponseCode : [options.expectedResponseCode]; const includeResponseObject = options.includeResponseObject; - let expectedResponseCode = options.expectedResponseCode || [200]; - expectedResponseCode = Array.isArray(expectedResponseCode) ? expectedResponseCode - : [expectedResponseCode]; - - // cleanup options. Update tests when adding new value - ['rawResponseBody', 'continueOnErrorCode', 'expectedResponseCode', 'includeResponseObject', - 'port', 'protocol', 'fullURI', 'allowSelfSignedCert' - ].forEach((key) => { + const rawResponseBody = options.rawResponseBody; + + MAKE_REQUEST_OPTS_TO_REMOVE.forEach((key) => { delete options[key]; }); @@ -886,12 +960,15 @@ module.exports = { /** * Network check - with max timeout interval (5 seconds) * - * @param {String} host - host address - * @param {Integer} port - host port + * @param {String} host - host address + * @param {Integer} port - host port + * @param {Object} [options] - options + * @param {Integer} [options.timeout] - timeout before fail if unable to establish connection, by default 5s. + * @param {Integer} [options.period] - how often to check connection status, by default 100ms. * * @returns {Promise} Returns promise resolved on successful check */ - networkCheck(host, port) { + networkCheck(host, port, options) { let done = false; const connectPromise = new Promise((resolve, reject) => { const client = net.createConnection({ host, port }) @@ -908,23 +985,31 @@ module.exports = { }); }); - // 100 ms period with 50 max tries = 5 sec - const period = 100; const maxTries = 50; let currentTry = 1; + options = this.assignDefaults(options, { + period: 100, + timeout: 5 * 1000 + }); + if (options.timeout <= options.period) { + options.period = options.timeout; + } const timeoutPromise = new Promise((resolve, reject) => { const interval = setInterval(() => { + options.timeout -= options.period; + const fail = () => { clearInterval(interval); - reject(new Error(`unable to connect: ${host}:${port}`)); // max timeout, reject + reject(new Error(`unable to connect: ${host}:${port} (timeout exceeded)`)); // max timeout, reject }; if (done === true) { clearInterval(interval); resolve(); // connection success, resolve - } else if (done === 'error') fail(); - else if (currentTry < maxTries) ; // try again - else fail(); - currentTry += 1; - }, period); + } else if (done === 'error') { + fail(); + } else if (options.timeout <= 0) { + fail(); + } + }, options.period); }); return Promise.all([connectPromise, timeoutPromise]) diff --git a/src/nodejs/restWorker.js b/src/nodejs/restWorker.js index 7cccbf80..cdf8f161 100644 --- a/src/nodejs/restWorker.js +++ b/src/nodejs/restWorker.js @@ -8,27 +8,27 @@ 'use strict'; -const fs = require('fs'); -const path = require('path'); const http = require('http'); const https = require('https'); -const constants = require('../lib/constants.js'); -const logger = require('../lib/logger.js'); -const util = require('../lib/util.js'); +const constants = require('../lib/constants'); +const logger = require('../lib/logger'); +const util = require('../lib/util'); const baseSchema = require('../schema/latest/base_schema.json'); -const persistentStorage = require('../lib/persistentStorage.js'); -const configWorker = require('../lib/config.js'); -const eventListener = require('../lib/eventListener.js'); // eslint-disable-line no-unused-vars -const consumers = require('../lib/consumers.js'); // eslint-disable-line no-unused-vars -const systemPoller = require('../lib/systemPoller.js'); -const iHealthPoller = require('../lib/ihealth.js'); // eslint-disable-line no-unused-vars +const deviceUtil = require('../lib/deviceUtil'); +const retryPromise = require('../lib/util').retryPromise; +const persistentStorage = require('../lib/persistentStorage'); +const configWorker = require('../lib/config'); +const eventListener = require('../lib/eventListener'); // eslint-disable-line no-unused-vars +const consumers = require('../lib/consumers'); // eslint-disable-line no-unused-vars +const systemPoller = require('../lib/systemPoller'); +const iHealthPoller = require('../lib/ihealth'); // eslint-disable-line no-unused-vars /** @module restWorkers */ /** - * Simple router to route incomming requests to REST API. + * Simple router to route incoming requests to REST API. * * @class * @@ -54,7 +54,7 @@ function SimpleRouter() { * @param {SimpleRouter~requestCallback} callback - request handler */ SimpleRouter.prototype.register = function (method, endpointURI, callback) { - if (this.routes[endpointURI] === undefined) { + if (typeof this.routes[endpointURI] === 'undefined') { this.routes[endpointURI] = {}; } if (Array.isArray(method)) { @@ -105,7 +105,7 @@ SimpleRouter.prototype._processRestOperation = function (restOperation) { // evaluate data as JSON and returns code 500 on failure. // Don't know how to re-define this behavior. if (restOperation.getBody() && restOperation.getContentType().toLowerCase() !== 'application/json') { - util.restOperationResponder(restOperation, 405, + util.restOperationResponder(restOperation, 415, { code: 415, message: 'Unsupported Media Type', accept: ['application/json'] }); return; } @@ -212,6 +212,17 @@ RestWorker.prototype._initializeApplication = function (success, failure) { logger.exception('Startup Failed', err); failure(); }); + + // Gather info about host device. Running it as decoupled process + // to do not slow down application startup due REST API or other + // service may be not started yet. + retryPromise(() => deviceUtil.gatherHostDeviceInfo(), { maxTries: 100, delay: 30 }) + .then(() => { + logger.debug('Host Device Info gathered'); + }) + .catch((err) => { + logger.exception(`Unable to gather Host Device Info: ${err}`, err); + }); }; /** @@ -255,16 +266,13 @@ RestWorker.prototype.onPost = function (restOperation) { * @returns {void} */ RestWorker.prototype.processInfoRequest = function (restOperation) { - // usually version located at this path - /var/config/rest/iapps/f5-telemetry/version, - // we are in /var/config/rest/iapps/f5-telemetry/nodejs/restWorkers/ dir. - const vinfo = fs.readFileSync(path.join(__dirname, '..', 'version'), 'ascii').split('-'); - + const schemaVersionEnum = baseSchema.properties.schemaVersion.enum; util.restOperationResponder(restOperation, 200, { nodeVersion: process.version, - version: vinfo[0], - release: vinfo[1], - schemaCurrent: baseSchema.properties.schemaVersion.enum[0], - schemaMinimum: baseSchema.properties.schemaVersion.enum.reverse()[0] + version: constants.VERSION, + release: constants.RELEASE, + schemaCurrent: schemaVersionEnum[0], + schemaMinimum: schemaVersionEnum[schemaVersionEnum.length - 1] }); }; @@ -294,11 +302,9 @@ RestWorker.prototype.configChangeHandler = function (config) { logger.debug('configWorker change event in restWorker'); // helpful debug const settings = util.getDeclarationByName( - config, constants.CONTROLS_CLASS_NAME, constants.CONTROLS_PROPERTY_NAME - ); - if (util.isObjectEmpty(settings)) { - return; - } + config, constants.CONFIG_CLASSES.CONTROLS_CLASS_NAME, constants.CONTROLS_PROPERTY_NAME + ) || {}; + this.router.removeAllHandlers(); this.registerRestEndpoints(settings.debug); }; diff --git a/src/schema/1.10.0/base_schema.json b/src/schema/1.10.0/base_schema.json new file mode 100644 index 00000000..8d695794 --- /dev/null +++ b/src/schema/1.10.0/base_schema.json @@ -0,0 +1,416 @@ +{ + "$id": "base_schema.json", + "$async": true, + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Telemetry Streaming", + "description": "", + "type": "object", + "definitions": { + "enable": { + "title": "Enable", + "description": "This property can be used to enable/disable the poller/listener" , + "type": "boolean" + }, + "trace": { + "title": "Trace", + "description": "Enables data dumping to file. Boolean uses pre-defined file location, however value could be a string which contains path to a specific file instead" , + "type": ["boolean", "string"] + }, + "secret": { + "title": "Passphrase (secret)", + "description": "" , + "type": "object", + "properties": { + "class": { + "title": "Class", + "description": "Telemetry streaming secret class", + "type": "string", + "enum": [ "Secret" ], + "default": "Secret" + }, + "cipherText": { + "title": "Cipher Text: this contains a secret to encrypt", + "type": "string" + }, + "environmentVar": { + "title": "Environment Variable: this contains the named env var where the secret resides", + "type": "string" + }, + "protected": { + "$comment": "Meta property primarily used to determine if 'cipherText' needs to be encrypted", + "title": "Protected", + "type": "string", + "enum": [ "plainText", "plainBase64", "SecureVault" ], + "default": "plainText" + } + }, + "oneOf": [ + { "required": [ "cipherText" ] }, + { "required": [ "environmentVar" ] } + ], + "f5secret": true + }, + "username": { + "$comment": "Common field for username to use everywhere in scheme", + "title": "Username", + "type": "string" + }, + "stringOrSecret": { + "$async": true, + "allOf": [ + { + "if": { "type": "string" }, + "then": {}, + "else": {} + }, + { + "if": { "type": "object" }, + "then": { "$ref": "base_schema.json#/definitions/secret" }, + "else": {} + } + ] + }, + "constants": { + "title": "Constants", + "description": "" , + "type": "object", + "properties": { + "class": { + "title": "Class", + "description": "Telemetry streaming constants class", + "type": "string", + "enum": [ "Constants" ] + } + }, + "additionalProperties": true + }, + "tag": { + "$comment": "Defaults do not get applied for $ref objects, so place defaults alongside instead.", + "title": "Tag", + "description": "" , + "type": "object", + "properties": { + "tenant": { + "title": "Tenant tag", + "type": "string" + }, + "application": { + "title": "Application tag", + "type": "string" + } + }, + "additionalProperties": true + }, + "action": { + "title": "Action", + "description": "An action to be done on system data or on event data.", + "type": "object", + "properties": { + "enable": { + "title": "Enable", + "description": "Whether to enable this action in the declaration or not.", + "type": "boolean", + "default": true + }, + "setTag": { + "title": "Set Tag", + "description": "The tag values to be added.", + "type": "object", + "additionalProperties": true + }, + "ifAllMatch": { + "title": "If All Match", + "description": "The conditions that will be checked against. All must be true.", + "type": "object", + "additionalProperties": true + }, + "ifAnyMatch": { + "title": "If Any Match", + "description": "An array of ifAllMatch objects. Any individual ifAllMatch object may match, but each condition within an ifAllMatch object must be true", + "type": "array", + "additionalProperties": false + }, + "includeData": { + "title": "Include Data", + "description": "The data fields to include in the output", + "type": "object", + "additionalProperties": false + }, + "excludeData": { + "title": "Exclude Data", + "description": "The data fields to exclude in the output", + "type": "object", + "additionalProperties": false + }, + "locations": { + "title": "Location", + "description": "The location(s) to apply the action.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/location" + } + } + }, + "dependencies": { + "includeData": { + "allOf": [ + { + "required": ["locations"] + }, + { + "not": { "required": ["setTag"] } + }, + { + "not": { "required": ["excludeData"] } + } + ] + }, + "excludeData": { + "allOf": [ + { + "required": ["locations"] + }, + { + "not": { "required": ["setTag"] } + }, + { + "not": { "required": ["includeData"] } + } + ] + }, + "setTag": { + "allOf": [ + { + "not": { "required": ["includeData"] } + }, + { + "not": { "required": ["excludeData"] } + } + ] + }, + "ifAnyMatch": { + "allOf": [ + { + "not": { "required": ["ifAllMatch"] } + } + ] + }, + "ifAllMatch": { + "allOf": [ + { + "not": { "required": ["ifAnyMatch"] } + } + ] + } + }, + "additionalProperties": false, + "if": { + "required": [ "setTag" ], + "properties": { + "setTag": { + "anyOf": [ + { + "additionalProperties": { + "const": "`A`" + } + }, + { + "additionalProperties": { + "const": "`T`" + } + } + ] + } + } + }, + "then": { + "not": { + "required": ["locations"] + } + } + }, + "location": { + "title": "Location", + "description": "Used to specify a location in TS data. Use boolean type with value true to specify the location.", + "oneOf": [ + { + "type": "boolean", + "const": true + }, + { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/location" + } + } + ] + }, + "match": { + "$comment": "Defaults do not get applied for $ref objects, so place defaults alongside instead.", + "title": "Pattern to filter data", + "description": "", + "type": "string" + }, + "enableHostConnectivityCheck": { + "$comment": "This property can be used to enable/disable the host connectivity check in configurations where this is in effect", + "title": "Host", + "description": "" , + "type": "boolean" + }, + "allowSelfSignedCert": { + "$comment": "This property can be used by consumers, system pollers to enable/disable SSL Cert check", + "title": "Allow Self-Signed Certificate", + "description": "" , + "type": "boolean" + }, + "host": { + "$comment": "This property can be used by consumers, system pollers", + "title": "Host", + "description": "" , + "type": "string", + "anyOf": [ + { "format": "ipv4" }, + { "format": "ipv6" }, + { "format": "hostname" } + ], + "hostConnectivityCheck": true + }, + "port": { + "title": "Port", + "description": "" , + "type": "integer", + "minimum": 0, + "maximum": 65535 + }, + "protocol": { + "title": "Protocol", + "description": "" , + "type": "string", + "enum": [ "http", "https" ] + }, + "proxy": { + "title": "Proxy Configuration", + "description": "", + "type": "object", + "$async": true, + "dependencies": { + "passphrase": [ "username" ] + }, + "required": [ "host" ], + "properties": { + "host": { + "$ref": "#/definitions/host" + }, + "port": { + "default": 80, + "allOf": [ + { + "$ref": "#/definitions/port" + } + ] + }, + "protocol": { + "default": "http", + "allOf": [ + { + "$ref": "#/definitions/protocol" + } + ] + }, + "enableHostConnectivityCheck": { + "$ref": "#/definitions/enableHostConnectivityCheck" + }, + "allowSelfSignedCert": { + "$ref": "#/definitions/allowSelfSignedCert" + }, + "username": { + "$ref": "#/definitions/username" + }, + "passphrase": { + "$ref": "#/definitions/secret" + } + }, + "additionalProperties": false + } + }, + "properties": { + "class": { + "title": "Class", + "description": "Telemetry streaming top level class", + "type": "string", + "enum": [ "Telemetry" ] + }, + "schemaVersion": { + "title": "Schema version", + "description": "Version of ADC Declaration schema this declaration uses", + "type": "string", + "$comment": "IMPORTANT: In enum array, please put current schema version first, oldest-supported version last. Keep enum array sorted most-recent-first.", + "enum": [ "1.10.0", "1.9.0", "1.8.0", "1.7.0", "1.6.0", "1.5.0", "1.4.0", "1.3.0", "1.2.0", "1.1.0", "1.0.0", "0.9.0" ], + "default": "1.10.0" + }, + "$schema": { + "title": "Schema", + "description": "", + "type": "string" + }, + "scratch": { + "title": "Scratch", + "description": "Holds some system data during declaration processing", + "type": "object", + "properties": { + "expand": { + "title": "Expand", + "type": "boolean" + } + } + } + }, + "additionalProperties": { + "$comment": "AJV does not resolve defaults inside oneOf/anyOf, so instead use allOf. Any schema refs should also use allOf with an if/then/else on class", + "properties": { + "class": { + "title": "Class", + "type": "string", + "enum": [ + "Telemetry_System", + "Telemetry_System_Poller", + "Telemetry_Listener", + "Telemetry_Consumer", + "Telemetry_iHealth_Poller", + "Telemetry_Endpoints", + "Controls", + "Shared" + ] + } + }, + "allOf": [ + { + "$ref": "system_schema.json#" + }, + { + "$ref": "system_poller_schema.json#" + }, + { + "$ref": "listener_schema.json#" + }, + { + "$ref": "consumer_schema.json#" + }, + { + "$ref": "ihealth_poller_schema.json#" + }, + { + "$ref": "endpoints_schema.json#" + }, + { + "$ref": "controls_schema.json#" + }, + { + "$ref": "shared_schema.json#" + } + ] + }, + "required": [ + "class" + ] +} \ No newline at end of file diff --git a/src/schema/1.10.0/consumer_schema.json b/src/schema/1.10.0/consumer_schema.json new file mode 100644 index 00000000..192c8b86 --- /dev/null +++ b/src/schema/1.10.0/consumer_schema.json @@ -0,0 +1,499 @@ +{ + "$id": "consumer_schema.json", + "$async": true, + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Telemetry Streaming Consumer schema", + "description": "", + "type": "object", + "definitions": { + "host": { + "$comment": "Required for certain consumers: standard property", + "title": "Host", + "description": "FQDN or IP address" , + "type": "string", + "anyOf": [ + { "format": "ipv4" }, + { "format": "ipv6" }, + { "format": "hostname" } + ], + "hostConnectivityCheck": true + }, + "protocols": { + "$comment": "Required for certain consumers: standard property", + "title": "Protocols (all)", + "description": "" , + "type": "string", + "enum": [ "https", "http", "tcp", "udp", "binaryTcpTls", "binaryTcp" ] + }, + "port": { + "$comment": "Required for certain consumers: standard property", + "title": "Port", + "description": "" , + "type": "integer", + "minimum": 0, + "maximum": 65535 + }, + "path": { + "$async": true, + "$comment": "Required for certain consumers: standard property", + "title": "Path", + "description": "Path to post data to", + "type": ["string", "object"], + "f5expand": true, + "allOf": [ + { + "$ref": "base_schema.json#/definitions/stringOrSecret" + } + ] + }, + "method": { + "$comment": "Required for certain consumers: standard property", + "title": "Method", + "description": "HTTP method to use (limited to sensical choices)" , + "type": "string", + "enum": [ "POST", "GET", "PUT" ] + }, + "headers": { + "$async": true, + "$comment": "Required for certain consumers: standard property", + "title": "Headers", + "description": "HTTP headers to use" , + "type": "array", + "items": { + "properties": { + "name": { + "description": "Name of this header", + "type": "string", + "f5expand": true + }, + "value": { + "description": "Value of this header", + "type": ["string", "object"], + "f5expand": true, + "allOf": [ + { + "$ref": "base_schema.json#/definitions/stringOrSecret" + } + ] + } + }, + "required": [ + "name", + "value" + ], + "additionalProperties": false + } + }, + "format": { + "$comment": "Required for certain consumers: standard property", + "title": "Format (informs consumer additional formatting may be required)", + "description": "", + "type": "string", + "enum": [ "default", "legacy" ] + }, + "username": { + "$comment": "Required for certain consumers: standard property", + "title": "Username", + "description": "" , + "type": "string", + "f5expand": true + }, + "region": { + "$comment": "Required for certain consumers: AWS_CloudWatch, AWS_S3", + "title": "Region", + "description": "" , + "type": "string", + "f5expand": true + }, + "bucket": { + "$comment": "Required for certain consumers: AWS_S3", + "title": "Bucket", + "description": "" , + "type": "string", + "f5expand": true + }, + "logGroup": { + "$comment": "Required for certain consumers: AWS_CloudWatch", + "title": "Log Group", + "description": "" , + "type": "string", + "f5expand": true + }, + "logStream": { + "$comment": "Required for certain consumers: AWS_CloudWatch", + "title": "Log Stream", + "description": "" , + "type": "string", + "f5expand": true + }, + "workspaceId": { + "$comment": "Required for certain consumers: Azure_Log_Analytics", + "title": "Workspace ID", + "description": "" , + "type": "string", + "f5expand": true + }, + "topic": { + "$comment": "Required for certain consumers: Kafka", + "title": "Topic", + "description": "" , + "type": "string", + "f5expand": true + }, + "index": { + "$comment": "Required for certain consumers: ElasticSearch", + "title": "Index Name", + "description": "" , + "type": "string", + "f5expand": true + }, + "apiVersion": { + "$comment": "Required for certain consumers: ElasticSearch", + "title": "API Version", + "description": "" , + "type": "string", + "f5expand": true + }, + "dataType": { + "$comment": "Required for certain consumers: ElasticSearch", + "title": "Index/Data type", + "description": "" , + "type": "string", + "f5expand": true + }, + "authenticationProtocol": { + "$comment": "Required for certain consumers: Kafka", + "title": "Authentication Protocol", + "description": "" , + "type": "string", + "f5expand": true, + "enum": [ + "SASL-PLAIN", + "None" + ] + }, + "projectId": { + "$comment": "Required for certain consumers: Google_StackDriver", + "title": "Project ID", + "description": "The ID of the relevant project.", + "type": "string", + "f5expand": true + }, + "serviceEmail": { + "$comment": "Required for certain consumers: Google_StackDriver", + "title": "Service Email", + "description": "The service email.", + "type": "string", + "f5expand": true + }, + "privateKeyId": { + "$comment": "Required for certain consumers: Google_StackDriver", + "title": "Private Key ID", + "description": "The private key ID.", + "type": "string", + "f5expand": true + } + }, + "allOf": [ + { + "if": { "properties": { "class": { "const": "Telemetry_Consumer" } } }, + "then": { + "required": [ + "class", + "type" + ], + "properties": { + "class": { + "title": "Class", + "description": "Telemetry Streaming Consumer class", + "type": "string", + "enum": [ "Telemetry_Consumer" ] + }, + "enable": { + "default": true, + "allOf": [ + { + "$ref": "base_schema.json#/definitions/enable" + } + ] + }, + "trace": { + "default": false, + "allOf": [ + { + "$ref": "base_schema.json#/definitions/trace" + } + ] + }, + "type": { + "title": "Type", + "description": "" , + "type": "string", + "enum": [ "default", "Generic_HTTP", "Splunk", "Azure_Log_Analytics", "AWS_CloudWatch", "AWS_S3", "Graphite", "Kafka", "ElasticSearch", "Sumo_Logic", "Statsd", "Google_StackDriver" ] + }, + "enableHostConnectivityCheck": { + "$ref": "base_schema.json#/definitions/enableHostConnectivityCheck" + }, + "allowSelfSignedCert": { + "default": false, + "allOf": [ + { + "$ref": "base_schema.json#/definitions/allowSelfSignedCert" + } + ] + } + }, + "allOf": [ + { + "$comment": "This allows enforcement of no additional properties in this nested schema - could reuse above properties but prefer a seperate block", + "properties": { + "class": {}, + "enable": {}, + "trace": {}, + "type": {}, + "enableHostConnectivityCheck": {}, + "host": {}, + "protocol": {}, + "port": {}, + "path": {}, + "method": {}, + "headers": {}, + "allowSelfSignedCert": {}, + "username": {}, + "passphrase": {}, + "format": {}, + "workspaceId": {}, + "region": {}, + "logGroup": {}, + "logStream": {}, + "bucket": {}, + "topic": {}, + "apiVersion": {}, + "index": {}, + "dataType": {}, + "authenticationProtocol": {}, + "projectId": {}, + "serviceEmail": {}, + "privateKey": {}, + "privateKeyId": {} + }, + "additionalProperties": false + }, + { + "if": { "properties": { "type": { "const": "default" } } }, + "then": { + "required": [], + "properties": {} + }, + "else": {} + }, + { + "if": { "properties": { "type": { "const": "Generic_HTTP" } } }, + "then": { + "required": [ + "host" + ], + "properties": { + "host": { "$ref": "#/definitions/host" }, + "protocol": { "$ref": "#/definitions/protocols", "default": "https" }, + "port": { "$ref": "#/definitions/port", "default": 443 }, + "path": { "$ref": "#/definitions/path", "default": "/" }, + "method": { "$ref": "#/definitions/method", "default": "POST" }, + "headers": { "$ref": "#/definitions/headers" }, + "passphrase": { "$ref": "base_schema.json#/definitions/secret" } + } + }, + "else": {} + }, + { + "if": { "properties": { "type": { "const": "Splunk" } } }, + "then": { + "required": [ + "host", + "passphrase" + ], + "properties": { + "host": { "$ref": "#/definitions/host" }, + "protocol": { "$ref": "#/definitions/protocols", "default": "https" }, + "port": { "$ref": "#/definitions/port", "default": 8088 }, + "passphrase": { "$ref": "base_schema.json#/definitions/secret" }, + "format": { "$ref": "#/definitions/format", "default": "default" } + } + }, + "else": {} + }, + { + "if": { "properties": { "type": { "const": "Azure_Log_Analytics" } } }, + "then": { + "required": [ + "workspaceId", + "passphrase" + ], + "properties": { + "workspaceId": { "$ref": "#/definitions/workspaceId" }, + "passphrase": { "$ref": "base_schema.json#/definitions/secret" } + } + }, + "else": {} + }, + { + "if": { "properties": { "type": { "const": "AWS_CloudWatch" } } }, + "then": { + "required": [ + "region", + "logGroup", + "logStream" + ], + "properties": { + "region": { "$ref": "#/definitions/region" }, + "logGroup": { "$ref": "#/definitions/logGroup" }, + "logStream": { "$ref": "#/definitions/logStream" }, + "username": { "$ref": "#/definitions/username" }, + "passphrase": { "$ref": "base_schema.json#/definitions/secret" } + } + }, + "else": {} + }, + { + "if": { "properties": { "type": { "const": "AWS_S3" } } }, + "then": { + "required": [ + "region", + "bucket", + "username", + "passphrase" + ], + "properties": { + "region": { "$ref": "#/definitions/region" }, + "bucket": { "$ref": "#/definitions/bucket" }, + "username": { "$ref": "#/definitions/username" }, + "passphrase": { "$ref": "base_schema.json#/definitions/secret" } + } + }, + "else": {} + }, + { + "if": { "properties": { "type": { "const": "Graphite" } } }, + "then": { + "required": [ + "host" + ], + "properties": { + "host": { "$ref": "#/definitions/host" }, + "protocol": { "$ref": "#/definitions/protocols", "default": "https" }, + "port": { "$ref": "#/definitions/port", "default": 443 }, + "path": { "$ref": "#/definitions/path", "default": "/events/" } + } + }, + "else": {} + }, + { + "if": { "properties": { "type": { "const": "Kafka" } } }, + "then": { + "required": [ + "host", + "topic" + ], + "properties": { + "authenticationProtocol": { "$ref": "#/definitions/authenticationProtocol", "default": "None" }, + "host": { "$ref": "#/definitions/host" }, + "protocol": { "$ref": "#/definitions/protocols", "default": "binaryTcpTls" }, + "port": { "$ref": "#/definitions/port", "default": 9092 }, + "topic": { "$ref": "#/definitions/topic" } + }, + "allOf": [ + { + "if": { "properties": { "authenticationProtocol": { "const": "SASL-PLAIN" } } }, + "then": { + "required": [ + "username" + ], + "properties": { + "username": { "$ref": "#/definitions/username" }, + "passphrase": { "$ref": "base_schema.json#/definitions/secret" } + }, + "dependencies": { + "passphrase": [ "username" ] + } + } + } + ] + }, + "else": {} + }, + { + "if": { "properties": { "type": { "const": "ElasticSearch" } } }, + "then": { + "required": [ + "host", + "index" + ], + "properties": { + "host": { "$ref": "#/definitions/host" }, + "protocol": { "$ref": "#/definitions/protocols", "default": "https" }, + "port": { "$ref": "#/definitions/port", "default": 9200 }, + "path": { "$ref": "#/definitions/path" }, + "username": { "$ref": "#/definitions/username" }, + "passphrase": { "$ref": "base_schema.json#/definitions/secret" }, + "apiVersion": { "$ref": "#/definitions/apiVersion"}, + "index": { "$ref": "#/definitions/index" }, + "dataType": { "$ref": "#/definitions/dataType", "default": "f5.telemetry" } + } + }, + "else": {} + }, + { + "if": { "properties": { "type": { "const": "Sumo_Logic" } } }, + "then": { + "required": [ + "host", + "passphrase" + ], + "properties": { + "host": { "$ref": "#/definitions/host" }, + "protocol": { "$ref": "#/definitions/protocols", "default": "https" }, + "port": { "$ref": "#/definitions/port", "default": 443 }, + "path": { "$ref": "#/definitions/path", "default": "/receiver/v1/http/" }, + "passphrase": { "$ref": "base_schema.json#/definitions/secret" } + } + }, + "else": {} + }, + { + "if": { "properties": { "type": { "const": "Statsd" } } }, + "then": { + "required": [ + "host" + ], + "properties": { + "host": { "$ref": "#/definitions/host" }, + "protocol": { "$ref": "#/definitions/protocols", "default": "udp" }, + "port": { "$ref": "#/definitions/port", "default": 8125 } + } + }, + "else": {} + }, + { + "if": { "properties": { "type": { "const": "Google_StackDriver" } } }, + "then": { + "required": [ + "projectId", + "privateKeyId", + "privateKey", + "serviceEmail" + ], + "properties": { + "privateKeyId": { "$ref": "#/definitions/privateKeyId" }, + "serviceEmail": { "$ref": "#/definitions/serviceEmail" }, + "privateKey": { "$ref": "base_schema.json#/definitions/secret" }, + "projectId": { "$ref": "#/definitions/projectId" } + } + }, + "else": {} + } + ] + }, + "else": {} + } + ] +} diff --git a/src/schema/1.10.0/controls_schema.json b/src/schema/1.10.0/controls_schema.json new file mode 100644 index 00000000..bf386308 --- /dev/null +++ b/src/schema/1.10.0/controls_schema.json @@ -0,0 +1,45 @@ +{ + "$id": "controls_schema.json", + "$async": true, + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Telemetry Streaming Controls schema", + "description": "", + "type": "object", + "allOf": [ + { + "if": { "properties": { "class": { "const": "Controls" } } }, + "then": { + "required": [ + "class" + ], + "properties": { + "class": { + "title": "Class", + "description": "Telemetry Streaming Controls class", + "type": "string", + "enum": [ "Controls" ] + }, + "logLevel": { + "title": "Logging Level", + "description": "", + "type": "string", + "default": "info", + "enum": [ + "debug", + "info", + "error" + ] + }, + "debug": { + "title": "Enable debug mode", + "description": "", + "type": "boolean", + "default": false + } + }, + "additionalProperties": false + }, + "else": {} + } + ] +} \ No newline at end of file diff --git a/src/schema/1.10.0/endpoints_schema.json b/src/schema/1.10.0/endpoints_schema.json new file mode 100644 index 00000000..60414755 --- /dev/null +++ b/src/schema/1.10.0/endpoints_schema.json @@ -0,0 +1,165 @@ +{ + "$id": "endpoints_schema.json", + "$async": true, + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Telemetry Streaming Endpoints schema", + "description": "", + "type": "object", + "definitions": { + "endpoint": { + "$async": true, + "title": "Telemetry Endpoint", + "description": "", + "type": "object", + "properties": { + "enable": { + "title": "Enable endpoint", + "default": true, + "allOf": [ + { + "$ref": "base_schema.json#/definitions/enable" + } + ] + }, + "name": { + "title": "Endpoint name", + "type": "string", + "minLength": 1 + }, + "path": { + "title": "Path to query data from", + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false + }, + "endpoints": { + "$async": true, + "title": "Telemetry Endpoints", + "description": "", + "type": "object", + "properties": { + "enable": { + "title": "Enable endpoints", + "default": true, + "allOf": [ + { + "$ref": "base_schema.json#/definitions/enable" + } + ] + }, + "basePath": { + "title": "Base Path", + "description": "Optional base path value to prepend to each individual endpoint paths", + "type": "string", + "default": "" + }, + "items": { + "title": "Items", + "description": "Object with each property an endpoint with their own properties", + "type": "object", + "additionalProperties": { + "allOf": [ + { + "$ref": "#/definitions/endpoint" + }, + { + "required": [ "path" ] + } + ] + }, + "minProperties": 1 + } + } + }, + "endpointsObjectRef": { + "$async": true, + "allOf": [ + { + "$ref": "#/definitions/endpoints" + }, + { + "properties": { + "enable": {}, + "basePath": {}, + "items": {} + }, + "required": [ "items" ], + "additionalProperties": false + } + ] + }, + "endpointObjectRef": { + "$async": true, + "allOf": [ + { + "$ref": "#/definitions/endpoint" + }, + { + "properties": { + "enable": {}, + "name": {}, + "path": {} + }, + "required": [ "name", "path" ], + "additionalProperties": false + } + ] + }, + "endpointsPointerRef": { + "$async": true, + "title": "Telemetry_Endpoints Name", + "description": "Name of the Telemetry_Endpoints object", + "type": "string", + "declarationClass": "Telemetry_Endpoints", + "minLength": 1 + }, + "endpointsItemPointerRef": { + "$async": true, + "title": "Telemetry_Endpoints Name and Item Key", + "description": "Name of the Telemetry_Endpoints object and the endpoint item key, e.g endpointsA/item1", + "type": "string", + "declarationClassProp": { + "path" :"Telemetry_Endpoints/items", + "partsNum": 2 + }, + "minLength": 1 + } + }, + "allOf": [ + { + "if": { "properties": { "class": { "const": "Telemetry_Endpoints" } } }, + "then": { + "required": [ + "class", + "items" + ], + "properties": { + "class": { + "title": "Class", + "description": "Telemetry Streaming Endpoints class", + "type": "string", + "enum": [ "Telemetry_Endpoints" ] + } + }, + "allOf": [ + { + "$comment": "This allows enforcement of no additional properties in this nested schema - could reuse above properties but prefer a separate block", + "properties": { + "class": {}, + "enable": {}, + "basePath": {}, + "items": {} + }, + "additionalProperties": false + }, + { + "$ref": "#/definitions/endpoints" + } + ] + }, + "else": {} + } + ] +} \ No newline at end of file diff --git a/src/schema/1.10.0/ihealth_poller_schema.json b/src/schema/1.10.0/ihealth_poller_schema.json new file mode 100644 index 00000000..596f98af --- /dev/null +++ b/src/schema/1.10.0/ihealth_poller_schema.json @@ -0,0 +1,240 @@ +{ + "$id": "ihealth_poller_schema.json", + "$async": true, + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Telemetry Streaming iHealth Poller schema", + "description": "", + "type": "object", + "definitions": { + "time24hr": { + "title": "Time in HH:MM, 24hr", + "description": "", + "type": "string", + "pattern": "^([0-9]|0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]?$" + }, + "iHealthPoller": { + "$comment": "system_schema.json should be updated when new property added", + "$async": true, + "title": "iHealth Poller", + "description": "", + "type": "object", + "required": [ + "interval", + "username", + "passphrase" + ], + "properties": { + "enable": { + "default": true, + "allOf": [ + { + "$ref": "base_schema.json#/definitions/enable" + } + ] + }, + "trace": { + "default": false, + "allOf": [ + { + "$ref": "base_schema.json#/definitions/trace" + } + ] + }, + "proxy": { + "title": "Proxy configuration", + "properties": { + "port": { + "default": 80 + }, + "protocol": { + "default": "http" + } + }, + "allOf": [ + { + "$ref": "base_schema.json#/definitions/proxy" + } + ] + }, + "username": { + "title": "iHealth Username", + "$ref": "base_schema.json#/definitions/username" + }, + "passphrase": { + "title": "iHealth Passphrase", + "$ref": "base_schema.json#/definitions/secret" + }, + "downloadFolder": { + "title": "Directory to download Qkview to", + "description": "", + "type": "string", + "minLength": 1, + "pathExists": true + }, + "interval": { + "title": "Operating interval", + "description": "" , + "type": "object", + "properties": { + "timeWindow": { + "title": "Two or more hours window in 24hr format that iHealth data can be sent", + "description": "", + "type": "object", + "properties": { + "start": { + "title": "Time when the window starts", + "$ref": "#/definitions/time24hr" + }, + "end": { + "title": "Time when the window ends", + "$ref": "#/definitions/time24hr" + } + }, + "timeWindowMinSize": 120, + "required": [ "start", "end" ], + "additionalProperties": false + }, + "frequency": { + "title": "Interval frequency", + "description": "", + "type": "string", + "default": "daily", + "enum": [ + "daily", + "weekly", + "monthly" + ] + } + + }, + "required": [ + "timeWindow" + ], + "allOf": [ + { + "if": { "properties": { "frequency": { "const": "daily" } } }, + "then": { + "properties": { + "timeWindow": {}, + "frequency": {} + }, + "additionalProperties": false + } + }, + { + "if": { "properties": { "frequency": { "const": "weekly" } } }, + "then": { + "properties": { + "timeWindow": {}, + "frequency": {}, + "day": { + "title": "", + "description": "", + "oneOf": [ + { + "type": "string", + "pattern": "^([mM]onday|[tT]uesday|[wW]ednesday|[tT]hursday|[fF]riday|[sS]aturday|[sS]unday)$" + }, + { + "$comment": "0 and 7 eq. Sunday", + "type": "integer", + "minimum": 0, + "maximum": 7 + } + ] + } + }, + "required": [ "day" ], + "additionalProperties": false + } + }, + { + "if": { "properties": { "frequency": { "const": "monthly" } } }, + "then": { + "properties": { + "timeWindow": {}, + "frequency": {}, + "day": { + "title": "", + "description": "", + "type": "integer", + "minimum": 1, + "maximum": 31 + } + }, + "required": [ "day" ], + "additionalProperties": false + } + } + ] + } + } + }, + "iHealthPollerPointerRef": { + "$async": true, + "type": "string", + "declarationClass": "Telemetry_iHealth_Poller" + }, + "iHealthPollerObjectRef": { + "$async": true, + "allOf": [ + { + "$comment": "This allows enforcement of no additional properties in this nested schema - could reuse above properties but prefer a seperate block", + "properties": { + "enable": {}, + "trace": {}, + "interval": {}, + "proxy": {}, + "username": {}, + "passphrase": {}, + "downloadFolder": {} + }, + "additionalProperties": false + }, + { + "$ref": "ihealth_poller_schema.json#/definitions/iHealthPoller" + } + ] + } + }, + "allOf": [ + { + "if": { "properties": { "class": { "const": "Telemetry_iHealth_Poller" } } }, + "then": { + "required": [ + "class", + "username", + "passphrase" + ], + "properties": { + "class": { + "title": "Class", + "description": "Telemetry Streaming iHealth Poller class", + "type": "string", + "enum": [ "Telemetry_iHealth_Poller" ] + } + }, + "allOf": [ + { + "$comment": "This allows enforcement of no additional properties in this nested schema - could reuse above properties but prefer a seperate block", + "properties": { + "class": {}, + "enable": {}, + "trace": {}, + "interval": {}, + "proxy": {}, + "username": {}, + "passphrase": {}, + "downloadFolder": {} + }, + "additionalProperties": false + }, + { + "$ref": "#/definitions/iHealthPoller" + } + ] + }, + "else": {} + } + ] +} \ No newline at end of file diff --git a/src/schema/1.10.0/listener_schema.json b/src/schema/1.10.0/listener_schema.json new file mode 100644 index 00000000..fb4f69cb --- /dev/null +++ b/src/schema/1.10.0/listener_schema.json @@ -0,0 +1,90 @@ +{ + "$id": "listener_schema.json", + "$async": true, + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Telemetry Streaming event listener schema", + "description": "", + "type": "object", + "allOf": [ + { + "if": { "properties": { "class": { "const": "Telemetry_Listener" } } }, + "then": { + "required": [ + "class" + ], + "properties": { + "class": { + "title": "Class", + "description": "Telemetry Streaming Event Listener class", + "type": "string", + "enum": [ "Telemetry_Listener" ] + }, + "enable": { + "default": true, + "allOf": [ + { + "$ref": "base_schema.json#/definitions/enable" + } + ] + }, + "trace": { + "default": false, + "allOf": [ + { + "$ref": "base_schema.json#/definitions/trace" + } + ] + }, + "port": { + "minimum": 1024, + "maximum": 65535, + "default": 6514, + "allOf": [ + { + "$ref": "base_schema.json#/definitions/port" + } + ] + }, + "tag": { + "$comment": "Deprecated! Use actions with a setTag action.", + "allOf": [ + { + "$ref": "base_schema.json#/definitions/tag" + } + ] + }, + "match": { + "default": "", + "allOf": [ + { + "$ref": "base_schema.json#/definitions/match" + } + ] + }, + "actions": { + "title": "Actions", + "description": "Actions to be performed on the listener.", + "type": "array", + "items": { + "allOf": [ + { + "$ref": "base_schema.json#/definitions/action" + } + ] + }, + "default": [ + { + "setTag": { + "tenant": "`T`", + "application": "`A`" + } + } + ] + } + }, + "additionalProperties": false + }, + "else": {} + } + ] +} \ No newline at end of file diff --git a/src/schema/1.10.0/shared_schema.json b/src/schema/1.10.0/shared_schema.json new file mode 100644 index 00000000..94056444 --- /dev/null +++ b/src/schema/1.10.0/shared_schema.json @@ -0,0 +1,51 @@ +{ + "$id": "shared_schema.json", + "$async": true, + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Telemetry streaming Shared schema", + "description": "", + "type": "object", + "allOf": [ + { + "if": { "properties": { "class": { "const": "Shared" } } }, + "then": { + "required": [ + "class" + ], + "properties": { + "class": { + "title": "Class", + "description": "Telemetry streaming Shared class", + "type": "string", + "enum": [ "Shared" ] + } + }, + "additionalProperties": { + "properties": { + "class": { + "title": "Class", + "type": "string", + "enum": [ + "Constants", + "Secret" + ] + } + }, + "allOf": [ + { + "if": { "properties": { "class": { "const": "Constants" } } }, + "then": { "$ref": "base_schema.json#/definitions/constants" }, + "else": {} + }, + { + "if": { "properties": { "class": { "const": "Secret" } } }, + "then": { "$ref": "base_schema.json#/definitions/secret" }, + "else": {} + } + ] + } + }, + "else": {} + } + ] +} \ No newline at end of file diff --git a/src/schema/1.10.0/system_poller_schema.json b/src/schema/1.10.0/system_poller_schema.json new file mode 100644 index 00000000..dc134e1b --- /dev/null +++ b/src/schema/1.10.0/system_poller_schema.json @@ -0,0 +1,237 @@ +{ + "$id": "system_poller_schema.json", + "$async": true, + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Telemetry Streaming system poller schema", + "description": "", + "type": "object", + "definitions": { + "systemPoller": { + "$comment": "system_schema.json should be updated when new property added", + "$async": true, + "title": "System Poller", + "description": "", + "type": "object", + "properties": { + "enable": { + "default": true, + "allOf": [ + { + "$ref": "base_schema.json#/definitions/enable" + } + ] + }, + "interval": { + "title": "Collection interval (in seconds)", + "description": "If endpointList is specified, minimum=1. Without endpointList, minimum=60 and maximum=60000" , + "type": "integer", + "default": 300 + }, + "trace": { + "$ref": "base_schema.json#/definitions/trace" + }, + "tag": { + "$comment": "Deprecated! Use actions with a setTag action.", + "allOf": [ + { + "$ref": "base_schema.json#/definitions/tag" + } + ] + }, + "actions": { + "title": "Actions", + "description": "Actions to be performed on the systemPoller.", + "type": "array", + "items": { + "allOf": [ + { + "$ref": "base_schema.json#/definitions/action" + } + ] + }, + "default": [ + { + "setTag": { + "tenant": "`T`", + "application": "`A`" + } + } + ] + }, + "endpointList": { + "title": "Endpoint List", + "description": "List of endpoints to use in data collection", + "oneOf": [ + { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "endpoints_schema.json#/definitions/endpointsPointerRef" + }, + { + "$ref": "endpoints_schema.json#/definitions/endpointsItemPointerRef" + }, + { + "if": { "required": [ "items" ]}, + "then": { + "$ref": "endpoints_schema.json#/definitions/endpointsObjectRef" + }, + "else": { + "$ref": "endpoints_schema.json#/definitions/endpointObjectRef" + } + } + + ] + }, + "minItems": 1 + }, + { + "$ref": "endpoints_schema.json#/definitions/endpointsPointerRef" + }, + { + "$ref": "endpoints_schema.json#/definitions/endpointsObjectRef" + } + ] + } + }, + "allOf": [ + { + "if": { "required": [ "endpointList" ] }, + "then": { + "properties": { + "interval": { + "minimum": 1 + } + } + }, + "else": { + "properties":{ + "interval": { + "minimum": 60, + "maximum": 6000 + } + } + } + } + ] + }, + "systemPollerPointerRef": { + "$async": true, + "type": "string", + "declarationClass": "Telemetry_System_Poller" + }, + "systemPollerObjectRef": { + "$async": true, + "allOf": [ + { + "$comment": "This allows enforcement of no additional properties in this nested schema - could reuse above properties but prefer a seperate block", + "properties": { + "enable": {}, + "trace": {}, + "interval": {}, + "tag": {}, + "actions": {}, + "endpointList": {} + }, + "additionalProperties": false + }, + { + "$ref": "#/definitions/systemPoller" + } + ] + } + }, + "allOf": [ + { + "if": { "properties": { "class": { "const": "Telemetry_System_Poller" } } }, + "then": { + "required": [ + "class" + ], + "properties": { + "class": { + "title": "Class", + "description": "Telemetry Streaming System Poller class", + "type": "string", + "enum": [ "Telemetry_System_Poller" ] + }, + "host": { + "$comment": "Deprecated! Use Telemetry_System to define target device", + "default": "localhost", + "allOf": [ + { + "$ref": "base_schema.json#/definitions/host" + } + ] + }, + "port": { + "$comment": "Deprecated! Use Telemetry_System to define target device", + "default": 8100, + "allOf": [ + { + "$ref": "base_schema.json#/definitions/port" + } + ] + }, + "protocol": { + "$comment": "Deprecated! Use Telemetry_System to define target device", + "default": "http", + "allOf": [ + { + "$ref": "base_schema.json#/definitions/protocol" + } + ] + }, + "allowSelfSignedCert": { + "$comment": "Deprecated! Use Telemetry_System to define target device", + "title": "Allow Self-Signed Certificate", + "default": false, + "allOf": [ + { + "$ref": "base_schema.json#/definitions/allowSelfSignedCert" + } + ] + }, + "enableHostConnectivityCheck": { + "$comment": "Deprecated! Use Telemetry_System to define target device", + "$ref": "base_schema.json#/definitions/enableHostConnectivityCheck" + }, + "username": { + "$comment": "Deprecated! Use Telemetry_System to define target device", + "$ref": "base_schema.json#/definitions/username" + }, + "passphrase": { + "$comment": "Deprecated! Use Telemetry_System to define target device", + "$ref": "base_schema.json#/definitions/secret" + } + }, + "allOf": [ + { + "$comment": "This allows enforcement of no additional properties in this nested schema - could reuse above properties but prefer a seperate block", + "properties": { + "class": {}, + "enable": {}, + "trace": {}, + "interval": {}, + "tag": {}, + "host": {}, + "port": {}, + "protocol": {}, + "allowSelfSignedCert": {}, + "enableHostConnectivityCheck": {}, + "username": {}, + "passphrase": {}, + "actions": {}, + "endpointList": {} + }, + "additionalProperties": false + }, + { + "$ref": "#/definitions/systemPoller" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/src/schema/1.10.0/system_schema.json b/src/schema/1.10.0/system_schema.json new file mode 100644 index 00000000..ccad163e --- /dev/null +++ b/src/schema/1.10.0/system_schema.json @@ -0,0 +1,122 @@ +{ + "$id": "system_schema.json", + "$async": true, + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Telemetry Streaming System schema", + "description": "", + "type": "object", + "allOf": [ + { + "if": { "properties": { "class": { "const": "Telemetry_System" } } }, + "then": { + "required": [ + "class" + ], + "properties": { + "class": { + "title": "Class", + "description": "Telemetry Streaming System class", + "type": "string", + "enum": [ "Telemetry_System" ] + }, + "enable": { + "title": "Enable all pollers attached to device", + "default": true, + "allOf": [ + { + "$ref": "base_schema.json#/definitions/enable" + } + ] + }, + "trace": { + "$ref": "base_schema.json#/definitions/trace" + }, + "host": { + "title": "System connection address", + "default": "localhost", + "allOf": [ + { + "$ref": "base_schema.json#/definitions/host" + } + ] + }, + "port": { + "title": "System connection port", + "default": 8100, + "allOf": [ + { + "$ref": "base_schema.json#/definitions/port" + } + ] + }, + "protocol": { + "title": "System connection protocol", + "default": "http", + "allOf": [ + { + "$ref": "base_schema.json#/definitions/protocol" + } + ] + }, + "allowSelfSignedCert": { + "title": "Allow Self-Signed Certificate", + "default": false, + "allOf": [ + { + "$ref": "base_schema.json#/definitions/allowSelfSignedCert" + } + ] + }, + "enableHostConnectivityCheck": { + "$ref": "base_schema.json#/definitions/enableHostConnectivityCheck" + }, + "username": { + "title": "System Username", + "$ref": "base_schema.json#/definitions/username" + }, + "passphrase": { + "title": "System Passphrase", + "$ref": "base_schema.json#/definitions/secret" + }, + "systemPoller": { + "title": "System Poller declaration", + "oneOf": [ + { + "$ref": "system_poller_schema.json#/definitions/systemPollerPointerRef" + }, + { + "$ref": "system_poller_schema.json#/definitions/systemPollerObjectRef" + }, + { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "system_poller_schema.json#/definitions/systemPollerObjectRef" + }, + { + "$ref": "system_poller_schema.json#/definitions/systemPollerPointerRef" + } + ] + }, + "minItems": 1 + } + ] + }, + "iHealthPoller": { + "title": "iHealth Poller declaration", + "oneOf": [ + { + "$ref": "ihealth_poller_schema.json#/definitions/iHealthPollerPointerRef" + }, + { + "$ref": "ihealth_poller_schema.json#/definitions/iHealthPollerObjectRef" + } + ] + } + }, + "additionalProperties": false + } + } + ] +} \ No newline at end of file diff --git a/src/schema/latest/base_schema.json b/src/schema/latest/base_schema.json index 67e5679b..8d695794 100644 --- a/src/schema/latest/base_schema.json +++ b/src/schema/latest/base_schema.json @@ -124,6 +124,12 @@ "type": "object", "additionalProperties": true }, + "ifAnyMatch": { + "title": "If Any Match", + "description": "An array of ifAllMatch objects. Any individual ifAllMatch object may match, but each condition within an ifAllMatch object must be true", + "type": "array", + "additionalProperties": false + }, "includeData": { "title": "Include Data", "description": "The data fields to include in the output", @@ -181,6 +187,20 @@ "not": { "required": ["excludeData"] } } ] + }, + "ifAnyMatch": { + "allOf": [ + { + "not": { "required": ["ifAllMatch"] } + } + ] + }, + "ifAllMatch": { + "allOf": [ + { + "not": { "required": ["ifAnyMatch"] } + } + ] } }, "additionalProperties": false, @@ -325,8 +345,8 @@ "description": "Version of ADC Declaration schema this declaration uses", "type": "string", "$comment": "IMPORTANT: In enum array, please put current schema version first, oldest-supported version last. Keep enum array sorted most-recent-first.", - "enum": [ "1.9.0", "1.8.0", "1.7.0", "1.6.0", "1.5.0", "1.4.0", "1.3.0", "1.2.0", "1.1.0", "1.0.0", "0.9.0" ], - "default": "1.9.0" + "enum": [ "1.10.0", "1.9.0", "1.8.0", "1.7.0", "1.6.0", "1.5.0", "1.4.0", "1.3.0", "1.2.0", "1.1.0", "1.0.0", "0.9.0" ], + "default": "1.10.0" }, "$schema": { "title": "Schema", @@ -357,6 +377,7 @@ "Telemetry_Listener", "Telemetry_Consumer", "Telemetry_iHealth_Poller", + "Telemetry_Endpoints", "Controls", "Shared" ] @@ -378,6 +399,9 @@ { "$ref": "ihealth_poller_schema.json#" }, + { + "$ref": "endpoints_schema.json#" + }, { "$ref": "controls_schema.json#" }, diff --git a/src/schema/latest/consumer_schema.json b/src/schema/latest/consumer_schema.json index 242f5a2c..192c8b86 100644 --- a/src/schema/latest/consumer_schema.json +++ b/src/schema/latest/consumer_schema.json @@ -209,16 +209,39 @@ "type": "string", "enum": [ "Telemetry_Consumer" ] }, - "enable": { "$ref": "base_schema.json#/definitions/enable", "default": true }, - "trace": { "$ref": "base_schema.json#/definitions/trace", "default": false }, + "enable": { + "default": true, + "allOf": [ + { + "$ref": "base_schema.json#/definitions/enable" + } + ] + }, + "trace": { + "default": false, + "allOf": [ + { + "$ref": "base_schema.json#/definitions/trace" + } + ] + }, "type": { "title": "Type", "description": "" , "type": "string", "enum": [ "default", "Generic_HTTP", "Splunk", "Azure_Log_Analytics", "AWS_CloudWatch", "AWS_S3", "Graphite", "Kafka", "ElasticSearch", "Sumo_Logic", "Statsd", "Google_StackDriver" ] }, - "enableHostConnectivityCheck": { "$ref": "base_schema.json#/definitions/enableHostConnectivityCheck" }, - "allowSelfSignedCert": { "$ref": "base_schema.json#/definitions/allowSelfSignedCert" } + "enableHostConnectivityCheck": { + "$ref": "base_schema.json#/definitions/enableHostConnectivityCheck" + }, + "allowSelfSignedCert": { + "default": false, + "allOf": [ + { + "$ref": "base_schema.json#/definitions/allowSelfSignedCert" + } + ] + } }, "allOf": [ { diff --git a/src/schema/latest/endpoints_schema.json b/src/schema/latest/endpoints_schema.json new file mode 100644 index 00000000..60414755 --- /dev/null +++ b/src/schema/latest/endpoints_schema.json @@ -0,0 +1,165 @@ +{ + "$id": "endpoints_schema.json", + "$async": true, + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Telemetry Streaming Endpoints schema", + "description": "", + "type": "object", + "definitions": { + "endpoint": { + "$async": true, + "title": "Telemetry Endpoint", + "description": "", + "type": "object", + "properties": { + "enable": { + "title": "Enable endpoint", + "default": true, + "allOf": [ + { + "$ref": "base_schema.json#/definitions/enable" + } + ] + }, + "name": { + "title": "Endpoint name", + "type": "string", + "minLength": 1 + }, + "path": { + "title": "Path to query data from", + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false + }, + "endpoints": { + "$async": true, + "title": "Telemetry Endpoints", + "description": "", + "type": "object", + "properties": { + "enable": { + "title": "Enable endpoints", + "default": true, + "allOf": [ + { + "$ref": "base_schema.json#/definitions/enable" + } + ] + }, + "basePath": { + "title": "Base Path", + "description": "Optional base path value to prepend to each individual endpoint paths", + "type": "string", + "default": "" + }, + "items": { + "title": "Items", + "description": "Object with each property an endpoint with their own properties", + "type": "object", + "additionalProperties": { + "allOf": [ + { + "$ref": "#/definitions/endpoint" + }, + { + "required": [ "path" ] + } + ] + }, + "minProperties": 1 + } + } + }, + "endpointsObjectRef": { + "$async": true, + "allOf": [ + { + "$ref": "#/definitions/endpoints" + }, + { + "properties": { + "enable": {}, + "basePath": {}, + "items": {} + }, + "required": [ "items" ], + "additionalProperties": false + } + ] + }, + "endpointObjectRef": { + "$async": true, + "allOf": [ + { + "$ref": "#/definitions/endpoint" + }, + { + "properties": { + "enable": {}, + "name": {}, + "path": {} + }, + "required": [ "name", "path" ], + "additionalProperties": false + } + ] + }, + "endpointsPointerRef": { + "$async": true, + "title": "Telemetry_Endpoints Name", + "description": "Name of the Telemetry_Endpoints object", + "type": "string", + "declarationClass": "Telemetry_Endpoints", + "minLength": 1 + }, + "endpointsItemPointerRef": { + "$async": true, + "title": "Telemetry_Endpoints Name and Item Key", + "description": "Name of the Telemetry_Endpoints object and the endpoint item key, e.g endpointsA/item1", + "type": "string", + "declarationClassProp": { + "path" :"Telemetry_Endpoints/items", + "partsNum": 2 + }, + "minLength": 1 + } + }, + "allOf": [ + { + "if": { "properties": { "class": { "const": "Telemetry_Endpoints" } } }, + "then": { + "required": [ + "class", + "items" + ], + "properties": { + "class": { + "title": "Class", + "description": "Telemetry Streaming Endpoints class", + "type": "string", + "enum": [ "Telemetry_Endpoints" ] + } + }, + "allOf": [ + { + "$comment": "This allows enforcement of no additional properties in this nested schema - could reuse above properties but prefer a separate block", + "properties": { + "class": {}, + "enable": {}, + "basePath": {}, + "items": {} + }, + "additionalProperties": false + }, + { + "$ref": "#/definitions/endpoints" + } + ] + }, + "else": {} + } + ] +} \ No newline at end of file diff --git a/src/schema/latest/ihealth_poller_schema.json b/src/schema/latest/ihealth_poller_schema.json index bb0a1051..596f98af 100644 --- a/src/schema/latest/ihealth_poller_schema.json +++ b/src/schema/latest/ihealth_poller_schema.json @@ -169,6 +169,32 @@ ] } } + }, + "iHealthPollerPointerRef": { + "$async": true, + "type": "string", + "declarationClass": "Telemetry_iHealth_Poller" + }, + "iHealthPollerObjectRef": { + "$async": true, + "allOf": [ + { + "$comment": "This allows enforcement of no additional properties in this nested schema - could reuse above properties but prefer a seperate block", + "properties": { + "enable": {}, + "trace": {}, + "interval": {}, + "proxy": {}, + "username": {}, + "passphrase": {}, + "downloadFolder": {} + }, + "additionalProperties": false + }, + { + "$ref": "ihealth_poller_schema.json#/definitions/iHealthPoller" + } + ] } }, "allOf": [ diff --git a/src/schema/latest/system_poller_schema.json b/src/schema/latest/system_poller_schema.json index 84fddf17..dc134e1b 100644 --- a/src/schema/latest/system_poller_schema.json +++ b/src/schema/latest/system_poller_schema.json @@ -21,22 +21,15 @@ } ] }, - "trace": { - "default": false, - "allOf": [ - { - "$ref": "base_schema.json#/definitions/trace" - } - ] - }, "interval": { "title": "Collection interval (in seconds)", - "description": "" , + "description": "If endpointList is specified, minimum=1. Without endpointList, minimum=60 and maximum=60000" , "type": "integer", - "minimum": 60, - "maximum": 6000, "default": 300 }, + "trace": { + "$ref": "base_schema.json#/definitions/trace" + }, "tag": { "$comment": "Deprecated! Use actions with a setTag action.", "allOf": [ @@ -64,8 +57,89 @@ } } ] + }, + "endpointList": { + "title": "Endpoint List", + "description": "List of endpoints to use in data collection", + "oneOf": [ + { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "endpoints_schema.json#/definitions/endpointsPointerRef" + }, + { + "$ref": "endpoints_schema.json#/definitions/endpointsItemPointerRef" + }, + { + "if": { "required": [ "items" ]}, + "then": { + "$ref": "endpoints_schema.json#/definitions/endpointsObjectRef" + }, + "else": { + "$ref": "endpoints_schema.json#/definitions/endpointObjectRef" + } + } + + ] + }, + "minItems": 1 + }, + { + "$ref": "endpoints_schema.json#/definitions/endpointsPointerRef" + }, + { + "$ref": "endpoints_schema.json#/definitions/endpointsObjectRef" + } + ] } - } + }, + "allOf": [ + { + "if": { "required": [ "endpointList" ] }, + "then": { + "properties": { + "interval": { + "minimum": 1 + } + } + }, + "else": { + "properties":{ + "interval": { + "minimum": 60, + "maximum": 6000 + } + } + } + } + ] + }, + "systemPollerPointerRef": { + "$async": true, + "type": "string", + "declarationClass": "Telemetry_System_Poller" + }, + "systemPollerObjectRef": { + "$async": true, + "allOf": [ + { + "$comment": "This allows enforcement of no additional properties in this nested schema - could reuse above properties but prefer a seperate block", + "properties": { + "enable": {}, + "trace": {}, + "interval": {}, + "tag": {}, + "actions": {}, + "endpointList": {} + }, + "additionalProperties": false + }, + { + "$ref": "#/definitions/systemPoller" + } + ] } }, "allOf": [ @@ -111,7 +185,13 @@ }, "allowSelfSignedCert": { "$comment": "Deprecated! Use Telemetry_System to define target device", - "$ref": "base_schema.json#/definitions/allowSelfSignedCert" + "title": "Allow Self-Signed Certificate", + "default": false, + "allOf": [ + { + "$ref": "base_schema.json#/definitions/allowSelfSignedCert" + } + ] }, "enableHostConnectivityCheck": { "$comment": "Deprecated! Use Telemetry_System to define target device", @@ -142,7 +222,8 @@ "enableHostConnectivityCheck": {}, "username": {}, "passphrase": {}, - "actions": {} + "actions": {}, + "endpointList": {} }, "additionalProperties": false }, diff --git a/src/schema/latest/system_schema.json b/src/schema/latest/system_schema.json index 47aa3e3c..ccad163e 100644 --- a/src/schema/latest/system_schema.json +++ b/src/schema/latest/system_schema.json @@ -29,13 +29,7 @@ ] }, "trace": { - "title": "Enable all pollers' tracers attached to device", - "default": false, - "allOf": [ - { - "$ref": "base_schema.json#/definitions/trace" - } - ] + "$ref": "base_schema.json#/definitions/trace" }, "host": { "title": "System connection address", @@ -65,7 +59,13 @@ ] }, "allowSelfSignedCert": { - "$ref": "base_schema.json#/definitions/allowSelfSignedCert" + "title": "Allow Self-Signed Certificate", + "default": false, + "allOf": [ + { + "$ref": "base_schema.json#/definitions/allowSelfSignedCert" + } + ] }, "enableHostConnectivityCheck": { "$ref": "base_schema.json#/definitions/enableHostConnectivityCheck" @@ -82,26 +82,24 @@ "title": "System Poller declaration", "oneOf": [ { - "type": "string", - "declarationClass": "Telemetry_System_Poller" + "$ref": "system_poller_schema.json#/definitions/systemPollerPointerRef" }, { - "allOf": [ - { - "$comment": "This allows enforcement of no additional properties in this nested schema - could reuse above properties but prefer a seperate block", - "properties": { - "enable": {}, - "trace": {}, - "interval": {}, - "tag": {}, - "actions": {} + "$ref": "system_poller_schema.json#/definitions/systemPollerObjectRef" + }, + { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "system_poller_schema.json#/definitions/systemPollerObjectRef" }, - "additionalProperties": false - }, - { - "$ref": "system_poller_schema.json#/definitions/systemPoller" - } - ] + { + "$ref": "system_poller_schema.json#/definitions/systemPollerPointerRef" + } + ] + }, + "minItems": 1 } ] }, @@ -109,35 +107,16 @@ "title": "iHealth Poller declaration", "oneOf": [ { - "type": "string", - "declarationClass": "Telemetry_iHealth_Poller" + "$ref": "ihealth_poller_schema.json#/definitions/iHealthPollerPointerRef" }, { - "allOf": [ - { - "$comment": "This allows enforcement of no additional properties in this nested schema - could reuse above properties but prefer a seperate block", - "properties": { - "enable": {}, - "trace": {}, - "interval": {}, - "proxy": {}, - "username": {}, - "passphrase": {}, - "downloadFolder": {} - }, - "additionalProperties": false - }, - { - "$ref": "ihealth_poller_schema.json#/definitions/iHealthPoller" - } - ] + "$ref": "ihealth_poller_schema.json#/definitions/iHealthPollerObjectRef" } ] } }, "additionalProperties": false - }, - "else": {} + } } ] } \ No newline at end of file diff --git a/test/README.md b/test/README.md index 34e17929..85cc407e 100644 --- a/test/README.md +++ b/test/README.md @@ -16,7 +16,11 @@ Triggered: Every commit pushed to central repository. Best practices: - Create a separate ```*Test.js``` for each source file being tested. -- Keep mocking simple: Simply overwrite the dependent module's function(s) after a require where possible, which is most of the time. Note: Mocha consolidates requires between test files, place require inside the ```before``` function (or similar) to avoid this behavior. +- Use ```sinon``` to mock module's function, property and etc. +- Use ```nock``` to mimic network interaction. +- Use ```assert``` from ```chai.assert``` for assertions. +- Put ```require('./shared/restoreCache')();``` on top of test file. See other ```*Test.js``` for examples. +- Avoid ```require``` statements inside of ```describe```, ```before```, ```beforeEach``` and etc. If you are really need it then it will be better to put all your imports along with ```require('./shared/restoreCache')()``` to ```before``` and do not forget to put ```require('./shared/restoreCache')()``` in ```after``` to restore ```require.cache``` state to avoid impact to other tests. This will still allows you to keep ```require.cache``` clean. - Keep the folder structure flat, this project is not that large or complex. - Monitor and enforce coverage, but avoid writing tests simply to increase coverage when there is no other perceived value. - With that being said, **enforce coverage** in automated test. diff --git a/test/customMochaReporter.js b/test/customMochaReporter.js index 87675d78..d75f1240 100644 --- a/test/customMochaReporter.js +++ b/test/customMochaReporter.js @@ -92,6 +92,10 @@ function CustomMochaReporter(runner, options) { fileLogger.info(`PASSED: ${test.title}`); }); + runner.on('retry', (test, err) => { + fileLogger.error(`RETRY-ERROR: ${test.title}\n${err.message || err}\n${err.stack}`); + }); + runner.on('fail', (test) => { failedTests += 1; currentTest.endTime = Date.now(); diff --git a/test/functional/consumerSystemTests.js b/test/functional/consumerSystemTests.js index abcca38e..b67a9b60 100644 --- a/test/functional/consumerSystemTests.js +++ b/test/functional/consumerSystemTests.js @@ -12,8 +12,8 @@ const fs = require('fs'); -const constants = require('./shared/constants.js'); -const util = require('./shared/util.js'); +const constants = require('./shared/constants'); +const util = require('./shared/util'); const consumerHost = util.getHosts('CONSUMER')[0]; // only expect one const checkDockerCmd = 'if [[ -e $(which docker) ]]; then echo exists; fi'; diff --git a/test/functional/consumersTests/azureLogAnalyticsTests.js b/test/functional/consumersTests/azureLogAnalyticsTests.js index 506bcf30..6a387904 100644 --- a/test/functional/consumersTests/azureLogAnalyticsTests.js +++ b/test/functional/consumersTests/azureLogAnalyticsTests.js @@ -12,9 +12,9 @@ const assert = require('assert'); const fs = require('fs'); -const util = require('../shared/util.js'); -const constants = require('../shared/constants.js'); -const dutUtils = require('../dutTests.js').utils; +const util = require('../shared/util'); +const constants = require('../shared/constants'); +const dutUtils = require('../dutTests').utils; const DUTS = util.getHosts('BIGIP'); diff --git a/test/functional/consumersTests/elasticsearchTests.js b/test/functional/consumersTests/elasticsearchTests.js index 3e48f45b..892130ff 100644 --- a/test/functional/consumersTests/elasticsearchTests.js +++ b/test/functional/consumersTests/elasticsearchTests.js @@ -12,9 +12,9 @@ const assert = require('assert'); const fs = require('fs'); -const util = require('../shared/util.js'); -const constants = require('../shared/constants.js'); -const dutUtils = require('../dutTests.js').utils; +const util = require('../shared/util'); +const constants = require('../shared/constants'); +const dutUtils = require('../dutTests').utils; // module requirements const MODULE_REQUIREMENTS = { DOCKER: true }; @@ -131,7 +131,7 @@ function test() { it('should retrieve SystemPoller data', () => dutUtils.getSystemPollerData(dut, constants.DECL.SYSTEM_NAME) .then((data) => { - systemPollerData = data; + systemPollerData = data[0]; assert.notStrictEqual(systemPollerData, undefined); assert.notStrictEqual(systemPollerData.system, undefined); })); diff --git a/test/functional/consumersTests/fluentdTests.js b/test/functional/consumersTests/fluentdTests.js index aee9b98d..15327dbb 100644 --- a/test/functional/consumersTests/fluentdTests.js +++ b/test/functional/consumersTests/fluentdTests.js @@ -12,9 +12,9 @@ const assert = require('assert'); const fs = require('fs'); -const util = require('../shared/util.js'); -const constants = require('../shared/constants.js'); -const dutUtils = require('../dutTests.js').utils; +const util = require('../shared/util'); +const constants = require('../shared/constants'); +const dutUtils = require('../dutTests').utils; // module requirements const MODULE_REQUIREMENTS = { DOCKER: true }; @@ -107,7 +107,7 @@ function test() { before(() => new Promise(resolve => setTimeout(resolve, 30 * 1000)) .then(() => dutUtils.getSystemPollersData((hostObj, data) => { - systemPollerData[hostObj.hostname] = data; + systemPollerData[hostObj.hostname] = data[0]; }))); it('should get log data from Fluentd stdout', () => runRemoteCmd(`docker logs ${FLUENTD_NAME}`) diff --git a/test/functional/consumersTests/googleStackDriverTests.js b/test/functional/consumersTests/googleStackDriverTests.js index 9f2c3f65..a06713c7 100644 --- a/test/functional/consumersTests/googleStackDriverTests.js +++ b/test/functional/consumersTests/googleStackDriverTests.js @@ -12,7 +12,7 @@ const assert = require('assert'); const fs = require('fs'); const jwt = require('jsonwebtoken'); const constants = require('../shared/constants'); -const dutUtils = require('../dutTests.js').utils; +const dutUtils = require('../dutTests').utils; const sharedUtil = require('../shared/util'); const util = require('../../../src/lib/util'); diff --git a/test/functional/consumersTests/kafkaTests.js b/test/functional/consumersTests/kafkaTests.js index 758f2365..5b52966a 100644 --- a/test/functional/consumersTests/kafkaTests.js +++ b/test/functional/consumersTests/kafkaTests.js @@ -13,9 +13,9 @@ const assert = require('assert'); const fs = require('fs'); const kafka = require('kafka-node'); -const util = require('../shared/util.js'); -const constants = require('../shared/constants.js'); -const dutUtils = require('../dutTests.js').utils; +const util = require('../shared/util'); +const constants = require('../shared/constants'); +const dutUtils = require('../dutTests').utils; // module requirements const MODULE_REQUIREMENTS = { DOCKER: true }; diff --git a/test/functional/consumersTests/splunkTests.js b/test/functional/consumersTests/splunkTests.js index 465b1745..f74e8e1a 100644 --- a/test/functional/consumersTests/splunkTests.js +++ b/test/functional/consumersTests/splunkTests.js @@ -12,9 +12,9 @@ const assert = require('assert'); const fs = require('fs'); -const util = require('../shared/util.js'); -const constants = require('../shared/constants.js'); -const dutUtils = require('../dutTests.js').utils; +const util = require('../shared/util'); +const constants = require('../shared/constants'); +const dutUtils = require('../dutTests').utils; // module requirements const MODULE_REQUIREMENTS = { DOCKER: true }; diff --git a/test/functional/consumersTests/statsdTests.js b/test/functional/consumersTests/statsdTests.js index ead42db2..b3061053 100644 --- a/test/functional/consumersTests/statsdTests.js +++ b/test/functional/consumersTests/statsdTests.js @@ -13,9 +13,9 @@ const assert = require('assert'); const fs = require('fs'); const deepDiff = require('deep-diff'); -const util = require('../shared/util.js'); -const constants = require('../shared/constants.js'); -const dutUtils = require('../dutTests.js').utils; +const util = require('../shared/util'); +const constants = require('../shared/constants'); +const dutUtils = require('../dutTests').utils; // module requirements const MODULE_REQUIREMENTS = { DOCKER: true }; @@ -120,7 +120,7 @@ function test() { }); }; - const getMerticsName = (data) => { + const getMetricsName = (data) => { const copyData = JSON.parse(JSON.stringify(data)); stripMetrics(copyData); const diff = deepDiff(copyData, data) || []; @@ -191,7 +191,7 @@ function test() { const sysPollerMetricsData = {}; it('should fetch system poller data via debug endpoint from DUTs', () => dutUtils.getSystemPollersData((hostObj, data) => { - sysPollerMetricsData[hostObj.hostname] = getMerticsName(data); + sysPollerMetricsData[hostObj.hostname] = getMetricsName(data[0]); })); DUTS.forEach((dut) => { diff --git a/test/functional/deployment/declaration.yml b/test/functional/deployment/declaration.yml index 76c690ee..abb10905 100644 --- a/test/functional/deployment/declaration.yml +++ b/test/functional/deployment/declaration.yml @@ -3,7 +3,9 @@ f5_do_ntp_config: true f5_do_provisioning: true f5_provisioning: ltm: nominal +f5_do_force_licensing: false f5_do_licensing: true +f5_do_phoning_home: false request: project: folder: /root/deploy-projects/_DEPLOYMENT_NAME_ diff --git a/test/functional/dutTests.js b/test/functional/dutTests.js index 8b1f9cf3..023be4a4 100644 --- a/test/functional/dutTests.js +++ b/test/functional/dutTests.js @@ -14,8 +14,8 @@ const assert = require('assert'); const fs = require('fs'); const net = require('net'); const readline = require('readline'); -const util = require('./shared/util.js'); -const constants = require('./shared/constants.js'); +const util = require('./shared/util'); +const constants = require('./shared/constants'); const baseILXUri = '/mgmt/shared/telemetry'; const duts = util.getHosts('BIGIP'); @@ -367,11 +367,38 @@ function test() { // read in example config const declaration = fs.readFileSync(constants.DECL.BASIC_EXAMPLE).toString(); + function searchCipherTexts(data, cb) { + const stack = [data]; + const forKey = (key) => { + const val = stack[0][key]; + if (key === 'cipherText') { + const ret = cb(val); + if (typeof ret !== 'undefined') { + stack[0][key] = ret; + } + } else if (typeof val === 'object' && val !== null) { + stack.push(val); + } + }; + while (stack.length) { + Object.keys(stack[0]).forEach(forKey); + stack.shift(); + } + } + function checkPassphraseObject(data) { - const passphrase = data.declaration[constants.DECL.CONSUMER_NAME].passphrase; // check that the declaration returned contains encrypted text // note: this only applies to TS running on BIG-IP (which is all we are testing for now) - assert.strictEqual(passphrase.cipherText.startsWith('$M'), true); + let secretsFound = 0; + searchCipherTexts(data, (cipherText) => { + secretsFound += 1; + assert.strictEqual(cipherText.startsWith('$M'), true, 'cipherText should start with $M$'); + }); + assert.notStrictEqual(secretsFound, 0, 'Expected at least 1 cipherText field'); + } + + function removeCipherTexts(data) { + searchCipherTexts(data, () => 'replacedSecret'); } // account for 1+ DUTs @@ -381,7 +408,6 @@ function test() { const user = item.username; const password = item.password; - let postResponse; let authToken = null; let options = {}; @@ -398,58 +424,59 @@ function test() { }; }); - it('should post configuration', () => { - const uri = `${baseILXUri}/declare`; - - const postOptions = { + it('should post same configuration twice and get it after', () => { + let uri = `${baseILXUri}/declare`; + const postOptions = Object.assign(util.deepCopy(options), { method: 'POST', - headers: options.headers, body: declaration - }; + }); + let postResponses = []; - return util.makeRequest(host, uri, postOptions) + return util.makeRequest(host, uri, util.deepCopy(postOptions)) .then((data) => { - util.logger.info('Declaration response:', { host, data }); + util.logger.info('POST request #1: Declaration response:', { host, data }); assert.strictEqual(data.message, 'success'); - checkPassphraseObject(data); - }); - }); - - it('should post configuration (again)', () => { - const uri = `${baseILXUri}/declare`; - const postOptions = { - method: 'POST', - headers: options.headers, - body: declaration - }; + checkPassphraseObject(data); + postResponses.push(data); - return util.makeRequest(host, uri, postOptions) + return util.makeRequest(host, uri, util.deepCopy(postOptions)); + }) .then((data) => { - util.logger.info('Declaration response:', { host, data }); - postResponse = data; // used later - }); - }); + util.logger.info('POST request #2: Declaration response:', { host, data }); + assert.strictEqual(data.message, 'success'); - it('should get configuration', () => { - const uri = `${baseILXUri}/declare`; + checkPassphraseObject(data); + postResponses.push(data); - return util.makeRequest(host, uri, options) + uri = `${baseILXUri}/declare`; + return util.makeRequest(host, uri, util.deepCopy(options)); + }) .then((data) => { - util.logger.info('Declaration response:', { host, data }); - assert.strictEqual(JSON.stringify(data.declaration), JSON.stringify(postResponse.declaration)); + util.logger.info('GET request: Declaration response:', { host, data }); + assert.strictEqual(data.message, 'success'); + checkPassphraseObject(data); + postResponses.push(data); + + // compare GET to recent POST + assert.deepStrictEqual(postResponses[2], postResponses[1]); + // lest compare first POST to second POST (only one difference is secrets) + postResponses = postResponses.map(removeCipherTexts); + assert.deepStrictEqual(postResponses[0], postResponses[1]); }); }); - it('should get systempoller info', () => { + it('should get response from systempoller endpoint', () => { const uri = `${baseILXUri}/systempoller/${constants.DECL.SYSTEM_NAME}`; return util.makeRequest(host, uri, options) .then((data) => { - data = data || {}; + data = data || []; util.logger.info(`SystemPoller response (${uri}):`, { host, data }); + assert.strictEqual(data.length, 1); // read schema and validate data + data = data[0]; const schema = JSON.parse(fs.readFileSync(constants.DECL.SYSTEM_POLLER_SCHEMA)); const valid = util.validateAgainstSchema(data, schema); if (valid !== true) { @@ -474,31 +501,28 @@ function test() { }); }); - it('should post a configuration containing system poller filtering', () => { - const filterDeclaration = fs.readFileSync(constants.DECL.FILTER_EXAMPLE).toString(); - const uri = `${baseILXUri}/declare`; - const postOptions = { + it('should apply configuration containing system poller filtering', () => { + let uri = `${baseILXUri}/declare`; + const postOptions = Object.assign(util.deepCopy(options), { method: 'POST', - headers: options.headers, - body: filterDeclaration - }; + body: fs.readFileSync(constants.DECL.FILTER_EXAMPLE).toString() + }); return util.makeRequest(host, uri, postOptions) .then((data) => { util.logger.info('Declaration response:', { host, data }); assert.strictEqual(data.message, 'success'); - }); - }); - - - it('should get filtered systempoller info', () => { - const uri = `${baseILXUri}/systempoller/${constants.DECL.SYSTEM_NAME}`; - return util.makeRequest(host, uri, options) + uri = `${baseILXUri}/systempoller/${constants.DECL.SYSTEM_NAME}`; + return util.makeRequest(host, uri, util.deepCopy(options)); + }) .then((data) => { - data = data || {}; + data = data || []; util.logger.info(`Filtered SystemPoller response (${uri}):`, { host, data }); + + assert.strictEqual(data.length, 1); // verify that certain data was filtered out, while other data was preserved + data = data[0]; assert.strictEqual(Object.keys(data.system).indexOf('provisioning'), -1); assert.strictEqual(Object.keys(data.system.diskStorage).indexOf('/usr'), -1); assert.notStrictEqual(Object.keys(data.system.diskStorage).indexOf('/'), -1); @@ -507,30 +531,27 @@ function test() { }); }); - it('should post a configuration containing chained system poller actions', () => { - const filterDeclaration = fs.readFileSync(constants.DECL.ACTION_CHAINING_EXAMPLE).toString(); - const uri = `${baseILXUri}/declare`; - const postOptions = { + it('should apply configuration containing chained system poller actions', () => { + let uri = `${baseILXUri}/declare`; + const postOptions = Object.assign(util.deepCopy(options), { method: 'POST', - headers: options.headers, - body: filterDeclaration - }; + body: fs.readFileSync(constants.DECL.ACTION_CHAINING_EXAMPLE).toString() + }); return util.makeRequest(host, uri, postOptions) .then((data) => { util.logger.info('Declaration response:', { host, data }); assert.strictEqual(data.message, 'success'); - }); - }); - - - it('should get filtered systempoller info', () => { - const uri = `${baseILXUri}/systempoller/${constants.DECL.SYSTEM_NAME}`; - return util.makeRequest(host, uri, options) + uri = `${baseILXUri}/systempoller/${constants.DECL.SYSTEM_NAME}`; + return util.makeRequest(host, uri, util.deepCopy(options)); + }) .then((data) => { data = data || {}; util.logger.info(`Filtered SystemPoller response (${uri}):`, { host, data }); + + assert.strictEqual(data.length, 1); + data = data[0]; // verify /var is included with, with 1_tagB removed assert.notStrictEqual(Object.keys(data.system.diskStorage).indexOf('/var'), -1); assert.deepEqual(data.system.diskStorage['/var']['1_tagB'], { '1_valueB_1': 'value1' }); @@ -539,6 +560,62 @@ function test() { assert.deepEqual(data.system.diskStorage['/var/log']['1_tagA'], 'myTag'); }); }); + + it('should apply configuration containing filters with ifAnyMatch', () => { + let uri = `${baseILXUri}/declare`; + const postOptions = Object.assign(util.deepCopy(options), { + method: 'POST', + body: fs.readFileSync(constants.DECL.FILTERING_WITH_MATCHING_EXAMPLE).toString() + }); + + return util.makeRequest(host, uri, postOptions) + .then((data) => { + util.logger.info('Declaration response:', { host, data }); + assert.strictEqual(data.message, 'success'); + + uri = `${baseILXUri}/systempoller/${constants.DECL.SYSTEM_NAME}`; + return util.makeRequest(host, uri, util.deepCopy(options)); + }) + .then((data) => { + data = data || {}; + util.logger.info(`Filtered and Matched SystemPoller response (${uri}):`, { host, data }); + + assert.strictEqual(data.length, 1); + data = data[0]; + // verify that 'system' key and child objects are included + assert.deepEqual(Object.keys(data), ['system']); + assert.ok(Object.keys(data.system).length > 1); + // verify that 'system.diskStorage' is NOT excluded + assert.notStrictEqual(Object.keys(data.system).indexOf('diskStorage'), -1); + }); + }); + + it('should apply configuration containing multiple system pollers and endpointList', () => { + let uri = `${baseILXUri}/declare`; + const postOptions = Object.assign(util.deepCopy(options), { + method: 'POST', + body: fs.readFileSync(constants.DECL.ENDPOINTLIST_EXAMPLE).toString() + }); + + return util.makeRequest(host, uri, postOptions) + .then((data) => { + util.logger.info('Declaration response:', { host, data }); + assert.strictEqual(data.message, 'success'); + + uri = `${baseILXUri}/systempoller/${constants.DECL.SYSTEM_NAME}`; + return util.makeRequest(host, uri, util.deepCopy(options)); + }) + .then((data) => { + util.logger.info(`System Poller with endpointList response (${uri}):`, { host, data }); + assert.ok(Array.isArray(data)); + + const pollerOneData = data[0]; + const pollerTwoData = data[1]; + assert.notStrictEqual(pollerOneData.custom_ipOther, undefined); + assert.notStrictEqual(pollerOneData.custom_dns, undefined); + assert.ok(pollerTwoData.custom_provisioning.items.length > 0); + }); + }); }); }); } diff --git a/test/functional/shared/constants.js b/test/functional/shared/constants.js index e5004929..801d958f 100644 --- a/test/functional/shared/constants.js +++ b/test/functional/shared/constants.js @@ -15,6 +15,8 @@ module.exports = { BASIC_EXAMPLE: `${__dirname}/basic.json`, FILTER_EXAMPLE: `${__dirname}/filter_system_poller.json`, ACTION_CHAINING_EXAMPLE: `${__dirname}/system_poller_chained_actions.json`, + FILTERING_WITH_MATCHING_EXAMPLE: `${__dirname}/system_poller_matched_filtering.json`, + ENDPOINTLIST_EXAMPLE: `${__dirname}/system_poller_endpointlist.json`, CONSUMER_NAME: 'My_Consumer', SYSTEM_NAME: 'My_System', SYSTEM_POLLER_SCHEMA: fs.realpathSync(`${__dirname}/../../../shared/output_schemas/system_poller_schema.json`) diff --git a/test/functional/shared/system_poller_endpointlist.json b/test/functional/shared/system_poller_endpointlist.json new file mode 100644 index 00000000..065fa1cd --- /dev/null +++ b/test/functional/shared/system_poller_endpointlist.json @@ -0,0 +1,38 @@ +{ + "class": "Telemetry", + "controls": { + "class": "Controls", + "logLevel": "debug", + "debug": true + }, + "My_Endpoints": { + "class": "Telemetry_Endpoints", + "basePath": "mgmt/tm/ltm/profile", + "items": { + "custom_ipOther": { + "path": "ipother/stats" + }, + "custom_dns": { + "path": "dns/stats" + } + } + }, + "My_System": { + "class": "Telemetry_System", + "systemPoller": [ + { + "interval": 30, + "endpointList": "My_Endpoints" + }, + { + "interval": 12000, + "endpointList": [ + { + "name": "custom_provisioning", + "path": "/mgmt/tm/sys/provision" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/test/functional/shared/system_poller_matched_filtering.json b/test/functional/shared/system_poller_matched_filtering.json new file mode 100644 index 00000000..910bbc79 --- /dev/null +++ b/test/functional/shared/system_poller_matched_filtering.json @@ -0,0 +1,48 @@ +{ + "class": "Telemetry", + "controls": { + "class": "Controls", + "logLevel": "debug", + "debug": true + }, + "My_System": { + "class": "Telemetry_System", + "systemPoller": { + "interval": 60, + "actions": [ + { + "includeData": {}, + "ifAnyMatch": [ + { + "system": { + "configReady": "shouldnotmatch" + } + }, + { + "system": { + "configReady": "yes" + } + } + ], + "locations": { + "system": true + } + }, + { + "ifAllMatch": { + "system": { + "version": "shouldnotmatch" + } + }, + "excludeData": {}, + "enable": true, + "locations": { + "system": { + "diskStorage": true + } + } + } + ] + } + } +} \ No newline at end of file diff --git a/test/functional/shared/util.js b/test/functional/shared/util.js index e13abba0..edad0fec 100644 --- a/test/functional/shared/util.js +++ b/test/functional/shared/util.js @@ -15,8 +15,8 @@ const net = require('net'); const request = require('request'); const SSHClient = require('ssh2').Client; // eslint-disable-line import/no-extraneous-dependencies -const constants = require('./constants.js'); -const logger = require('../../winstonLogger.js').logger; +const constants = require('./constants'); +const logger = require('../../winstonLogger').logger; /** * Allows calling makeRequest with retryOptions diff --git a/test/functional/testRunner.js b/test/functional/testRunner.js index d1b05f06..e793a50c 100644 --- a/test/functional/testRunner.js +++ b/test/functional/testRunner.js @@ -11,10 +11,10 @@ /* eslint-disable no-console */ // initliaze logger -const util = require('./shared/util.js'); // eslint-disable-line -const constants = require('./shared/constants.js'); -const dutTests = require('./dutTests.js'); -const consumerHostTests = require('./consumerSystemTests.js'); +const util = require('./shared/util'); // eslint-disable-line +const constants = require('./shared/constants'); +const dutTests = require('./dutTests'); +const consumerHostTests = require('./consumerSystemTests'); const skipDut = process.env[constants.ENV_VARS.TEST_CONTROLS.SKIP_DUT_TESTS]; const skipConsumer = process.env[constants.ENV_VARS.TEST_CONTROLS.SKIP_CONSUMER_TESTS]; diff --git a/test/unit/.mocha.opts b/test/unit/.mocha.opts index 05169c10..5068714a 100644 --- a/test/unit/.mocha.opts +++ b/test/unit/.mocha.opts @@ -1,3 +1,5 @@ # mocha.opts + --require ./test/unit/shared/bootstrap.js + --recursive ./test/unit/**/*.js --timeout 600000 --reporter ./test/customMochaReporter.js \ No newline at end of file diff --git a/test/unit/configTests.js b/test/unit/configTests.js index 8002c879..2c252c60 100644 --- a/test/unit/configTests.js +++ b/test/unit/configTests.js @@ -8,40 +8,28 @@ 'use strict'; -const sinon = require('sinon'); +/* eslint-disable import/order */ + +require('./shared/restoreCache')(); + const chai = require('chai'); const chaiAsPromised = require('chai-as-promised'); +const sinon = require('sinon'); const TeemDevice = require('@f5devcentral/f5-teem').Device; +const config = require('../../src/lib/config'); +const constants = require('../../src/lib/constants'); +const deviceUtil = require('../../src/lib/deviceUtil'); +const psModule = require('../../src/lib/persistentStorage'); +const util = require('../../src/lib/util'); +const MockRestOperation = require('./shared/util').MockRestOperation; + chai.use(chaiAsPromised); const assert = chai.assert; -const constants = require('../../src/lib/constants.js'); - -/* eslint-disable global-require */ - -function MockRestOperation(opts) { - this.method = opts.method || 'GET'; - this.body = opts.body; - this.statusCode = null; -} -MockRestOperation.prototype.getMethod = function () { return this.method; }; -MockRestOperation.prototype.setMethod = function (method) { this.method = method; }; -MockRestOperation.prototype.getBody = function () { return this.body; }; -MockRestOperation.prototype.setBody = function (body) { this.body = body; }; -MockRestOperation.prototype.getStatusCode = function () { return this.statusCode; }; -MockRestOperation.prototype.setStatusCode = function (code) { this.statusCode = code; }; -MockRestOperation.prototype.complete = function () { }; - - describe('Config', () => { let persistentStorage; - let config; - let util; - let deviceUtil; - - let configValidator; - let formatConfig; + let restStorage; const baseState = { _data_: { @@ -53,49 +41,20 @@ describe('Config', () => { }; before(() => { - const psModule = require('../../src/lib/persistentStorage.js'); - config = require('../../src/lib/config.js'); - util = require('../../src/lib/util.js'); - deviceUtil = require('../../src/lib/deviceUtil.js'); - - const restWorker = { - loadState: (cb) => { cb(null, baseState); }, - saveState: (first, state, cb) => { cb(null); } - }; persistentStorage = psModule.persistentStorage; - persistentStorage.storage = new psModule.RestStorage(restWorker); - - configValidator = config.validator; - - formatConfig = util.formatConfig; - }); - beforeEach(() => { - persistentStorage.storage._cache = JSON.parse(JSON.stringify(baseState)); - }); - afterEach(() => { - config.validator = configValidator; - util.formatConfig = formatConfig; - sinon.restore(); - }); - after(() => { - Object.keys(require.cache).forEach((key) => { - delete require.cache[key]; + restStorage = new psModule.RestStorage({ + loadState: (first, cb) => { cb(null, baseState); }, + saveState: (first, state, cb) => { cb(null); } }); }); - it('should validate basic declaration', () => { - const obj = { - class: 'Telemetry' - }; - return config.validate(obj); + beforeEach(() => { + sinon.stub(persistentStorage, 'storage').value(restStorage); + restStorage._cache = JSON.parse(JSON.stringify(baseState)); }); - it('should throw error in validate function', () => { - const obj = { - class: 'Telemetry' - }; - config.validator = null; - return assert.isRejected(config.validate(obj), 'Validator is not available'); + afterEach(() => { + sinon.restore(); }); it('should compile schema', () => { @@ -103,157 +62,168 @@ describe('Config', () => { assert.strictEqual(typeof compiledSchema, 'function'); }); - it('should validate and apply basic declaration', () => { - const obj = { - class: 'Telemetry' - }; - const validatedObj = { - class: 'Telemetry', - schemaVersion: constants.VERSION - }; - return config.validateAndApply(obj) - .then((data) => { - assert.deepEqual(data, validatedObj); - }) - .catch(err => Promise.reject(err)); - }); + describe('.validate()', () => { + it('should validate basic declaration', () => { + const obj = { + class: 'Telemetry' + }; + return assert.isFulfilled(config.validate(obj)); + }); - it('should process client POST request', () => { - const mockRestOperation = new MockRestOperation({ method: 'POST' }); - mockRestOperation.setBody({ - class: 'Telemetry' + it('should throw error in validate function', () => { + const obj = { + class: 'Telemetry' + }; + sinon.stub(config, 'validator').value(null); + return assert.isRejected(config.validate(obj), 'Validator is not available'); }); + }); - const actualResponseBody = { - message: 'success', - declaration: { + describe('.validateAndApply()', () => { + it('should validate and apply basic declaration', () => { + const obj = { + class: 'Telemetry' + }; + const validatedObj = { class: 'Telemetry', schemaVersion: constants.VERSION - } - }; - return config.processClientRequest(mockRestOperation) - .then(() => { - assert.strictEqual(mockRestOperation.statusCode, 200); - assert.deepEqual(mockRestOperation.body, actualResponseBody); - }) - .catch(err => Promise.reject(err)); + }; + return assert.becomes(config.validateAndApply(obj), validatedObj); + }); }); - it('should process client GET request - no configuration', () => { - const actualResponseBody = { - message: 'success', - declaration: {} - }; - - const mockRestOperation = new MockRestOperation({ method: 'GET' }); - mockRestOperation.setBody({}); - - return config.processClientRequest(mockRestOperation) - .then(() => { - assert.strictEqual(mockRestOperation.statusCode, 200); - assert.deepEqual(mockRestOperation.body, actualResponseBody); + describe('.processClientRequest()', () => { + it('should process client POST request', () => { + const mockRestOperation = new MockRestOperation({ method: 'POST' }); + mockRestOperation.setBody({ + class: 'Telemetry' }); - }); + const actualResponseBody = { + message: 'success', + declaration: { + class: 'Telemetry', + schemaVersion: constants.VERSION + } + }; + return assert.isFulfilled(config.processClientRequest(mockRestOperation) + .then(() => { + assert.strictEqual(mockRestOperation.statusCode, 200); + assert.deepStrictEqual(mockRestOperation.body, actualResponseBody); + })); + }); - it('should process client GET request - existing config', () => { - const mockRestOperationPOST = new MockRestOperation({ method: 'POST' }); - mockRestOperationPOST.setBody({ - class: 'Telemetry' + it('should process client GET request - no configuration', () => { + const actualResponseBody = { + message: 'success', + declaration: {} + }; + const mockRestOperation = new MockRestOperation({ method: 'GET' }); + mockRestOperation.setBody({}); + + return assert.isFulfilled(config.processClientRequest(mockRestOperation) + .then(() => { + assert.strictEqual(mockRestOperation.statusCode, 200); + assert.deepStrictEqual(mockRestOperation.body, actualResponseBody); + })); }); - const mockRestOperationGET = new MockRestOperation({ method: 'GET' }); - mockRestOperationGET.setBody({}); - - return config.processClientRequest(mockRestOperationPOST) - .then(() => { - assert.strictEqual(mockRestOperationPOST.statusCode, 200); - return config.processClientRequest(mockRestOperationGET); - }) - .then(() => { - assert.strictEqual(mockRestOperationGET.statusCode, 200); - assert.deepEqual(mockRestOperationGET.body, mockRestOperationPOST.body); + it('should process client GET request - existing config', () => { + const mockRestOperationPOST = new MockRestOperation({ method: 'POST' }); + mockRestOperationPOST.setBody({ + class: 'Telemetry' }); - }); - - it('should fail to validate client request', () => { - const mockRestOperation = new MockRestOperation({ method: 'POST' }); - mockRestOperation.setBody({ - class: 'foo' + const mockRestOperationGET = new MockRestOperation({ method: 'GET' }); + mockRestOperationGET.setBody({}); + + return assert.isFulfilled(config.processClientRequest(mockRestOperationPOST) + .then(() => { + assert.strictEqual(mockRestOperationPOST.statusCode, 200); + return config.processClientRequest(mockRestOperationGET); + }) + .then(() => { + assert.strictEqual(mockRestOperationGET.statusCode, 200); + assert.deepStrictEqual(mockRestOperationGET.body, mockRestOperationPOST.body); + })); }); - return config.processClientRequest(mockRestOperation) - .then(() => { - assert.strictEqual(mockRestOperation.statusCode, 422); - assert.strictEqual(mockRestOperation.body.message, 'Unprocessable entity'); - }); - }); - it('should fail to process client request', () => { - const mockRestOperation = new MockRestOperation({ method: 'POST' }); - mockRestOperation.setBody({ - class: 'Telemetry' + it('should fail to validate client request', () => { + const mockRestOperation = new MockRestOperation({ method: 'POST' }); + mockRestOperation.setBody({ + class: 'foo' + }); + return assert.isFulfilled(config.processClientRequest(mockRestOperation) + .then(() => { + assert.strictEqual(mockRestOperation.statusCode, 422); + assert.strictEqual(mockRestOperation.body.message, 'Unprocessable entity'); + })); }); - util.formatConfig = () => { throw new Error('foo'); }; + it('should fail to process client request', () => { + sinon.stub(util, 'formatConfig').throws(new Error('foo')); + const mockRestOperation = new MockRestOperation({ method: 'POST' }); + mockRestOperation.setBody({ + class: 'Telemetry' + }); + return assert.isFulfilled(config.processClientRequest(mockRestOperation) + .then(() => { + assert.strictEqual(mockRestOperation.statusCode, 500); + assert.strictEqual(mockRestOperation.body.message, 'Internal Server Error'); + })); + }); - return config.processClientRequest(mockRestOperation) - .then(() => { - assert.strictEqual(mockRestOperation.statusCode, 500); - assert.strictEqual(mockRestOperation.body.message, 'Internal Server Error'); + it('should send TEEM report', (done) => { + const decl = { + class: 'Telemetry', + schemaVersion: '1.6.0' + }; + const assetInfo = { + name: 'Telemetry Streaming', + version: '1.6.0' + }; + const teemDevice = new TeemDevice(assetInfo); + sinon.stub(teemDevice, 'report').callsFake((type, version, declaration) => { + assert.deepStrictEqual(declaration, decl); + done(); }); - }); - it('should send TEEM report', (done) => { - const decl = { - class: 'Telemetry', - schemaVersion: '1.6.0' - }; - const assetInfo = { - name: 'Telemetry Streaming', - version: '1.6.0' - }; - const teemDevice = new TeemDevice(assetInfo); - - sinon.stub(teemDevice, 'report').callsFake((type, version, declaration) => { - assert.deepEqual(declaration, decl); - done(); + const restOperation = new MockRestOperation({ method: 'POST' }); + restOperation.setBody(decl); + config.teemDevice = teemDevice; + config.processClientRequest(restOperation); }); - const restOperation = new MockRestOperation({ method: 'POST' }); - restOperation.setBody(decl); - config.teemDevice = teemDevice; - config.processClientRequest(restOperation); - }); - it('should still receive 200 response if f5-teem fails', () => { - const decl = { - class: 'Telemetry', - schemaVersion: '1.6.0' - }; - const assetInfo = { - name: 'Telemetry Streaming', - version: '1.6.0' - }; - const teemDevice = new TeemDevice(assetInfo); - - sinon.stub(teemDevice, 'report').rejects(new Error('f5-teem failed')); - const restOperation = new MockRestOperation({ method: 'POST' }); - restOperation.setBody(decl); - config.teemDevice = teemDevice; - return config.processClientRequest(restOperation) - .then(() => { - assert.equal(restOperation.statusCode, 200); - assert.equal(restOperation.body.message, 'success'); - assert.deepEqual(restOperation.body.declaration, decl); - }); + it('should still receive 200 response if f5-teem fails', () => { + const decl = { + class: 'Telemetry', + schemaVersion: '1.6.0' + }; + const assetInfo = { + name: 'Telemetry Streaming', + version: '1.6.0' + }; + const teemDevice = new TeemDevice(assetInfo); + sinon.stub(teemDevice, 'report').rejects(new Error('f5-teem failed')); + + const restOperation = new MockRestOperation({ method: 'POST' }); + restOperation.setBody(decl); + config.teemDevice = teemDevice; + return assert.isFulfilled(config.processClientRequest(restOperation) + .then(() => { + assert.equal(restOperation.statusCode, 200); + assert.equal(restOperation.body.message, 'success'); + assert.deepStrictEqual(restOperation.body.declaration, decl); + })); + }); }); - describe('saveConfig', () => { + describe('.saveConfig()', () => { it('should fail to save config', () => { sinon.stub(persistentStorage, 'set').rejects(new Error('saveStateError')); return assert.isRejected(config.saveConfig(), /saveStateError/); }); }); - describe('getConfig', () => { + describe('.getConfig()', () => { it('should return BASE_CONFIG if data.parsed is undefined', () => { sinon.stub(persistentStorage, 'get').resolves(undefined); return assert.becomes(config.getConfig(), { raw: {}, parsed: {} }); @@ -270,7 +240,7 @@ describe('Config', () => { }); }); - describe('loadConfig', () => { + describe('.loadConfig()', () => { it('should reject if persistenStorage errors', () => { sinon.stub(persistentStorage, 'get').rejects(new Error('loadStateError')); return assert.isRejected(config.loadConfig(), /loadStateError/); @@ -292,35 +262,12 @@ describe('Config', () => { }); }); - it('should fail to set config when invalid config provided', () => { - // assert.isRejected does not work for this test. - // Due to the throw new Error in _notifyConfigChange, there is no promise to check against. - try { - config.setConfig({}); - } catch (err) { - return assert.strictEqual(err.message, '_notifyConfigChange() Missing parsed config.'); - } - return assert.fail('This test PASSED but was supposed to FAIL'); - }); - - it('should able to get declaration by name', () => { - const obj = { - class: 'Telemetry', - My_System: { - class: 'Telemetry_System', - systemPoller: 'My_Poller' - }, - My_Poller: { - class: 'Telemetry_System_Poller' - } - }; - return config.validate(obj) - .then((validated) => { - validated = util.formatConfig(validated); - const poller = util.getDeclarationByName( - validated, constants.SYSTEM_POLLER_CLASS_NAME, 'My_Poller' - ); - assert.strictEqual(poller.class, constants.SYSTEM_POLLER_CLASS_NAME); - }); + describe('.setConfig()', () => { + it('should fail to set config when invalid config provided', () => { + assert.throws( + () => config.setConfig({}), + '_notifyConfigChange() Missing parsed config.' + ); + }); }); }); diff --git a/test/unit/constantsTests.js b/test/unit/constantsTests.js index 7ca13f65..639a7261 100644 --- a/test/unit/constantsTests.js +++ b/test/unit/constantsTests.js @@ -8,15 +8,34 @@ 'use strict'; -const assert = require('assert'); +/* eslint-disable import/order */ -const constants = require('../../src/lib/constants.js'); +require('./shared/restoreCache')(); -/* eslint-disable global-require */ +const chai = require('chai'); +const chaiAsPromised = require('chai-as-promised'); + +const constants = require('../../src/lib/constants'); +const packageInfo = require('../../package.json'); + +chai.use(chaiAsPromised); +const assert = chai.assert; describe('Constants', () => { // just to avoid mistakes it('global STRICT_TLS_REQUIRED should be true', () => { assert.strictEqual(constants.STRICT_TLS_REQUIRED, true); }); + + it('should get version info from package.json', () => { + const versionInfo = packageInfo.version.split('-'); + if (versionInfo.length === 1) { + versionInfo.push('1'); + } + // to be sure that we really have some data + assert.notStrictEqual(versionInfo[0].length, 0); + assert.notStrictEqual(versionInfo[1].length, 0); + assert.strictEqual(constants.VERSION, versionInfo[0]); + assert.strictEqual(constants.RELEASE, versionInfo[1]); + }); }); diff --git a/test/unit/consumers/awsCloudWatchConsumerTests.js b/test/unit/consumers/awsCloudWatchConsumerTests.js index d6aca900..9622aff9 100644 --- a/test/unit/consumers/awsCloudWatchConsumerTests.js +++ b/test/unit/consumers/awsCloudWatchConsumerTests.js @@ -8,18 +8,21 @@ 'use strict'; +/* eslint-disable import/order */ + +require('../shared/restoreCache')(); + +const AWS = require('aws-sdk'); const chai = require('chai'); const chaiAsPromised = require('chai-as-promised'); -const AWS = require('aws-sdk'); - -chai.use(chaiAsPromised); -const assert = chai.assert; const sinon = require('sinon'); -const util = require('../shared/util.js'); const awsCloudWatchIndex = require('../../../src/lib/consumers/AWS_CloudWatch/index'); +const testUtil = require('../shared/util'); + +chai.use(chaiAsPromised); +const assert = chai.assert; -/* eslint-disable global-require */ describe('AWS_CloudWatch', () => { let clock; let awsConfigUpdate; @@ -32,6 +35,7 @@ describe('AWS_CloudWatch', () => { username: 'awsuser', passphrase: 'awssecret' }; + beforeEach(() => { awsConfigUpdate = sinon.stub(AWS.config, 'update').resolves(); sinon.stub(AWS, 'CloudWatchLogs').returns({ @@ -69,13 +73,13 @@ describe('AWS_CloudWatch', () => { awsConfigUpdate.callsFake((options) => { optionsParam = options; }); - const context = util.buildConsumerContext({ + const context = testUtil.buildConsumerContext({ config: defaultConsumerConfig }); awsCloudWatchIndex(context); assert.strictEqual(optionsParam.region, 'us-west-1'); - assert.deepEqual(optionsParam.credentials, + assert.deepStrictEqual(optionsParam.credentials, new AWS.Credentials({ accessKeyId: 'awsuser', secretAccessKey: 'awssecret' })); }); @@ -87,7 +91,7 @@ describe('AWS_CloudWatch', () => { const config = Object.assign({}, defaultConsumerConfig); delete config.username; delete config.passphrase; - const context = util.buildConsumerContext({ config }); + const context = testUtil.buildConsumerContext({ config }); awsCloudWatchIndex(context); assert.strictEqual(optionsParam.region, 'us-west-1'); @@ -102,22 +106,22 @@ describe('AWS_CloudWatch', () => { }; it('should process systemInfo data', (done) => { - const context = util.buildConsumerContext({ + const context = testUtil.buildConsumerContext({ eventType: 'systemInfo', config: defaultConsumerConfig }); expectedParams.logEvents[0] = { - message: JSON.stringify(util.deepCopy(context.event.data)), + message: JSON.stringify(testUtil.deepCopy(context.event.data)), timestamp: 0 }; putLogEventsStub = (params) => { try { - assert.deepEqual(params, expectedParams); + assert.deepStrictEqual(params, expectedParams); done(); } catch (err) { // done() with parameter is treated as an error. - // Use catch back to pass thrown error from assert.deepEqual to done() callback + // Use catch back to pass thrown error from assert.deepStrictEqual to done() callback done(err); } }; @@ -126,22 +130,22 @@ describe('AWS_CloudWatch', () => { }); it('should process event data', (done) => { - const context = util.buildConsumerContext({ + const context = testUtil.buildConsumerContext({ eventType: 'AVR', config: defaultConsumerConfig }); expectedParams.logEvents[0] = { - message: JSON.stringify(util.deepCopy(context.event.data)), + message: JSON.stringify(testUtil.deepCopy(context.event.data)), timestamp: 0 }; putLogEventsStub = (params) => { try { - assert.deepEqual(params, expectedParams); + assert.deepStrictEqual(params, expectedParams); done(); } catch (err) { // done() with parameter is treated as an error. - // Use catch back to pass thrown error from assert.deepEqual to done() callback + // Use catch back to pass thrown error from assert.deepStrictEqual to done() callback done(err); } }; diff --git a/test/unit/consumers/awsS3ConsumerTests.js b/test/unit/consumers/awsS3ConsumerTests.js index 3d59991c..44a88430 100644 --- a/test/unit/consumers/awsS3ConsumerTests.js +++ b/test/unit/consumers/awsS3ConsumerTests.js @@ -8,18 +8,21 @@ 'use strict'; +/* eslint-disable import/order */ + +require('../shared/restoreCache')(); + +const AWS = require('aws-sdk'); const chai = require('chai'); const chaiAsPromised = require('chai-as-promised'); -const AWS = require('aws-sdk'); - -chai.use(chaiAsPromised); -const assert = chai.assert; const sinon = require('sinon'); const awsS3Index = require('../../../src/lib/consumers/AWS_S3/index'); -const util = require('../shared/util.js'); +const testUtil = require('../shared/util'); + +chai.use(chaiAsPromised); +const assert = chai.assert; -/* eslint-disable global-require */ describe('AWS_S3', () => { let clock; let awsConfigUpdate; @@ -60,13 +63,13 @@ describe('AWS_S3', () => { awsConfigUpdate.callsFake((options) => { optionsParam = options; }); - const context = util.buildConsumerContext({ + const context = testUtil.buildConsumerContext({ config: defaultConsumerConfig }); awsS3Index(context); assert.strictEqual(optionsParam.region, 'us-west-1'); - assert.deepEqual(optionsParam.credentials, + assert.deepStrictEqual(optionsParam.credentials, new AWS.Credentials({ accessKeyId: 'awsuser', secretAccessKey: 'awssecret' })); }); @@ -82,25 +85,25 @@ describe('AWS_S3', () => { }; it('should process systemInfo data', () => { - const context = util.buildConsumerContext({ + const context = testUtil.buildConsumerContext({ eventType: 'systemInfo', config: defaultConsumerConfig }); - expectedParams.Body = JSON.stringify(util.deepCopy(context.event.data)); + expectedParams.Body = JSON.stringify(testUtil.deepCopy(context.event.data)); awsS3Index(context); - assert.deepEqual(s3PutObjectParams, expectedParams); + assert.deepStrictEqual(s3PutObjectParams, expectedParams); }); it('should process event data', () => { - const context = util.buildConsumerContext({ + const context = testUtil.buildConsumerContext({ eventType: 'AVR', config: defaultConsumerConfig }); - expectedParams.Body = JSON.stringify(util.deepCopy(context.event.data)); + expectedParams.Body = JSON.stringify(testUtil.deepCopy(context.event.data)); awsS3Index(context); - assert.deepEqual(s3PutObjectParams, expectedParams); + assert.deepStrictEqual(s3PutObjectParams, expectedParams); }); }); }); diff --git a/test/unit/consumers/azureLogAnalyticsConsumerTests.js b/test/unit/consumers/azureLogAnalyticsConsumerTests.js index 2cc2c4a0..57535bde 100644 --- a/test/unit/consumers/azureLogAnalyticsConsumerTests.js +++ b/test/unit/consumers/azureLogAnalyticsConsumerTests.js @@ -8,19 +8,22 @@ 'use strict'; +/* eslint-disable import/order */ + +require('../shared/restoreCache')(); + const chai = require('chai'); const chaiAsPromised = require('chai-as-promised'); const request = require('request'); - -chai.use(chaiAsPromised); -const assert = chai.assert; const sinon = require('sinon'); const azureAnalyticsIndex = require('../../../src/lib/consumers/Azure_Log_Analytics/index'); -const util = require('../shared/util.js'); -const azureLogData = require('./azureLogAnalyticsConsumerTestsData.js'); +const azureLogData = require('./azureLogAnalyticsConsumerTestsData'); +const testUtil = require('../shared/util'); + +chai.use(chaiAsPromised); +const assert = chai.assert; -/* eslint-disable global-require */ describe('Azure_Log_Analytics', () => { let clock; let requests; @@ -29,6 +32,7 @@ describe('Azure_Log_Analytics', () => { workspaceId: 'myWorkspace', passphrase: 'secret' }; + beforeEach(() => { requests = []; sinon.stub(request, 'post').callsFake((opts, cb) => { @@ -46,7 +50,7 @@ describe('Azure_Log_Analytics', () => { describe('process', () => { it('should configure default request options', () => { - const context = util.buildConsumerContext({ + const context = testUtil.buildConsumerContext({ eventType: 'systemInfo', config: defaultConsumerConfig }); @@ -57,7 +61,7 @@ describe('Azure_Log_Analytics', () => { return azureAnalyticsIndex(context) .then(() => { assert.strictEqual(requests[0].url, 'https://myWorkspace.ods.opinsights.azure.com/api/logs?api-version=2016-04-01'); - assert.deepEqual(requests[0].headers, { + assert.deepStrictEqual(requests[0].headers, { Authorization: 'SharedKey myWorkspace:MGiiWY+WTAxB35tyZ1YljyfwMM5QCqr4ge+giSjcgfI=', 'Content-Type': 'application/json', 'Log-Type': 'F5Telemetry_new', @@ -67,7 +71,7 @@ describe('Azure_Log_Analytics', () => { }); it('should configure request options with provided values', () => { - const context = util.buildConsumerContext({ + const context = testUtil.buildConsumerContext({ eventType: 'systemInfo', config: { workspaceId: 'myWorkspace', @@ -82,28 +86,59 @@ describe('Azure_Log_Analytics', () => { return azureAnalyticsIndex(context) .then(() => { assert.strictEqual(requests[0].url, 'https://myWorkspace.ods.opinsights.azure.com/api/logs?api-version=2016-04-01'); - assert.deepEqual(requests[0].headers, { + assert.deepStrictEqual(requests[0].headers, { + Authorization: 'SharedKey myWorkspace:MGiiWY+WTAxB35tyZ1YljyfwMM5QCqr4ge+giSjcgfI=', + 'Content-Type': 'application/json', + 'Log-Type': 'customLogType_new', + 'x-ms-date': 'Thu, 01 Jan 1970 00:00:00 GMT' + }); + }); + }); + + it('should trace data with secrets redacted', () => { + let traceData; + const context = testUtil.buildConsumerContext({ + eventType: 'systemInfo', + config: { + workspaceId: 'myWorkspace', + passphrase: 'secret', + logType: 'customLogType' + } + }); + context.event.data = { + new: 'data' + }; + context.tracer = { + write: (input) => { + traceData = JSON.parse(input); + } + }; + + return azureAnalyticsIndex(context) + .then(() => { + assert.deepStrictEqual(requests[0].headers, { Authorization: 'SharedKey myWorkspace:MGiiWY+WTAxB35tyZ1YljyfwMM5QCqr4ge+giSjcgfI=', 'Content-Type': 'application/json', 'Log-Type': 'customLogType_new', 'x-ms-date': 'Thu, 01 Jan 1970 00:00:00 GMT' }); + assert.strictEqual(traceData[0].headers.Authorization, '*****'); }); }); it('should process systemInfo data', () => { - const context = util.buildConsumerContext({ + const context = testUtil.buildConsumerContext({ eventType: 'systemInfo', config: defaultConsumerConfig }); const expectedData = azureLogData.systemData[0].expectedData; return azureAnalyticsIndex(context) - .then(() => assert.deepEqual(requests, expectedData)); + .then(() => assert.deepStrictEqual(requests, expectedData)); }); it('should process event data', () => { - const context = util.buildConsumerContext({ + const context = testUtil.buildConsumerContext({ eventType: 'AVR', config: defaultConsumerConfig }); @@ -111,7 +146,7 @@ describe('Azure_Log_Analytics', () => { context.event.type = 'AVR'; return azureAnalyticsIndex(context) - .then(() => assert.deepEqual(requests, expectedData)); + .then(() => assert.deepStrictEqual(requests, expectedData)); }); }); }); diff --git a/test/unit/consumers/azureLogAnalyticsConsumerTestsData.js b/test/unit/consumers/azureLogAnalyticsConsumerTestsData.js index e74d45a2..b59e0b46 100644 --- a/test/unit/consumers/azureLogAnalyticsConsumerTestsData.js +++ b/test/unit/consumers/azureLogAnalyticsConsumerTestsData.js @@ -29,9 +29,9 @@ module.exports = { 'Content-Type': 'application/json', 'x-ms-date': 'Thu, 01 Jan 1970 00:00:00 GMT', 'Log-Type': 'F5Telemetry_virtualServers', - Authorization: 'SharedKey myWorkspace:RvHnhG5CNjJcEJ1QKVL4NyjtWZMVTqC0q5LNfZj8+uc=' + Authorization: 'SharedKey myWorkspace:BkS4afzvOJG7fnXwKVd95oR3dh+l9pCZV214RQICgsc=' }, - body: '[{"/Common/Shared/telemetry_local":{"clientside.bitsIn":0,"clientside.bitsOut":0,"clientside.curConns":0,"destination":"255.255.255.254:6514","availabilityState":"unknown","enabledState":"enabled","name":"/Common/Shared/telemetry_local","ipProtocol":"tcp","mask":"255.255.255.255","tenant":"Common","application":"Shared"},"/Common/something":{"clientside.bitsIn":0,"clientside.bitsOut":0,"clientside.curConns":0,"destination":"192.0.2.0:8787","availabilityState":"unknown","enabledState":"enabled","name":"/Common/something","ipProtocol":"tcp","mask":"255.255.255.255","pool":"/Common/static","tenant":"Common"},"/Common/telemetry_gjd":{"clientside.bitsIn":0,"clientside.bitsOut":0,"clientside.curConns":0,"destination":"255.255.255.254:1234","availabilityState":"unknown","enabledState":"enabled","name":"/Common/telemetry_gjd","ipProtocol":"tcp","mask":"255.255.255.255","tenant":"Common"},"/Common/testvs":{"clientside.bitsIn":0,"clientside.bitsOut":0,"clientside.curConns":0,"destination":"192.0.2.0:7788","availabilityState":"unknown","enabledState":"enabled","name":"/Common/testvs","ipProtocol":"tcp","mask":"255.255.255.255","pool":"/Common/static","tenant":"Common"}}]', + body: '[{"/Common/Shared/telemetry_local":{"clientside.bitsIn":0,"clientside.bitsOut":0,"clientside.curConns":0,"destination":"255.255.255.254:6514","availabilityState":"unknown","enabledState":"enabled","name":"/Common/Shared/telemetry_local","ipProtocol":"tcp","mask":"255.255.255.255","tenant":"Common","application":"Shared","profiles":{"/Common/tcp":{"name":"/Common/tcp","tenant":"Common"},"/Common/app/http":{"name":"/Common/app/http","tenant":"Common","application":"app"}}},"/Common/something":{"clientside.bitsIn":0,"clientside.bitsOut":0,"clientside.curConns":0,"destination":"192.0.2.0:8787","availabilityState":"unknown","enabledState":"enabled","name":"/Common/something","ipProtocol":"tcp","mask":"255.255.255.255","pool":"/Common/static","tenant":"Common","profiles":{"/Common/tcpCustom":{"name":"/Common/tcpCustom","tenant":"Common"},"/Common/app/http":{"name":"/Common/app/http","tenant":"Common","application":"app"}}},"/Common/telemetry_gjd":{"clientside.bitsIn":0,"clientside.bitsOut":0,"clientside.curConns":0,"destination":"255.255.255.254:1234","availabilityState":"unknown","enabledState":"enabled","name":"/Common/telemetry_gjd","ipProtocol":"tcp","mask":"255.255.255.255","tenant":"Common"},"/Common/testvs":{"clientside.bitsIn":0,"clientside.bitsOut":0,"clientside.curConns":0,"destination":"192.0.2.0:7788","availabilityState":"unknown","enabledState":"enabled","name":"/Common/testvs","ipProtocol":"tcp","mask":"255.255.255.255","pool":"/Common/static","tenant":"Common"}}]', strictSSL: true }, { diff --git a/test/unit/consumers/data/systemPollerData.json b/test/unit/consumers/data/systemPollerData.json index 9678232f..9fd9941b 100644 --- a/test/unit/consumers/data/systemPollerData.json +++ b/test/unit/consumers/data/systemPollerData.json @@ -264,7 +264,18 @@ "ipProtocol": "tcp", "mask": "255.255.255.255", "tenant": "Common", - "application": "Shared" + "application": "Shared", + "profiles": { + "/Common/tcp": { + "name": "/Common/tcp", + "tenant": "Common" + }, + "/Common/app/http": { + "name": "/Common/app/http", + "tenant": "Common", + "application": "app" + } + } }, "/Common/something": { "clientside.bitsIn": 0, @@ -277,7 +288,18 @@ "ipProtocol": "tcp", "mask": "255.255.255.255", "pool": "/Common/static", - "tenant": "Common" + "tenant": "Common", + "profiles": { + "/Common/tcpCustom": { + "name": "/Common/tcpCustom", + "tenant": "Common" + }, + "/Common/app/http": { + "name": "/Common/app/http", + "tenant": "Common", + "application": "app" + } + } }, "/Common/telemetry_gjd": { "clientside.bitsIn": 0, diff --git a/test/unit/consumers/defaultConsumerTests.js b/test/unit/consumers/defaultConsumerTests.js new file mode 100644 index 00000000..e70a4f5b --- /dev/null +++ b/test/unit/consumers/defaultConsumerTests.js @@ -0,0 +1,60 @@ +/* + * Copyright 2018. F5 Networks, Inc. See End User License Agreement ("EULA") for + * license terms. Notwithstanding anything to the contrary in the EULA, Licensee + * may copy and modify this software product for its internal business purposes. + * Further, Licensee may upload, publish and distribute the modified version of + * the software product on devcentral.f5.com. + */ + +'use strict'; + +/* eslint-disable import/order */ + +require('../shared/restoreCache')(); + +const chai = require('chai'); +const chaiAsPromised = require('chai-as-promised'); +const sinon = require('sinon'); + +const defaultConsumer = require('../../../src/lib/consumers/default'); + +chai.use(chaiAsPromised); +const assert = chai.assert; + +describe('Default Consumer', () => { + const tracerMock = { + write() {} + }; + const loggerMock = { + error() {}, + exception() {}, + info() {}, + debug() {} + }; + + const context = { + event: {}, + config: {}, + tracer: tracerMock, + logger: loggerMock + }; + + afterEach(() => { + sinon.restore(); + }); + + it('should process event', () => assert.isFulfilled(defaultConsumer(context))); + + it('should reject on missing event', () => { + sinon.stub(context, 'event').value(null); + return assert.isRejected( + defaultConsumer(context), + /No event to process/ + ); + }); + + it('should continue without tracer', () => { + sinon.stub(context, 'tracer').value(null); + return assert.isFulfilled(defaultConsumer(context)); + }); +}); diff --git a/test/unit/consumers/elasticSearchConsumerTests.js b/test/unit/consumers/elasticSearchConsumerTests.js index bc2d7383..ae8df522 100644 --- a/test/unit/consumers/elasticSearchConsumerTests.js +++ b/test/unit/consumers/elasticSearchConsumerTests.js @@ -8,31 +8,26 @@ 'use strict'; -const assert = require('assert'); -const sinon = require('sinon'); +/* eslint-disable import/order */ + +require('../shared/restoreCache')(); +const chai = require('chai'); +const chaiAsPromised = require('chai-as-promised'); const elastic = require('elasticsearch'); +const sinon = require('sinon'); +const elasticSearchIndex = require('../../../src/lib/consumers/ElasticSearch/index'); const util = require('../../../src/lib/util'); -const testUtil = require('../shared/util.js'); -/* eslint-disable global-require */ +const testUtil = require('../shared/util'); + +chai.use(chaiAsPromised); +const assert = chai.assert; describe('ElasticSearch', () => { let passedPayload; let passedClientConfig; - // stub elastic.Client before requiring elastic consumer, to ensure Client constructor is stubbed - sinon.stub(elastic, 'Client').callsFake((config) => { - passedClientConfig = config; - return { - index: (payload) => { - passedPayload = payload; - return Promise.resolve(); - } - }; - }); - const elasticSearchIndex = require('../../../src/lib/consumers/ElasticSearch/index'); - const defaultConsumerConfig = { host: 'localhost', port: '9200', @@ -43,6 +38,18 @@ describe('ElasticSearch', () => { protocol: 'http' }; + beforeEach(() => { + sinon.stub(elastic, 'Client').callsFake((config) => { + passedClientConfig = config; + return { + index: (payload) => { + passedPayload = payload; + return Promise.resolve(); + } + }; + }); + }); + afterEach(() => { sinon.restore(); }); @@ -93,7 +100,7 @@ describe('ElasticSearch', () => { type: 'f5telemetry' }; elasticSearchIndex(context); - assert.deepEqual(passedPayload, expectedPayload); + assert.deepStrictEqual(passedPayload, expectedPayload); }); it('should process event data', () => { @@ -114,7 +121,7 @@ describe('ElasticSearch', () => { }; elasticSearchIndex(context); - assert.deepEqual(passedPayload, expectedPayload); + assert.deepStrictEqual(passedPayload, expectedPayload); }); }); }); diff --git a/test/unit/consumers/genericHTTPConsumerTests.js b/test/unit/consumers/genericHTTPConsumerTests.js index e6e24351..4c495f87 100644 --- a/test/unit/consumers/genericHTTPConsumerTests.js +++ b/test/unit/consumers/genericHTTPConsumerTests.js @@ -8,18 +8,21 @@ 'use strict'; +/* eslint-disable import/order */ + +require('../shared/restoreCache')(); + const chai = require('chai'); const chaiAsPromised = require('chai-as-promised'); const request = require('request'); - -chai.use(chaiAsPromised); -const assert = chai.assert; const sinon = require('sinon'); const genericHttpIndex = require('../../../src/lib/consumers/Generic_HTTP/index'); -const util = require('../shared/util.js'); +const testUtil = require('../shared/util'); + +chai.use(chaiAsPromised); +const assert = chai.assert; -/* eslint-disable global-require */ describe('Generic_HTTP', () => { const defaultConsumerConfig = { port: 80, @@ -32,17 +35,17 @@ describe('Generic_HTTP', () => { describe('process', () => { it('should POST using default request options', (done) => { - const context = util.buildConsumerContext({ + const context = testUtil.buildConsumerContext({ config: defaultConsumerConfig }); sinon.stub(request, 'post').callsFake((opts) => { try { - assert.deepEqual(opts.url, 'https://localhost:80/'); + assert.deepStrictEqual(opts.url, 'https://localhost:80/'); done(); } catch (err) { // done() with parameter is treated as an error. - // Use catch back to pass thrown error from assert.deepEqual to done() callback + // Use catch back to pass thrown error from assert.deepStrictEqual to done() callback done(err); } }); @@ -50,7 +53,7 @@ describe('Generic_HTTP', () => { }); it('should GET using provided request options', (done) => { - const context = util.buildConsumerContext({ + const context = testUtil.buildConsumerContext({ config: { method: 'GET', protocol: 'http', @@ -68,12 +71,49 @@ describe('Generic_HTTP', () => { sinon.stub(request, 'get').callsFake((opts) => { try { - assert.deepEqual(opts.url, 'http://myMetricsSystem:8080/ingest'); - assert.deepEqual(opts.headers, { 'x-api-key': 'superSecret' }); + assert.deepStrictEqual(opts.url, 'http://myMetricsSystem:8080/ingest'); + assert.deepStrictEqual(opts.headers, { 'x-api-key': 'superSecret' }); + done(); + } catch (err) { + // done() with parameter is treated as an error. + // Use catch back to pass thrown error from assert.deepStrictEqual to done() callback + done(err); + } + }); + genericHttpIndex(context); + }); + + it('should trace data with secrets redacted', (done) => { + let traceData; + const context = testUtil.buildConsumerContext({ + config: { + method: 'POST', + protocol: 'http', + port: '8080', + path: '/ingest', + host: 'myMetricsSystem', + headers: [ + { + name: 'Authorization', + value: 'Basic ABC123' + } + ] + } + }); + context.tracer = { + write: (input) => { + traceData = JSON.parse(input); + } + }; + + sinon.stub(request, 'post').callsFake((opts) => { + try { + assert.deepStrictEqual(traceData.headers, { Authorization: '*****' }); + assert.deepStrictEqual(opts.headers, { Authorization: 'Basic ABC123' }); done(); } catch (err) { // done() with parameter is treated as an error. - // Use catch back to pass thrown error from assert.deepEqual to done() callback + // Use catch back to pass thrown error from assert.deepStrictEqual to done() callback done(err); } }); @@ -81,19 +121,19 @@ describe('Generic_HTTP', () => { }); it('should process systemInfo data', (done) => { - const context = util.buildConsumerContext({ + const context = testUtil.buildConsumerContext({ eventType: 'systemInfo', config: defaultConsumerConfig }); - const expectedData = util.deepCopy(context.event.data); + const expectedData = testUtil.deepCopy(context.event.data); sinon.stub(request, 'post').callsFake((opts) => { try { - assert.deepEqual(opts.body, JSON.stringify(expectedData)); + assert.deepStrictEqual(opts.body, JSON.stringify(expectedData)); done(); } catch (err) { // done() with parameter is treated as an error. - // Use catch back to pass thrown error from assert.deepEqual to done() callback + // Use catch back to pass thrown error from assert.deepStrictEqual to done() callback done(err); } }); @@ -102,19 +142,19 @@ describe('Generic_HTTP', () => { }); it('should process event data', (done) => { - const context = util.buildConsumerContext({ + const context = testUtil.buildConsumerContext({ eventType: 'AVR', config: defaultConsumerConfig }); - const expectedData = util.deepCopy(context.event.data); + const expectedData = testUtil.deepCopy(context.event.data); sinon.stub(request, 'post').callsFake((opts) => { try { - assert.deepEqual(opts.body, JSON.stringify(expectedData)); + assert.deepStrictEqual(opts.body, JSON.stringify(expectedData)); done(); } catch (err) { // done() with parameter is treated as an error. - // Use catch back to pass thrown error from assert.deepEqual to done() callback + // Use catch back to pass thrown error from assert.deepStrictEqual to done() callback done(err); } }); diff --git a/test/unit/consumers/googleStackDriverConsumerTests.js b/test/unit/consumers/googleStackDriverConsumerTests.js index 4794f838..e717f183 100644 --- a/test/unit/consumers/googleStackDriverConsumerTests.js +++ b/test/unit/consumers/googleStackDriverConsumerTests.js @@ -8,16 +8,23 @@ 'use strict'; +/* eslint-disable import/order */ + +require('../shared/restoreCache')(); + const chai = require('chai'); const chaiAsPromised = require('chai-as-promised'); const nock = require('nock'); +const stackDriverIndex = require('../../../src/lib/consumers/Google_StackDriver/index'); chai.use(chaiAsPromised); const assert = chai.assert; -const stackDriverIndex = require('../../../src/lib/consumers/Google_StackDriver/index'); - describe('Google_StackDriver', () => { + afterEach(() => { + nock.cleanAll(); + }); + it('should do nothing when data is not systemInfo', () => { const context = { event: {} @@ -51,6 +58,7 @@ describe('Google_StackDriver', () => { privateKey: '-----BEGIN RSA PRIVATE KEY-----\nMIICXAIBAAKBgQCqGKukO1De7zhZj6+H0qtjTkVxwTCpvKe4eCZ0FPqri0cb2JZfXJ/DgYSF6vUp\nwmJG8wVQZKjeGcjDOL5UlsuusFncCzWBQ7RKNUSesmQRMSGkVb1/3j+skZ6UtW+5u09lHNsj6tQ5\n1s1SPrCBkedbNf0Tp0GbMJDyR4e9T04ZZwIDAQABAoGAFijko56+qGyN8M0RVyaRAXz++xTqHBLh\n3tx4VgMtrQ+WEgCjhoTwo23KMBAuJGSYnRmoBZM3lMfTKevIkAidPExvYCdm5dYq3XToLkkLv5L2\npIIVOFMDG+KESnAFV7l2c+cnzRMW0+b6f8mR1CJzZuxVLL6Q02fvLi55/mbSYxECQQDeAw6fiIQX\nGukBI4eMZZt4nscy2o12KyYner3VpoeE+Np2q+Z3pvAMd/aNzQ/W9WaI+NRfcxUJrmfPwIGm63il\nAkEAxCL5HQb2bQr4ByorcMWm/hEP2MZzROV73yF41hPsRC9m66KrheO9HPTJuo3/9s5p+sqGxOlF\nL0NDt4SkosjgGwJAFklyR1uZ/wPJjj611cdBcztlPdqoxssQGnh85BzCj/u3WqBpE2vjvyyvyI5k\nX6zk7S0ljKtt2jny2+00VsBerQJBAJGC1Mg5Oydo5NwD6BiROrPxGo2bpTbu/fhrT8ebHkTz2epl\nU9VQQSQzY1oZMVX8i1m5WUTLPz2yLJIBQVdXqhMCQBGoiuSoSjafUhV7i1cEGpb88h5NBYZzWXGZ\n37sJ5QsW+sJyoNde3xH8vdXhzU7eT82D6X/scw9RZz+/6rCJ4p0=\n-----END RSA PRIVATE KEY-----\n' } }; + beforeEach(() => { nock('https://monitoring.googleapis.com/v3/projects/theProject/metricDescriptors') .get('') @@ -134,10 +142,6 @@ describe('Google_StackDriver', () => { .reply(200, {}); }); - afterEach(() => { - nock.cleanAll(); - }); - it('should process systemInfo data', () => { nock('https://oauth2.googleapis.com/token') .post('') diff --git a/test/unit/consumers/graphiteConsumerTests.js b/test/unit/consumers/graphiteConsumerTests.js index cc11a8d2..a28c1a2f 100644 --- a/test/unit/consumers/graphiteConsumerTests.js +++ b/test/unit/consumers/graphiteConsumerTests.js @@ -8,18 +8,21 @@ 'use strict'; +/* eslint-disable import/order */ + +require('../shared/restoreCache')(); + const chai = require('chai'); const chaiAsPromised = require('chai-as-promised'); const request = require('request'); - -chai.use(chaiAsPromised); -const assert = chai.assert; const sinon = require('sinon'); const graphiteIndex = require('../../../src/lib/consumers/Graphite/index'); -const util = require('../shared/util.js'); +const testUtil = require('../shared/util'); + +chai.use(chaiAsPromised); +const assert = chai.assert; -/* eslint-disable global-require */ describe('Graphite', () => { const defaultConsumerConfig = { port: 80, @@ -32,17 +35,17 @@ describe('Graphite', () => { describe('process', () => { it('should POST using default request options', (done) => { - const context = util.buildConsumerContext({ + const context = testUtil.buildConsumerContext({ config: defaultConsumerConfig }); sinon.stub(request, 'post').callsFake((opts) => { try { - assert.deepEqual(opts.url, 'http://localhost:80/events/'); + assert.deepStrictEqual(opts.url, 'http://localhost:80/events/'); done(); } catch (err) { // done() with parameter is treated as an error. - // Use catch back to pass thrown error from assert.deepEqual to done() callback + // Use catch back to pass thrown error from assert.deepStrictEqual to done() callback done(err); } }); @@ -50,7 +53,7 @@ describe('Graphite', () => { }); it('should POST using provided request options', (done) => { - const context = util.buildConsumerContext({ + const context = testUtil.buildConsumerContext({ config: { protocol: 'https', port: '8080', @@ -61,11 +64,11 @@ describe('Graphite', () => { sinon.stub(request, 'post').callsFake((opts) => { try { - assert.deepEqual(opts.url, 'https://myMetricsSystem:8080/ingest/'); + assert.deepStrictEqual(opts.url, 'https://myMetricsSystem:8080/ingest/'); done(); } catch (err) { // done() with parameter is treated as an error. - // Use catch back to pass thrown error from assert.deepEqual to done() callback + // Use catch back to pass thrown error from assert.deepStrictEqual to done() callback done(err); } }); @@ -73,23 +76,23 @@ describe('Graphite', () => { }); it('should process systemInfo data', (done) => { - const context = util.buildConsumerContext({ + const context = testUtil.buildConsumerContext({ eventType: 'systemInfo', config: defaultConsumerConfig }); const expectedData = JSON.stringify({ what: 'f5telemetry', tags: ['systemInfo'], - data: util.deepCopy(context.event.data) + data: testUtil.deepCopy(context.event.data) }); sinon.stub(request, 'post').callsFake((opts) => { try { - assert.deepEqual(opts.body, expectedData); + assert.deepStrictEqual(opts.body, expectedData); done(); } catch (err) { // done() with parameter is treated as an error. - // Use catch back to pass thrown error from assert.deepEqual to done() callback + // Use catch back to pass thrown error from assert.deepStrictEqual to done() callback done(err); } }); @@ -98,7 +101,7 @@ describe('Graphite', () => { }); it('should process event data', (done) => { - const context = util.buildConsumerContext({ + const context = testUtil.buildConsumerContext({ eventType: 'AVR', config: defaultConsumerConfig }); @@ -106,16 +109,16 @@ describe('Graphite', () => { const expectedData = JSON.stringify({ what: 'f5telemetry', tags: ['AVR'], - data: util.deepCopy(context.event.data) + data: testUtil.deepCopy(context.event.data) }); sinon.stub(request, 'post').callsFake((opts) => { try { - assert.deepEqual(opts.body, expectedData); + assert.deepStrictEqual(opts.body, expectedData); done(); } catch (err) { // done() with parameter is treated as an error. - // Use catch back to pass thrown error from assert.deepEqual to done() callback + // Use catch back to pass thrown error from assert.deepStrictEqual to done() callback done(err); } }); diff --git a/test/unit/consumers/kafkaConsumerTests.js b/test/unit/consumers/kafkaConsumerTests.js index 11163f7e..9d03ea74 100644 --- a/test/unit/consumers/kafkaConsumerTests.js +++ b/test/unit/consumers/kafkaConsumerTests.js @@ -8,18 +8,21 @@ 'use strict'; +/* eslint-disable import/order */ + +require('../shared/restoreCache')(); + const chai = require('chai'); const chaiAsPromised = require('chai-as-promised'); const kafka = require('kafka-node'); - -chai.use(chaiAsPromised); -const assert = chai.assert; const sinon = require('sinon'); const kafkaIndex = require('../../../src/lib/consumers/Kafka/index'); -const util = require('../shared/util.js'); +const testUtil = require('../shared/util'); + +chai.use(chaiAsPromised); +const assert = chai.assert; -/* eslint-disable global-require */ describe('Kafka', () => { let sendStub; let passedClientOptions; @@ -29,6 +32,7 @@ describe('Kafka', () => { port: '9092', topic: 'dataTopic' }; + beforeEach(() => { sinon.stub(kafka, 'KafkaClient').callsFake((opts) => { passedClientOptions = opts; @@ -52,7 +56,7 @@ describe('Kafka', () => { }]; it('should configure Kafka Client client options with default values', (done) => { - const context = util.buildConsumerContext({ + const context = testUtil.buildConsumerContext({ config: defaultConsumerConfig }); const expectedOptions = { @@ -65,11 +69,11 @@ describe('Kafka', () => { sendStub = () => { try { - assert.deepEqual(passedClientOptions, expectedOptions); + assert.deepStrictEqual(passedClientOptions, expectedOptions); done(); } catch (err) { // done() with parameter is treated as an error. - // Use catch back to pass thrown error from assert.deepEqual to done() callback + // Use catch back to pass thrown error from assert.deepStrictEqual to done() callback done(err); } }; @@ -78,7 +82,7 @@ describe('Kafka', () => { }); it('should configure Kafka Client client options with provided values', (done) => { - const context = util.buildConsumerContext({ + const context = testUtil.buildConsumerContext({ config: { host: 'kafka-second-host', port: '4545', @@ -105,11 +109,11 @@ describe('Kafka', () => { sendStub = () => { try { - assert.deepEqual(passedClientOptions, expectedOptions); + assert.deepStrictEqual(passedClientOptions, expectedOptions); done(); } catch (err) { // done() with parameter is treated as an error. - // Use catch back to pass thrown error from assert.deepEqual to done() callback + // Use catch back to pass thrown error from assert.deepStrictEqual to done() callback done(err); } }; @@ -118,19 +122,19 @@ describe('Kafka', () => { }); it('should process systemInfo data', (done) => { - const context = util.buildConsumerContext({ + const context = testUtil.buildConsumerContext({ eventType: 'systemInfo', config: defaultConsumerConfig }); - expectedPayload[0].messages = JSON.stringify(util.deepCopy(context.event.data)); + expectedPayload[0].messages = JSON.stringify(testUtil.deepCopy(context.event.data)); sendStub = (payload) => { try { - assert.deepEqual(payload, expectedPayload); + assert.deepStrictEqual(payload, expectedPayload); done(); } catch (err) { // done() with parameter is treated as an error. - // Use catch back to pass thrown error from assert.deepEqual to done() callback + // Use catch back to pass thrown error from assert.deepStrictEqual to done() callback done(err); } }; @@ -139,19 +143,19 @@ describe('Kafka', () => { }); it('should process event data', (done) => { - const context = util.buildConsumerContext({ + const context = testUtil.buildConsumerContext({ eventType: 'AVR', config: defaultConsumerConfig }); - expectedPayload[0].messages = JSON.stringify(util.deepCopy(context.event.data)); + expectedPayload[0].messages = JSON.stringify(testUtil.deepCopy(context.event.data)); sendStub = (payload) => { try { - assert.deepEqual(payload, expectedPayload); + assert.deepStrictEqual(payload, expectedPayload); done(); } catch (err) { // done() with parameter is treated as an error. - // Use catch back to pass thrown error from assert.deepEqual to done() callback + // Use catch back to pass thrown error from assert.deepStrictEqual to done() callback done(err); } }; diff --git a/test/unit/consumers/splunkConsumerTests.js b/test/unit/consumers/splunkConsumerTests.js index bebe4daa..ff8006b8 100644 --- a/test/unit/consumers/splunkConsumerTests.js +++ b/test/unit/consumers/splunkConsumerTests.js @@ -8,20 +8,23 @@ 'use strict'; +/* eslint-disable import/order */ + +require('../shared/restoreCache')(); + const chai = require('chai'); const chaiAsPromised = require('chai-as-promised'); const request = require('request'); +const sinon = require('sinon'); const zlib = require('zlib'); +const splunkIndex = require('../../../src/lib/consumers/Splunk/index'); +const splunkData = require('./splunkConsumerTestsData'); +const testUtil = require('../shared/util'); + chai.use(chaiAsPromised); const assert = chai.assert; -const sinon = require('sinon'); -const splunkIndex = require('../../../src/lib/consumers/Splunk/index'); -const splunkData = require('./splunkConsumerTestsData.js'); -const util = require('../shared/util.js'); - -/* eslint-disable global-require */ describe('Splunk', () => { let clock; @@ -51,14 +54,14 @@ describe('Splunk', () => { }; it('should configure request options with default values', (done) => { - const context = util.buildConsumerContext({ + const context = testUtil.buildConsumerContext({ config: defaultConsumerConfig }); sinon.stub(request, 'post').callsFake((opts) => { try { assert.strictEqual(opts.gzip, true); - assert.deepEqual(opts.headers, { + assert.deepStrictEqual(opts.headers, { 'Accept-Encoding': 'gzip', Authorization: 'Splunk mySecret', 'Content-Encoding': 'gzip', @@ -68,7 +71,7 @@ describe('Splunk', () => { done(); } catch (err) { // done() with parameter is treated as an error. - // Use catch back to pass thrown error from assert.deepEqual to done() callback + // Use catch back to pass thrown error from assert.deepStrictEqual to done() callback done(err); } }); @@ -77,7 +80,7 @@ describe('Splunk', () => { }); it('should configure request options with provided values', (done) => { - const context = util.buildConsumerContext({ + const context = testUtil.buildConsumerContext({ config: { protocol: 'http', host: 'remoteSplunk', @@ -89,7 +92,7 @@ describe('Splunk', () => { sinon.stub(request, 'post').callsFake((opts) => { try { - assert.deepEqual(opts.headers, { + assert.deepStrictEqual(opts.headers, { Authorization: 'Splunk superSecret', 'Content-Length': 92 }); @@ -97,7 +100,47 @@ describe('Splunk', () => { done(); } catch (err) { // done() with parameter is treated as an error. - // Use catch back to pass thrown error from assert.deepEqual to done() callback + // Use catch back to pass thrown error from assert.deepStrictEqual to done() callback + done(err); + } + }); + + splunkIndex(context); + }); + + it('should trace data with secrets redacted', (done) => { + let traceData; + const context = testUtil.buildConsumerContext({ + config: { + protocol: 'http', + host: 'remoteSplunk', + port: '4567', + passphrase: 'superSecret', + gzip: false + } + }); + context.tracer = { + write: (input) => { + traceData = JSON.parse(input); + } + }; + + sinon.stub(request, 'post').callsFake((opts) => { + try { + assert.deepStrictEqual(opts.headers, { + Authorization: 'Splunk superSecret', + 'Content-Length': 92 + }); + assert.notStrictEqual(traceData.consumer.passphrase.indexOf('*****'), -1, + 'consumer config passphrase should be redacted'); + assert.notStrictEqual(traceData.requestOpts.headers.Authorization.indexOf('*****'), -1, + 'passphrase in request headers should be redacted'); + assert.strictEqual(JSON.stringify(traceData).indexOf('superSecret'), -1, + 'passphrase should not be present anywhere in trace data'); + done(); + } catch (err) { + // done() with parameter is treated as an error. + // Use catch back to pass thrown error from assert.deepStrictEqual to done() callback done(err); } }); @@ -106,22 +149,22 @@ describe('Splunk', () => { }); it('should process systemInfo data', (done) => { - const context = util.buildConsumerContext({ + const context = testUtil.buildConsumerContext({ eventType: 'systemInfo', config: defaultConsumerConfig }); - const expectedData = util.deepCopy(expectedDataTemplate); + const expectedData = testUtil.deepCopy(expectedDataTemplate); expectedData.time = 1576001615000; - expectedData.event = util.deepCopy(context.event.data); + expectedData.event = testUtil.deepCopy(context.event.data); sinon.stub(request, 'post').callsFake((opts) => { try { const output = zlib.gunzipSync(opts.body).toString(); - assert.deepEqual(output, JSON.stringify(expectedData)); + assert.deepStrictEqual(output, JSON.stringify(expectedData)); done(); } catch (err) { // done() with parameter is treated as an error. - // Use catch back to pass thrown error from assert.deepEqual to done() callback + // Use catch back to pass thrown error from assert.deepStrictEqual to done() callback done(err); } }); @@ -130,21 +173,21 @@ describe('Splunk', () => { }); it('should process event data', (done) => { - const context = util.buildConsumerContext({ + const context = testUtil.buildConsumerContext({ eventType: 'AVR', config: defaultConsumerConfig }); - const expectedData = util.deepCopy(expectedDataTemplate); - expectedData.event = util.deepCopy(context.event.data); + const expectedData = testUtil.deepCopy(expectedDataTemplate); + expectedData.event = testUtil.deepCopy(context.event.data); sinon.stub(request, 'post').callsFake((opts) => { try { const output = zlib.gunzipSync(opts.body).toString(); - assert.deepEqual(output, JSON.stringify(expectedData)); + assert.deepStrictEqual(output, JSON.stringify(expectedData)); done(); } catch (err) { // done() with parameter is treated as an error. - // Use catch back to pass thrown error from assert.deepEqual to done() callback + // Use catch back to pass thrown error from assert.deepStrictEqual to done() callback done(err); } }); @@ -153,21 +196,26 @@ describe('Splunk', () => { }); it('should process systemInfo in legacy format', (done) => { - const context = util.buildConsumerContext({ + // test works correctly while: + // - we generating output in predictable order + // - Object.keys() returns the same array on different node versions + const expectedData = splunkData.legacySystemData[0].expectedData; + const context = testUtil.buildConsumerContext({ eventType: 'systemInfo', config: defaultConsumerConfig }); context.config.format = 'legacy'; - const expectedLegacyData = splunkData.legacySystemData[0].expectedData; sinon.stub(request, 'post').callsFake((opts) => { try { - const output = zlib.gunzipSync(opts.body).toString(); - assert.strictEqual(output, expectedLegacyData.replace(/(\r\n|\n|\r)/g, '')); + let output = zlib.gunzipSync(opts.body).toString(); + output = output.replace(/}{"time/g, '},{"time'); + output = JSON.parse(`[${output}]`); + assert.deepStrictEqual(output, expectedData); done(); } catch (err) { // done() with parameter is treated as an error. - // Use catch back to pass thrown error from assert.deepEqual to done() callback + // Use catch back to pass thrown error from assert.deepStrictEqual to done() callback done(err); } }); @@ -177,7 +225,7 @@ describe('Splunk', () => { describe('tmstats', () => { it('should replace periods in tmstat key names with underscores', (done) => { - const context = util.buildConsumerContext({ + const context = testUtil.buildConsumerContext({ eventType: 'systemInfo', config: defaultConsumerConfig }); @@ -225,7 +273,192 @@ describe('Splunk', () => { done(); } catch (err) { // done() with parameter is treated as an error. - // Use catch back to pass thrown error from assert.deepEqual to done() callback + // Use catch back to pass thrown error from assert.deepStrictEqual to done() callback + done(err); + } + }); + + splunkIndex(context); + }); + + it('should replace IPv6 prefix in monitorInstanceStat', (done) => { + const context = testUtil.buildConsumerContext({ + eventType: 'systemInfo', + config: defaultConsumerConfig + }); + context.config.format = 'legacy'; + context.event.data = { + system: { + systemTimestamp: '2020-01-17T18:02:51.000Z' + }, + tmstats: { + monitorInstanceStat: [ + { + ip_address: '::FFFF:192.0.0.1' + }, + { + 'ip.address': '::ffff:192.0.0.2' + }, + { + 'ip.address': '192.0.0.3' + } + ] + }, + telemetryServiceInfo: context.event.data.telemetryServiceInfo, + telemetryEventCategory: context.event.data.telemetryEventCategory + }; + sinon.stub(request, 'post').callsFake((opts) => { + try { + const output = zlib.gunzipSync(opts.body).toString(); + assert.notStrictEqual( + output.indexOf('"192.0.0.1"'), -1, 'output should remove ::FFFF from ::FFFF:192.0.0.1' + ); + assert.notStrictEqual( + output.indexOf('"192.0.0.2"'), -1, 'output should remove ::FFFF from ::ffff:192.0.0.2' + ); + assert.notStrictEqual( + output.indexOf('"192.0.0.3"'), -1, 'output should include 192.0.0.3' + ); + done(); + } catch (err) { + // done() with parameter is treated as an error. + // Use catch back to pass thrown error from assert.deepStrictEqual to done() callback + done(err); + } + }); + + splunkIndex(context); + }); + + it('should format hex IP', (done) => { + const context = testUtil.buildConsumerContext({ + eventType: 'systemInfo', + config: defaultConsumerConfig + }); + context.config.format = 'legacy'; + context.event.data = { + system: { + systemTimestamp: '2020-01-17T18:02:51.000Z' + }, + tmstats: { + virtualServerStat: [ + { + source: '00:00:00:00:00:00:00:00:00:00:FF:FF:C0:00:00:01:00:00:00:00' + }, + { + addr: '10:00:00:00:00:00:00:00:00:00:FF:F8:C0:00:00:01:00:00:00:00' + }, + { + destination: '192.0.0.3' + } + ] + }, + telemetryServiceInfo: context.event.data.telemetryServiceInfo, + telemetryEventCategory: context.event.data.telemetryEventCategory + }; + sinon.stub(request, 'post').callsFake((opts) => { + try { + const output = zlib.gunzipSync(opts.body).toString(); + assert.notStrictEqual( + output.indexOf('"source":"192.0.0.1"'), -1, 'output should include 192.0.0.1' + ); + assert.notStrictEqual( + output.indexOf('"addr":"1000:0000:0000:0000:0000:FFF8:C000:0001"'), -1, 'output should include 1000:0000:0000:0000:0000:FFF8:C000:0001' + ); + assert.notStrictEqual( + output.indexOf('"destination":"192.0.0.3"'), -1, 'output should include 192.0.0.3' + ); + done(); + } catch (err) { + // done() with parameter is treated as an error. + // Use catch back to pass thrown error from assert.deepStrictEqual to done() callback + done(err); + } + }); + + splunkIndex(context); + }); + + it('should include tenant and application', (done) => { + const context = testUtil.buildConsumerContext({ + eventType: 'systemInfo', + config: defaultConsumerConfig + }); + context.config.format = 'legacy'; + context.event.data = { + system: { + systemTimestamp: '2020-01-17T18:02:51.000Z' + }, + tmstats: { + virtualServerStat: [ + { + source: '00:00:00:00:00:00:00:00:00:00:FF:FF:C0:00:00:01:00:00:00:00', + tenant: 'tenant', + application: 'application' + } + ] + }, + telemetryServiceInfo: context.event.data.telemetryServiceInfo, + telemetryEventCategory: context.event.data.telemetryEventCategory + }; + sinon.stub(request, 'post').callsFake((opts) => { + try { + const output = zlib.gunzipSync(opts.body).toString(); + assert.notStrictEqual( + output.indexOf('"tenant":"tenant"'), -1, 'output should include tenant' + ); + assert.notStrictEqual( + output.indexOf('"application":"application"'), -1, 'output should include application' + ); + assert.notStrictEqual( + output.indexOf('"appComponent":""'), -1, 'output should include appComponent' + ); + done(); + } catch (err) { + // done() with parameter is treated as an error. + // Use catch back to pass thrown error from assert.deepStrictEqual to done() callback + done(err); + } + }); + + splunkIndex(context); + }); + + it('should include last_cycle_count', (done) => { + const context = testUtil.buildConsumerContext({ + eventType: 'systemInfo', + config: defaultConsumerConfig + }); + context.config.format = 'legacy'; + context.event.data = { + system: { + systemTimestamp: '2020-01-17T18:02:51.000Z' + }, + tmstats: { + virtualServerStat: [ + { + cycle_count: '10' + } + ], + virtualServerCpuStat: [ + { + avg: '10' + } + ] + }, + telemetryServiceInfo: context.event.data.telemetryServiceInfo, + telemetryEventCategory: context.event.data.telemetryEventCategory + }; + sinon.stub(request, 'post').callsFake((opts) => { + try { + const output = zlib.gunzipSync(opts.body).toString(); + assert.notStrictEqual( + output.indexOf('"last_cycle_count":"10"'), -1, 'output should include last_cycle_count' + ); + done(); + } catch (err) { + // done() with parameter is treated as an error. + // Use catch back to pass thrown error from assert.deepStrictEqual to done() callback done(err); } }); diff --git a/test/unit/consumers/splunkConsumerTestsData.js b/test/unit/consumers/splunkConsumerTestsData.js index a56d9a97..0d8c5d01 100644 --- a/test/unit/consumers/splunkConsumerTestsData.js +++ b/test/unit/consumers/splunkConsumerTestsData.js @@ -11,56 +11,980 @@ module.exports = { legacySystemData: [ { - expectedData: `{"time":1576001615000,"host":"bigip1","source":"bigip.tmsh.system_status","sourcetype":"f5:bigip:status:iapp:json","event":{"aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":"","version":"14.1.0.6","build":"0.0.9","location":"missing data","description":"missing data","marketing-name":"missing data","platform-id":"missing data","failover-state":"OFFLINE","chassis-id":"missing data","mode":"standalone","sync-status":"Standalone","sync-summary":" ","sync-color":"green","asm_state":"","last_asm_change":"","apm_state":"","afm_state":"","last_afm_deploy":"","ltm_config_time":"2019-12-10T18:13:26.000Z","gtm_config_time":"2147483647"}} -{"time":1576001615000,"host":"bigip1","source":"bigip.tmsh.interface_status","sourcetype":"f5:bigip:status:iapp:json","event":{"aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":"","interface_name":"1.0","interface_status":"up"}} -{"time":1576001615000,"host":"bigip1","source":"bigip.tmsh.interface_status","sourcetype":"f5:bigip:status:iapp:json","event":{"aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":"","interface_name":"mgmt","interface_status":"up"}} -{"time":1576001615000,"host":"bigip1","source":"bigip.tmsh.disk_usage","sourcetype":"f5:bigip:status:iapp:json","event":{"1024-blocks":"428150","Capacity":"20%","name":"/","Filesystem":"/","aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":""}} -{"time":1576001615000,"host":"bigip1","source":"bigip.tmsh.disk_usage","sourcetype":"f5:bigip:status:iapp:json","event":{"1024-blocks":"4079780","Capacity":"1%","name":"/dev","Filesystem":"/dev","aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":""}} -{"time":1576001615000,"host":"bigip1","source":"bigip.tmsh.disk_usage","sourcetype":"f5:bigip:status:iapp:json","event":{"1024-blocks":"4088828","Capacity":"1%","name":"/dev/shm","Filesystem":"/dev/shm","aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":""}} -{"time":1576001615000,"host":"bigip1","source":"bigip.tmsh.disk_usage","sourcetype":"f5:bigip:status:iapp:json","event":{"1024-blocks":"4088828","Capacity":"1%","name":"/run","Filesystem":"/run","aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":""}} -{"time":1576001615000,"host":"bigip1","source":"bigip.tmsh.disk_usage","sourcetype":"f5:bigip:status:iapp:json","event":{"1024-blocks":"4088828","Capacity":"0%","name":"/sys/fs/cgroup","Filesystem":"/sys/fs/cgroup","aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":""}} -{"time":1576001615000,"host":"bigip1","source":"bigip.tmsh.disk_usage","sourcetype":"f5:bigip:status:iapp:json","event":{"1024-blocks":"5186648","Capacity":"84%","name":"/usr","Filesystem":"/usr","aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":""}} -{"time":1576001615000,"host":"bigip1","source":"bigip.tmsh.disk_usage","sourcetype":"f5:bigip:status:iapp:json","event":{"1024-blocks":"15350768","Capacity":"2%","name":"/shared","Filesystem":"/shared","aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":""}} -{"time":1576001615000,"host":"bigip1","source":"bigip.tmsh.disk_usage","sourcetype":"f5:bigip:status:iapp:json","event":{"1024-blocks":"4088828","Capacity":"2%","name":"/shared/rrd.1.2","Filesystem":"/shared/rrd.1.2","aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":""}} -{"time":1576001615000,"host":"bigip1","source":"bigip.tmsh.disk_usage","sourcetype":"f5:bigip:status:iapp:json","event":{"1024-blocks":"3030800","Capacity":"34%","name":"/var","Filesystem":"/var","aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":""}} -{"time":1576001615000,"host":"bigip1","source":"bigip.tmsh.disk_usage","sourcetype":"f5:bigip:status:iapp:json","event":{"1024-blocks":"2171984","Capacity":"2%","name":"/config","Filesystem":"/config","aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":""}} -{"time":1576001615000,"host":"bigip1","source":"bigip.tmsh.disk_usage","sourcetype":"f5:bigip:status:iapp:json","event":{"1024-blocks":"4088828","Capacity":"1%","name":"/var/tmstat","Filesystem":"/var/tmstat","aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":""}} -{"time":1576001615000,"host":"bigip1","source":"bigip.tmsh.disk_usage","sourcetype":"f5:bigip:status:iapp:json","event":{"1024-blocks":"4096","Capacity":"1%","name":"/var/prompt","Filesystem":"/var/prompt","aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":""}} -{"time":1576001615000,"host":"bigip1","source":"bigip.tmsh.disk_usage","sourcetype":"f5:bigip:status:iapp:json","event":{"1024-blocks":"2958224","Capacity":"8%","name":"/var/log","Filesystem":"/var/log","aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":""}} -{"time":1576001615000,"host":"bigip1","source":"bigip.tmsh.disk_usage","sourcetype":"f5:bigip:status:iapp:json","event":{"1024-blocks":"25717852","Capacity":"4%","name":"/appdata","Filesystem":"/appdata","aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":""}} -{"time":1576001615000,"host":"bigip1","source":"bigip.tmsh.disk_usage","sourcetype":"f5:bigip:status:iapp:json","event":{"1024-blocks":"4088828","Capacity":"0%","name":"/var/loipc","Filesystem":"/var/loipc","aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":""}} -{"time":1576001615000,"host":"bigip1","source":"bigip.tmsh.disk_usage","sourcetype":"f5:bigip:status:iapp:json","event":{"1024-blocks":"817768","Capacity":"0%","name":"/run/user/91","Filesystem":"/run/user/91","aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":""}} -{"time":1576001615000,"host":"bigip1","source":"bigip.tmsh.disk_latency","sourcetype":"f5:bigip:status:iapp:json","event":{"r/s":"6.30","w/s":"3.81","%util":"0.61","name":"sda","Device":"sda","aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":""}} -{"time":1576001615000,"host":"bigip1","source":"bigip.tmsh.disk_latency","sourcetype":"f5:bigip:status:iapp:json","event":{"r/s":"0.00","w/s":"0.00","%util":"0.00","name":"dm-0","Device":"dm-0","aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":""}} -{"time":1576001615000,"host":"bigip1","source":"bigip.tmsh.disk_latency","sourcetype":"f5:bigip:status:iapp:json","event":{"r/s":"0.02","w/s":"1.65","%util":"0.09","name":"dm-1","Device":"dm-1","aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":""}} -{"time":1576001615000,"host":"bigip1","source":"bigip.tmsh.disk_latency","sourcetype":"f5:bigip:status:iapp:json","event":{"r/s":"0.74","w/s":"1.15","%util":"0.10","name":"dm-2","Device":"dm-2","aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":""}} -{"time":1576001615000,"host":"bigip1","source":"bigip.tmsh.disk_latency","sourcetype":"f5:bigip:status:iapp:json","event":{"r/s":"0.07","w/s":"2.09","%util":"0.10","name":"dm-3","Device":"dm-3","aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":""}} -{"time":1576001615000,"host":"bigip1","source":"bigip.tmsh.disk_latency","sourcetype":"f5:bigip:status:iapp:json","event":{"r/s":"0.28","w/s":"0.28","%util":"0.01","name":"dm-4","Device":"dm-4","aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":""}} -{"time":1576001615000,"host":"bigip1","source":"bigip.tmsh.disk_latency","sourcetype":"f5:bigip:status:iapp:json","event":{"r/s":"0.08","w/s":"0.36","%util":"0.04","name":"dm-5","Device":"dm-5","aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":""}} -{"time":1576001615000,"host":"bigip1","source":"bigip.tmsh.disk_latency","sourcetype":"f5:bigip:status:iapp:json","event":{"r/s":"3.97","w/s":"0.00","%util":"0.18","name":"dm-6","Device":"dm-6","aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":""}} -{"time":1576001615000,"host":"bigip1","source":"bigip.tmsh.disk_latency","sourcetype":"f5:bigip:status:iapp:json","event":{"r/s":"0.03","w/s":"0.01","%util":"0.00","name":"dm-7","Device":"dm-7","aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":""}} -{"time":1576001615000,"host":"bigip1","source":"bigip.tmsh.disk_latency","sourcetype":"f5:bigip:status:iapp:json","event":{"r/s":"0.38","w/s":"1.37","%util":"0.10","name":"dm-8","Device":"dm-8","aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":""}} -{"time":1576001615000,"host":"bigip1","source":"bigip.objectmodel.cert","sourcetype":"f5:bigip:config:iapp:json","event":{"aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":"","cert_name":"ca-bundle.crt","cert_subject":"CN=Starfield Services Root Certificate Authority,OU=http://certificates.starfieldtech.com/repository/,O=Starfield Technologies, Inc.,L=Scottsdale,ST=Arizona,C=US","cert_expiration_date":1893455999}} -{"time":1576001615000,"host":"bigip1","source":"bigip.objectmodel.cert","sourcetype":"f5:bigip:config:iapp:json","event":{"aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":"","cert_name":"default.crt","cert_subject":"emailAddress=root@localhost.localdomain,CN=localhost.localdomain,OU=IT,O=MyCompany,L=Seattle,ST=WA,C=US","cert_expiration_date":1887142817}} -{"time":1576001615000,"host":"bigip1","source":"bigip.objectmodel.cert","sourcetype":"f5:bigip:config:iapp:json","event":{"aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":"","cert_name":"f5-ca-bundle.crt","cert_subject":"CN=Entrust Root Certification Authority - G2,OU=(c) 2009 Entrust, Inc. - for authorized use only,OU=See www.entrust.net/legal-terms,O=Entrust, Inc.,C=US","cert_expiration_date":1922896554}} -{"time":1576001615000,"host":"bigip1","source":"bigip.objectmodel.cert","sourcetype":"f5:bigip:config:iapp:json","event":{"aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":"","cert_name":"f5-irule.crt","cert_subject":"emailAddress=support@f5.com,CN=support.f5.com,OU=Product Development,O=F5 Networks,L=Seattle,ST=Washington,C=US","cert_expiration_date":1815944413}} -{"time":1576001615000,"host":"bigip1","source":"bigip.tmsh.virtual_status","sourcetype":"f5:bigip:status:iapp:json","event":{"aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":"","virtual_name":"/Common/Shared/telemetry_local","app":"Shared","appComponent":"","tenant":"Common","availability_state":"unknown","enabled_state":"enabled","status_reason":""}} -{"time":1576001615000,"host":"bigip1","source":"bigip.tmsh.virtual_status","sourcetype":"f5:bigip:status:iapp:json","event":{"aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":"","virtual_name":"/Common/something","appComponent":"","tenant":"Common","availability_state":"unknown","enabled_state":"enabled","status_reason":""}} -{"time":1576001615000,"host":"bigip1","source":"bigip.tmsh.virtual_status","sourcetype":"f5:bigip:status:iapp:json","event":{"aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":"","virtual_name":"/Common/telemetry_gjd","appComponent":"","tenant":"Common","availability_state":"unknown","enabled_state":"enabled","status_reason":""}} -{"time":1576001615000,"host":"bigip1","source":"bigip.tmsh.virtual_status","sourcetype":"f5:bigip:status:iapp:json","event":{"aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":"","virtual_name":"/Common/testvs","appComponent":"","tenant":"Common","availability_state":"unknown","enabled_state":"enabled","status_reason":""}} -{"time":1576001615000,"host":"bigip1","source":"bigip.objectmodel.virtual","sourcetype":"f5:bigip:config:iapp:json","event":{"aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":"","virtual_name":"/Common/Shared/telemetry_local","app":"Shared","appComponent":"","tenant":"Common","iapp_name":"Shared","ip":"255.255.255.254:6514","mask":"255.255.255.255","port":"255.255.255.254:6514","protocol":"tcp"}} -{"time":1576001615000,"host":"bigip1","source":"bigip.objectmodel.virtual","sourcetype":"f5:bigip:config:iapp:json","event":{"aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":"","virtual_name":"/Common/something","appComponent":"","tenant":"Common","ip":"192.0.2.0:8787","mask":"255.255.255.255","port":"192.0.2.0:8787","protocol":"tcp"}} -{"time":1576001615000,"host":"bigip1","source":"bigip.objectmodel.virtual","sourcetype":"f5:bigip:config:iapp:json","event":{"aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":"","virtual_name":"/Common/telemetry_gjd","appComponent":"","tenant":"Common","ip":"255.255.255.254:1234","mask":"255.255.255.255","port":"255.255.255.254:1234","protocol":"tcp"}} -{"time":1576001615000,"host":"bigip1","source":"bigip.objectmodel.virtual","sourcetype":"f5:bigip:config:iapp:json","event":{"aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":"","virtual_name":"/Common/testvs","appComponent":"","tenant":"Common","ip":"192.0.2.0:7788","mask":"255.255.255.255","port":"192.0.2.0:7788","protocol":"tcp"}} -{"time":1576001615000,"host":"bigip1","source":"bigip.tmsh.pool_member_status","sourcetype":"f5:bigip:status:iapp:json","event":{"aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":"","pool_name":"/Common/Shared/telemetry","pool_member_name":"/Common/Shared/255.255.255.254:6514","callbackurl":"","address":"255.255.255.254","port":6514,"session_status":"","availability_state":"unknown","enabled_state":"enabled"}} -{"time":1576001615000,"host":"bigip1","source":"bigip.tmsh.pool_member_status","sourcetype":"f5:bigip:status:iapp:json","event":{"aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":"","pool_name":"/Common/static","pool_member_name":"/Common/192.0.2.0:8081","callbackurl":"","address":"192.0.2.0","port":8081,"session_status":"","availability_state":"unknown","enabled_state":"enabled"}} -{"time":1576001615000,"host":"bigip1","source":"bigip.tmsh.pool_member_status","sourcetype":"f5:bigip:status:iapp:json","event":{"aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":"","pool_name":"/Common/telemetry-local","pool_member_name":"/Common/192.0.2.1:6514","callbackurl":"","address":"192.0.2.1","port":6514,"session_status":"","availability_state":"unknown","enabled_state":"enabled"}} -{"time":1576001615000,"host":"bigip1","source":"bigip.tmsh.pool_member_status","sourcetype":"f5:bigip:status:iapp:json","event":{"aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":"","pool_name":"/Sample_01/A1/web_pool","pool_member_name":"/Sample_01/192.0.1.10:80","callbackurl":"","address":"192.0.1.10","port":80,"session_status":"","availability_state":"unknown","enabled_state":"enabled"}} -{"time":1576001615000,"host":"bigip1","source":"bigip.tmsh.pool_member_status","sourcetype":"f5:bigip:status:iapp:json","event":{"aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":"","pool_name":"/Sample_01/A1/web_pool","pool_member_name":"/Sample_01/192.0.1.11:80","callbackurl":"","address":"192.0.1.11","port":80,"session_status":"","availability_state":"unknown","enabled_state":"enabled"}} -{"time":1576001615000,"host":"bigip1","source":"bigip.tmsh.pool_member_status","sourcetype":"f5:bigip:status:iapp:json","event":{"aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":"","pool_name":"/Sample_01/A1/web_pool","pool_member_name":"/Sample_01/192.0.1.12:80","callbackurl":"","address":"192.0.1.12","port":80,"session_status":"","availability_state":"unknown","enabled_state":"enabled"}} -{"time":1576001615000,"host":"bigip1","source":"bigip.tmsh.pool_member_status","sourcetype":"f5:bigip:status:iapp:json","event":{"aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":"","pool_name":"/Sample_01/A1/web_pool","pool_member_name":"/Sample_01/192.0.1.13:80","callbackurl":"","address":"192.0.1.13","port":80,"session_status":"","availability_state":"unknown","enabled_state":"enabled"}} -{"time":1576001615000,"host":"bigip1","source":"bigip.tmsh.pool_member_status","sourcetype":"f5:bigip:status:iapp:json","event":{"aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":"","pool_name":"/Sample_event_sd/My_app/My_pool","pool_member_name":"/Sample_event_sd/192.0.2.6:80","callbackurl":"","address":"192.0.2.6","port":80,"session_status":"","availability_state":"unknown","enabled_state":"enabled"}} -{"time":1576001615000,"host":"bigip1","source":"bigip.stats.summary","sourcetype":"f5:bigip:stats:iapp:json","event":{"aggr_period":60,"device_base_mac":"fa:16:3e:da:e9:7b","devicegroup":"bigip1","facility":"","files_sent":1,"bytes_transfered":17072}}` + expectedData: [ + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.tmsh.system_status', + sourcetype: 'f5:bigip:status:iapp:json', + event: { + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '', + version: '14.1.0.6', + build: '0.0.9', + location: 'missing data', + description: 'missing data', + 'marketing-name': 'missing data', + 'platform-id': 'missing data', + 'failover-state': 'OFFLINE', + 'chassis-id': 'missing data', + mode: 'standalone', + 'sync-status': 'Standalone', + 'sync-summary': ' ', + 'sync-color': 'green', + asm_state: '', + last_asm_change: '', + apm_state: '', + afm_state: '', + last_afm_deploy: '', + ltm_config_time: '2019-12-10T18:13:26.000Z', + gtm_config_time: '2147483647' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.tmsh.interface_status', + sourcetype: 'f5:bigip:status:iapp:json', + event: { + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '', + interface_name: '1.0', + interface_status: 'up' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.tmsh.interface_status', + sourcetype: 'f5:bigip:status:iapp:json', + event: { + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '', + interface_name: 'mgmt', + interface_status: 'up' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.tmsh.disk_usage', + sourcetype: 'f5:bigip:status:iapp:json', + event: { + '1024-blocks': '428150', + Capacity: '20%', + name: '/', + Filesystem: '/', + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.tmsh.disk_usage', + sourcetype: 'f5:bigip:status:iapp:json', + event: { + '1024-blocks': '4079780', + Capacity: '1%', + name: '/dev', + Filesystem: '/dev', + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.tmsh.disk_usage', + sourcetype: 'f5:bigip:status:iapp:json', + event: { + '1024-blocks': '4088828', + Capacity: '1%', + name: '/dev/shm', + Filesystem: '/dev/shm', + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.tmsh.disk_usage', + sourcetype: 'f5:bigip:status:iapp:json', + event: { + '1024-blocks': '4088828', + Capacity: '1%', + name: '/run', + Filesystem: '/run', + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.tmsh.disk_usage', + sourcetype: 'f5:bigip:status:iapp:json', + event: { + '1024-blocks': '4088828', + Capacity: '0%', + name: '/sys/fs/cgroup', + Filesystem: '/sys/fs/cgroup', + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.tmsh.disk_usage', + sourcetype: 'f5:bigip:status:iapp:json', + event: { + '1024-blocks': '5186648', + Capacity: '84%', + name: '/usr', + Filesystem: '/usr', + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.tmsh.disk_usage', + sourcetype: 'f5:bigip:status:iapp:json', + event: { + '1024-blocks': '15350768', + Capacity: '2%', + name: '/shared', + Filesystem: '/shared', + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.tmsh.disk_usage', + sourcetype: 'f5:bigip:status:iapp:json', + event: { + '1024-blocks': '4088828', + Capacity: '2%', + name: '/shared/rrd.1.2', + Filesystem: '/shared/rrd.1.2', + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.tmsh.disk_usage', + sourcetype: 'f5:bigip:status:iapp:json', + event: { + '1024-blocks': '3030800', + Capacity: '34%', + name: '/var', + Filesystem: '/var', + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.tmsh.disk_usage', + sourcetype: 'f5:bigip:status:iapp:json', + event: { + '1024-blocks': '2171984', + Capacity: '2%', + name: '/config', + Filesystem: '/config', + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.tmsh.disk_usage', + sourcetype: 'f5:bigip:status:iapp:json', + event: { + '1024-blocks': '4088828', + Capacity: '1%', + name: '/var/tmstat', + Filesystem: '/var/tmstat', + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.tmsh.disk_usage', + sourcetype: 'f5:bigip:status:iapp:json', + event: { + '1024-blocks': '4096', + Capacity: '1%', + name: '/var/prompt', + Filesystem: '/var/prompt', + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.tmsh.disk_usage', + sourcetype: 'f5:bigip:status:iapp:json', + event: { + '1024-blocks': '2958224', + Capacity: '8%', + name: '/var/log', + Filesystem: '/var/log', + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.tmsh.disk_usage', + sourcetype: 'f5:bigip:status:iapp:json', + event: { + '1024-blocks': '25717852', + Capacity: '4%', + name: '/appdata', + Filesystem: '/appdata', + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.tmsh.disk_usage', + sourcetype: 'f5:bigip:status:iapp:json', + event: { + '1024-blocks': '4088828', + Capacity: '0%', + name: '/var/loipc', + Filesystem: '/var/loipc', + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.tmsh.disk_usage', + sourcetype: 'f5:bigip:status:iapp:json', + event: { + '1024-blocks': '817768', + Capacity: '0%', + name: '/run/user/91', + Filesystem: '/run/user/91', + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.tmsh.disk_latency', + sourcetype: 'f5:bigip:status:iapp:json', + event: { + 'r/s': '6.30', + 'w/s': '3.81', + '%util': '0.61', + name: 'sda', + Device: 'sda', + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.tmsh.disk_latency', + sourcetype: 'f5:bigip:status:iapp:json', + event: { + 'r/s': '0.00', + 'w/s': '0.00', + '%util': '0.00', + name: 'dm-0', + Device: 'dm-0', + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.tmsh.disk_latency', + sourcetype: 'f5:bigip:status:iapp:json', + event: { + 'r/s': '0.02', + 'w/s': '1.65', + '%util': '0.09', + name: 'dm-1', + Device: 'dm-1', + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.tmsh.disk_latency', + sourcetype: 'f5:bigip:status:iapp:json', + event: { + 'r/s': '0.74', + 'w/s': '1.15', + '%util': '0.10', + name: 'dm-2', + Device: 'dm-2', + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.tmsh.disk_latency', + sourcetype: 'f5:bigip:status:iapp:json', + event: { + 'r/s': '0.07', + 'w/s': '2.09', + '%util': '0.10', + name: 'dm-3', + Device: 'dm-3', + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.tmsh.disk_latency', + sourcetype: 'f5:bigip:status:iapp:json', + event: { + 'r/s': '0.28', + 'w/s': '0.28', + '%util': '0.01', + name: 'dm-4', + Device: 'dm-4', + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.tmsh.disk_latency', + sourcetype: 'f5:bigip:status:iapp:json', + event: { + 'r/s': '0.08', + 'w/s': '0.36', + '%util': '0.04', + name: 'dm-5', + Device: 'dm-5', + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.tmsh.disk_latency', + sourcetype: 'f5:bigip:status:iapp:json', + event: { + 'r/s': '3.97', + 'w/s': '0.00', + '%util': '0.18', + name: 'dm-6', + Device: 'dm-6', + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.tmsh.disk_latency', + sourcetype: 'f5:bigip:status:iapp:json', + event: { + 'r/s': '0.03', + 'w/s': '0.01', + '%util': '0.00', + name: 'dm-7', + Device: 'dm-7', + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.tmsh.disk_latency', + sourcetype: 'f5:bigip:status:iapp:json', + event: { + 'r/s': '0.38', + 'w/s': '1.37', + '%util': '0.10', + name: 'dm-8', + Device: 'dm-8', + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.objectmodel.cert', + sourcetype: 'f5:bigip:config:iapp:json', + event: { + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '', + cert_name: 'ca-bundle.crt', + cert_subject: 'CN=Starfield Services Root Certificate Authority,OU=http://certificates.starfieldtech.com/repository/,O=Starfield Technologies, Inc.,L=Scottsdale,ST=Arizona,C=US', + cert_expiration_date: 1893455999 + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.objectmodel.cert', + sourcetype: 'f5:bigip:config:iapp:json', + event: { + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '', + cert_name: 'default.crt', + cert_subject: 'emailAddress=root@localhost.localdomain,CN=localhost.localdomain,OU=IT,O=MyCompany,L=Seattle,ST=WA,C=US', + cert_expiration_date: 1887142817 + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.objectmodel.cert', + sourcetype: 'f5:bigip:config:iapp:json', + event: { + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '', + cert_name: 'f5-ca-bundle.crt', + cert_subject: 'CN=Entrust Root Certification Authority - G2,OU=(c) 2009 Entrust, Inc. - for authorized use only,OU=See www.entrust.net/legal-terms,O=Entrust, Inc.,C=US', + cert_expiration_date: 1922896554 + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.objectmodel.cert', + sourcetype: 'f5:bigip:config:iapp:json', + event: { + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '', + cert_name: 'f5-irule.crt', + cert_subject: 'emailAddress=support@f5.com,CN=support.f5.com,OU=Product Development,O=F5 Networks,L=Seattle,ST=Washington,C=US', + cert_expiration_date: 1815944413 + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.tmsh.virtual_status', + sourcetype: 'f5:bigip:status:iapp:json', + event: { + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '', + virtual_name: '/Common/Shared/telemetry_local', + app: 'Shared', + appComponent: '', + tenant: 'Common', + availability_state: 'unknown', + enabled_state: 'enabled', + status_reason: '' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.tmsh.virtual_status', + sourcetype: 'f5:bigip:status:iapp:json', + event: { + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '', + virtual_name: '/Common/something', + appComponent: '', + tenant: 'Common', + availability_state: 'unknown', + enabled_state: 'enabled', + status_reason: '' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.tmsh.virtual_status', + sourcetype: 'f5:bigip:status:iapp:json', + event: { + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '', + virtual_name: '/Common/telemetry_gjd', + appComponent: '', + tenant: 'Common', + availability_state: 'unknown', + enabled_state: 'enabled', + status_reason: '' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.tmsh.virtual_status', + sourcetype: 'f5:bigip:status:iapp:json', + event: { + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '', + virtual_name: '/Common/testvs', + appComponent: '', + tenant: 'Common', + availability_state: 'unknown', + enabled_state: 'enabled', + status_reason: '' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.objectmodel.virtual', + sourcetype: 'f5:bigip:config:iapp:json', + event: { + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '', + virtual_name: '/Common/Shared/telemetry_local', + app: 'Shared', + appComponent: '', + tenant: 'Common', + iapp_name: 'Shared', + ip: '255.255.255.254:6514', + mask: '255.255.255.255', + port: '255.255.255.254:6514', + protocol: 'tcp' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.objectmodel.virtual', + sourcetype: 'f5:bigip:config:iapp:json', + event: { + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '', + virtual_name: '/Common/something', + appComponent: '', + tenant: 'Common', + ip: '192.0.2.0:8787', + mask: '255.255.255.255', + port: '192.0.2.0:8787', + protocol: 'tcp' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.objectmodel.virtual', + sourcetype: 'f5:bigip:config:iapp:json', + event: { + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '', + virtual_name: '/Common/telemetry_gjd', + appComponent: '', + tenant: 'Common', + ip: '255.255.255.254:1234', + mask: '255.255.255.255', + port: '255.255.255.254:1234', + protocol: 'tcp' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.objectmodel.virtual', + sourcetype: 'f5:bigip:config:iapp:json', + event: { + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '', + virtual_name: '/Common/testvs', + appComponent: '', + tenant: 'Common', + ip: '192.0.2.0:7788', + mask: '255.255.255.255', + port: '192.0.2.0:7788', + protocol: 'tcp' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.objectmodel.virtual.profiles', + sourcetype: 'f5:bigip:config:iapp:json', + event: { + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '', + virtual_name: '/Common/Shared/telemetry_local', + profile_name: '/Common/tcp', + profile_type: 'profile', + tenant: 'Common', + app: 'Shared', + appComponent: '' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.objectmodel.virtual.profiles', + sourcetype: 'f5:bigip:config:iapp:json', + event: { + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '', + virtual_name: '/Common/Shared/telemetry_local', + profile_name: '/Common/app/http', + profile_type: 'profile', + tenant: 'Common', + app: 'Shared', + appComponent: '' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.objectmodel.virtual.profiles', + sourcetype: 'f5:bigip:config:iapp:json', + event: { + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '', + virtual_name: '/Common/something', + profile_name: '/Common/tcpCustom', + profile_type: 'profile', + tenant: 'Common', + appComponent: '' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.objectmodel.virtual.profiles', + sourcetype: 'f5:bigip:config:iapp:json', + event: { + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '', + virtual_name: '/Common/something', + profile_name: '/Common/app/http', + profile_type: 'profile', + tenant: 'Common', + appComponent: '' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.objectmodel.virtual.pools', + sourcetype: 'f5:bigip:config:iapp:json', + event: { + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '', + virtual_name: '/Common/something', + appComponent: '', + tenant: 'Common', + pool_name: '/Common/static' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.objectmodel.virtual.pools', + sourcetype: 'f5:bigip:config:iapp:json', + event: { + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '', + virtual_name: '/Common/testvs', + appComponent: '', + tenant: 'Common', + pool_name: '/Common/static' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.tmsh.pool_member_status', + sourcetype: 'f5:bigip:status:iapp:json', + event: { + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '', + pool_name: '/Common/Shared/telemetry', + pool_member_name: '/Common/Shared/255.255.255.254:6514', + callbackurl: '', + address: '255.255.255.254', + port: 6514, + session_status: '', + availability_state: 'unknown', + enabled_state: 'enabled' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.tmsh.pool_member_status', + sourcetype: 'f5:bigip:status:iapp:json', + event: { + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '', + pool_name: '/Common/static', + pool_member_name: '/Common/192.0.2.0:8081', + callbackurl: '', + address: '192.0.2.0', + port: 8081, + session_status: '', + availability_state: 'unknown', + enabled_state: 'enabled' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.tmsh.pool_member_status', + sourcetype: 'f5:bigip:status:iapp:json', + event: { + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '', + pool_name: '/Common/telemetry-local', + pool_member_name: '/Common/192.0.2.1:6514', + callbackurl: '', + address: '192.0.2.1', + port: 6514, + session_status: '', + availability_state: 'unknown', + enabled_state: 'enabled' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.tmsh.pool_member_status', + sourcetype: 'f5:bigip:status:iapp:json', + event: { + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '', + pool_name: '/Sample_01/A1/web_pool', + pool_member_name: '/Sample_01/192.0.1.10:80', + callbackurl: '', + address: '192.0.1.10', + port: 80, + session_status: '', + availability_state: 'unknown', + enabled_state: 'enabled' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.tmsh.pool_member_status', + sourcetype: 'f5:bigip:status:iapp:json', + event: { + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '', + pool_name: '/Sample_01/A1/web_pool', + pool_member_name: '/Sample_01/192.0.1.11:80', + callbackurl: '', + address: '192.0.1.11', + port: 80, + session_status: '', + availability_state: 'unknown', + enabled_state: 'enabled' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.tmsh.pool_member_status', + sourcetype: 'f5:bigip:status:iapp:json', + event: { + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '', + pool_name: '/Sample_01/A1/web_pool', + pool_member_name: '/Sample_01/192.0.1.12:80', + callbackurl: '', + address: '192.0.1.12', + port: 80, + session_status: '', + availability_state: 'unknown', + enabled_state: 'enabled' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.tmsh.pool_member_status', + sourcetype: 'f5:bigip:status:iapp:json', + event: { + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '', + pool_name: '/Sample_01/A1/web_pool', + pool_member_name: '/Sample_01/192.0.1.13:80', + callbackurl: '', + address: '192.0.1.13', + port: 80, + session_status: '', + availability_state: 'unknown', + enabled_state: 'enabled' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.tmsh.pool_member_status', + sourcetype: 'f5:bigip:status:iapp:json', + event: { + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '', + pool_name: '/Sample_event_sd/My_app/My_pool', + pool_member_name: '/Sample_event_sd/192.0.2.6:80', + callbackurl: '', + address: '192.0.2.6', + port: 80, + session_status: '', + availability_state: 'unknown', + enabled_state: 'enabled' + } + }, + { + time: 1576001615000, + host: 'bigip1', + source: 'bigip.stats.summary', + sourcetype: 'f5:bigip:stats:iapp:json', + event: { + aggr_period: 60, + device_base_mac: 'fa:16:3e:da:e9:7b', + devicegroup: 'bigip1', + facility: '', + files_sent: 1, + bytes_transfered: 19197 + } + } + ] } ] }; diff --git a/test/unit/consumers/statsdConsumerTests.js b/test/unit/consumers/statsdConsumerTests.js index 9517f00a..9c531374 100644 --- a/test/unit/consumers/statsdConsumerTests.js +++ b/test/unit/consumers/statsdConsumerTests.js @@ -8,19 +8,24 @@ 'use strict'; +/* eslint-disable import/order */ + +require('../shared/restoreCache')(); + const chai = require('chai'); const chaiAsPromised = require('chai-as-promised'); const proxyquire = require('proxyquire'); +const sinon = require('sinon'); + +const statsdExpectedData = require('./statsdConsumerTestsData'); +const testUtil = require('../shared/util'); chai.use(chaiAsPromised); const assert = chai.assert; -const sinon = require('sinon'); - -let statsDIndex; // later used to require ../../../src/lib/consumers/Statsd/index -const util = require('../shared/util.js'); -/* eslint-disable global-require */ describe('Statsd', () => { + let statsDIndex; // later used to require ../../../src/lib/consumers/Statsd/index + let passedClientParams; let metrics; @@ -58,37 +63,37 @@ describe('Statsd', () => { describe('process', () => { it('should configure StatsD client with default options', () => { - const context = util.buildConsumerContext({ + const context = testUtil.buildConsumerContext({ eventType: 'systemInfo', config: defaultConsumerConfig }); return statsDIndex(context) - .then(() => assert.deepEqual(passedClientParams, { + .then(() => assert.deepStrictEqual(passedClientParams, { host: 'statsd-host', port: '8125' })); }); it('should process systemInfo data', () => { - const context = util.buildConsumerContext({ + const context = testUtil.buildConsumerContext({ eventType: 'systemInfo', config: defaultConsumerConfig }); - const expectedData = require('./statsdConsumerTestsData.js').systemData[0].expectedData; + const expectedData = statsdExpectedData.systemData[0].expectedData; return statsDIndex(context) - .then(() => assert.deepEqual(metrics, expectedData)); + .then(() => assert.deepStrictEqual(metrics, expectedData)); }); it('should NOT process event data', () => { - const context = util.buildConsumerContext({ + const context = testUtil.buildConsumerContext({ eventType: 'AVR', config: defaultConsumerConfig }); return statsDIndex(context) - .then(() => assert.deepEqual(metrics, [])); + .then(() => assert.deepStrictEqual(metrics, [])); }); }); }); diff --git a/test/unit/consumers/sumoLogicConsumerTests.js b/test/unit/consumers/sumoLogicConsumerTests.js index 6faab7c6..379cc37f 100644 --- a/test/unit/consumers/sumoLogicConsumerTests.js +++ b/test/unit/consumers/sumoLogicConsumerTests.js @@ -8,18 +8,21 @@ 'use strict'; +/* eslint-disable import/order */ + +require('../shared/restoreCache')(); + const chai = require('chai'); const chaiAsPromised = require('chai-as-promised'); const request = require('request'); - -chai.use(chaiAsPromised); -const assert = chai.assert; const sinon = require('sinon'); const sumoLogicIndex = require('../../../src/lib/consumers/Sumo_Logic/index'); -const util = require('../shared/util.js'); +const testUtil = require('../shared/util'); + +chai.use(chaiAsPromised); +const assert = chai.assert; -/* eslint-disable global-require */ describe('Sumo_Logic', () => { afterEach(() => { sinon.restore(); @@ -32,19 +35,19 @@ describe('Sumo_Logic', () => { }; it('should configure default request options', (done) => { - const context = util.buildConsumerContext({ + const context = testUtil.buildConsumerContext({ config: defaultConsumerConfig }); sinon.stub(request, 'post').callsFake((opts) => { try { - assert.deepEqual(opts.headers, { 'content-type': 'application/json' }); + assert.deepStrictEqual(opts.headers, { 'content-type': 'application/json' }); assert.strictEqual(opts.strictSSL, true); assert.strictEqual(opts.url, 'https://localhost:443/receiver/v1/http/'); done(); } catch (err) { // done() with parameter is treated as an error. - // Use catch back to pass thrown error from assert.deepEqual to done() callback + // Use catch back to pass thrown error from assert.deepStrictEqual to done() callback done(err); } }); @@ -53,7 +56,7 @@ describe('Sumo_Logic', () => { }); it('should configure request options with provided values', (done) => { - const context = util.buildConsumerContext({ + const context = testUtil.buildConsumerContext({ config: { host: 'localhost', path: '/receiver/v1/http/', @@ -66,13 +69,46 @@ describe('Sumo_Logic', () => { sinon.stub(request, 'post').callsFake((opts) => { try { - assert.deepEqual(opts.headers, { 'content-type': 'application/json' }); + assert.deepStrictEqual(opts.headers, { 'content-type': 'application/json' }); assert.strictEqual(opts.strictSSL, false); assert.strictEqual(opts.url, 'http://localhost:80/receiver/v1/http/mySecret'); done(); } catch (err) { // done() with parameter is treated as an error. - // Use catch back to pass thrown error from assert.deepEqual to done() callback + // Use catch back to pass thrown error from assert.deepStrictEqual to done() callback + done(err); + } + }); + + sumoLogicIndex(context); + }); + + it('should trace data with secrets redacted', (done) => { + let traceData; + const context = testUtil.buildConsumerContext({ + config: { + host: 'localhost', + path: '/receiver/v1/http/', + passphrase: 'mySecret', + protocol: 'http', + port: 80, + allowSelfSignedCert: true + } + }); + context.tracer = { + write: (input) => { + traceData = JSON.parse(input); + } + }; + + sinon.stub(request, 'post').callsFake((opts) => { + try { + assert.notStrictEqual(traceData.url.indexOf('*****'), -1); + assert.strictEqual(opts.url, 'http://localhost:80/receiver/v1/http/mySecret'); + done(); + } catch (err) { + // done() with parameter is treated as an error. + // Use catch back to pass thrown error from assert.deepStrictEqual to done() callback done(err); } }); @@ -81,19 +117,19 @@ describe('Sumo_Logic', () => { }); it('should process systemInfo data', (done) => { - const context = util.buildConsumerContext({ + const context = testUtil.buildConsumerContext({ eventType: 'systemInfo', config: defaultConsumerConfig }); - const expectedData = util.deepCopy(context.event.data); + const expectedData = testUtil.deepCopy(context.event.data); sinon.stub(request, 'post').callsFake((opts) => { try { - assert.deepEqual(opts.body, JSON.stringify(expectedData)); + assert.deepStrictEqual(opts.body, JSON.stringify(expectedData)); done(); } catch (err) { // done() with parameter is treated as an error. - // Use catch back to pass thrown error from assert.deepEqual to done() callback + // Use catch back to pass thrown error from assert.deepStrictEqual to done() callback done(err); } }); @@ -102,19 +138,19 @@ describe('Sumo_Logic', () => { }); it('should process event data', (done) => { - const context = util.buildConsumerContext({ + const context = testUtil.buildConsumerContext({ eventType: 'AVR', config: defaultConsumerConfig }); - const expectedData = util.deepCopy(context.event.data); + const expectedData = testUtil.deepCopy(context.event.data); sinon.stub(request, 'post').callsFake((opts) => { try { - assert.deepEqual(opts.body, JSON.stringify(expectedData)); + assert.deepStrictEqual(opts.body, JSON.stringify(expectedData)); done(); } catch (err) { // done() with parameter is treated as an error. - // Use catch back to pass thrown error from assert.deepEqual to done() callback + // Use catch back to pass thrown error from assert.deepStrictEqual to done() callback done(err); } }); diff --git a/test/unit/consumersPluginTests.js b/test/unit/consumersPluginTests.js deleted file mode 100644 index 3d2c5769..00000000 --- a/test/unit/consumersPluginTests.js +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2018. F5 Networks, Inc. See End User License Agreement ("EULA") for - * license terms. Notwithstanding anything to the contrary in the EULA, Licensee - * may copy and modify this software product for its internal business purposes. - * Further, Licensee may upload, publish and distribute the modified version of - * the software product on devcentral.f5.com. - */ - -'use strict'; - -const assert = require('assert'); - -/* eslint-disable global-require */ - -describe('Consumers plugins', () => { - let cDefault; - let context; - - before(() => { - cDefault = require('../../src/lib/consumers/default'); - }); - beforeEach(() => { - const tracerMock = { - write() {} - }; - const loggerMock = { - error() {}, - exception() {}, - info() {}, - debug() {} - }; - context = { - event: {}, - config: {}, - tracer: tracerMock, - logger: loggerMock - }; - }); - after(() => { - Object.keys(require.cache).forEach((key) => { - delete require.cache[key]; - }); - }); - - it('default: should process event', () => { - cDefault(context) - .then(() => { - assert.strictEqual(1, 1); - }) - .catch(err => Promise.reject(err)); - }); - - it('default: should reject on missing event', () => { - context.event = undefined; - return cDefault(context) - .then(() => { - assert.fail('Should throw an error'); - }) - .catch((err) => { - if (err.code === 'ERR_ASSERTION') return Promise.reject(err); - if (/No event to process/.test(err)) return Promise.resolve(); - assert.fail(err); - return Promise.reject(err); - }); - }); - - it('default: should continue without tracer', () => { - context.tracer = undefined; - return cDefault(context) - .then(() => Promise.resolve()) - .catch(() => Promise.reject()); - }); -}); diff --git a/test/unit/consumersTests.js b/test/unit/consumersTests.js index 81db14ee..e3726213 100644 --- a/test/unit/consumersTests.js +++ b/test/unit/consumersTests.js @@ -8,33 +8,25 @@ 'use strict'; +/* eslint-disable import/order */ + +require('./shared/restoreCache')(); +require('./shared/disableAjv'); // consumers imports config with ajv + const chai = require('chai'); const chaiAsPromised = require('chai-as-promised'); +const config = require('../../src/lib/config'); +const constants = require('../../src/lib/constants'); +const consumers = require('../../src/lib/consumers'); + chai.use(chaiAsPromised); const assert = chai.assert; -const constants = require('../../src/lib/constants.js'); - -/* eslint-disable global-require */ - describe('Consumers', () => { - let config; - let consumers; - - before(() => { - config = require('../../src/lib/config.js'); - consumers = require('../../src/lib/consumers.js'); - }); - after(() => { - Object.keys(require.cache).forEach((key) => { - delete require.cache[key]; - }); - }); - it('should get valid consumers', () => { const exampleConfig = {}; - exampleConfig[constants.CONSUMERS_CLASS_NAME] = { + exampleConfig[constants.CONFIG_CLASSES.CONSUMER_CLASS_NAME] = { My_Consumer: { class: 'Consumer', type: 'default' @@ -45,22 +37,19 @@ describe('Consumers', () => { } }; config.emit('change', exampleConfig); // emit change event, then wait a short period - - return new Promise(resolve => setTimeout(() => { resolve(); }, 250)) + return assert.isFulfilled(new Promise(resolve => setTimeout(() => { resolve(); }, 250)) .then(() => { const loadedConsumers = consumers.getConsumers(); - assert.strictEqual(1, loadedConsumers.length); - }) - .catch(err => Promise.reject(err)); + assert.strictEqual(loadedConsumers.length, 1, 'should load default consumer'); + })); }); it('should return empty list of consumers', () => { config.emit('change', {}); // emit change event, then wait a short period - return new Promise(resolve => setTimeout(() => { resolve(); }, 250)) .then(() => { const loadedConsumers = consumers.getConsumers(); - assert.strictEqual(0, loadedConsumers.length); + assert.strictEqual(loadedConsumers.length, 0); }) .catch(err => Promise.reject(err)); }); diff --git a/test/unit/customEndpointsTests.js b/test/unit/customEndpointsTests.js new file mode 100644 index 00000000..8f59108a --- /dev/null +++ b/test/unit/customEndpointsTests.js @@ -0,0 +1,74 @@ +/* + * Copyright 2020. F5 Networks, Inc. See End User License Agreement ('EULA') for + * license terms. Notwithstanding anything to the contrary in the EULA, Licensee + * may copy and modify this software product for its internal business purposes. + * Further, Licensee may upload, publish and distribute the modified version of + * the software product on devcentral.f5.com. + */ + +'use strict'; + +const chai = require('chai'); +const chaiAsPromised = require('chai-as-promised'); +const nock = require('nock'); + +const testUtil = require('./shared/util'); +const SystemStats = require('../../src/lib/systemStats'); +const customEndptsTestsData = require('./customEndpointsTestsData'); + +chai.use(chaiAsPromised); +const assert = chai.assert; + +describe('Custom Endpoints (Telemetry_Endpoints)', () => { + const checkResponse = (endpointMock, response) => { + if (!response.kind) { + throw new Error(`Endpoint '${endpointMock.endpoint}' has no property 'kind' in response`); + } + }; + + Object.keys(customEndptsTestsData).forEach((testSetKey) => { + const testSet = customEndptsTestsData[testSetKey]; + + testUtil.getCallableDescribe(testSet)(testSet.name, () => { + afterEach(() => { + nock.cleanAll(); + }); + + testSet.tests.forEach((testConf) => { + testUtil.getCallableIt(testConf)(testConf.name, () => { + const options = { + endpointList: testConf.endpointList + }; + const getCollectedData = testConf.getCollectedData + ? testConf.getCollectedData : promise => promise; + + const stats = new SystemStats(options); + + return Promise.resolve() + .then(() => { + testUtil.mockEndpoints(testConf.endpoints || [], { responseChecker: checkResponse }); + return assert.becomes( + getCollectedData(stats.collect(), stats), + testConf.expectedData, + 'should match expected output on first attempt to collect data' + ); + }) + .then(() => { + assert.deepStrictEqual(stats.loader.cachedResponse, {}, 'cache should be erased'); + }) + .then(() => { + testUtil.mockEndpoints(testConf.endpoints || [], { responseChecker: checkResponse }); + return assert.becomes( + getCollectedData(stats.collect(), stats), + testConf.expectedData, + 'should match expected output on second attempt to collect data' + ); + }) + .then(() => { + assert.deepStrictEqual(stats.loader.cachedResponse, {}, 'cache should be erased'); + }); + }); + }); + }); + }); +}); diff --git a/test/unit/customEndpointsTestsData.js b/test/unit/customEndpointsTestsData.js new file mode 100644 index 00000000..e897ac49 --- /dev/null +++ b/test/unit/customEndpointsTestsData.js @@ -0,0 +1,501 @@ +/* + * Copyright 2020. F5 Networks, Inc. See End User License Agreement ('EULA') for + * license terms. Notwithstanding anything to the contrary in the EULA, Licensee + * may copy and modify this software product for its internal business purposes. + * Further, Licensee may upload, publish and distribute the modified version of + * the software product on devcentral.f5.com. + */ + +'use strict'; + + +module.exports = { + collectVirtualServersCustom: { + name: 'virtual servers and stats', + tests: [ + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should set virtualServers to { items: [] } if not configured (with items property as empty array)', + endpointList: { + virtualServers: { + name: 'virtualServers', + path: '/mgmt/tm/ltm/virtual' + } + }, + expectedData: { + virtualServers: { + items: [] + } + }, + endpoints: [ + { + endpoint: /\/mgmt\/tm\/ltm\/virtual/, + response: { + kind: 'tm:ltm:virtual:virtualcollectionstate', + selfLink: 'https://localhost/mgmt/tm/ltm/virtual?ver=14.1.0', + items: [] + } + } + ] + }, + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should set virtualServers to { items: [] } if not configured (without items property)', + endpointList: { + virtualServers: { + name: 'virtualServers', + path: '/mgmt/tm/ltm/virtual' + } + }, + expectedData: { + virtualServers: { + items: [] + } + }, + endpoints: [ + { + endpoint: /\/mgmt\/tm\/ltm\/virtual/, + response: { + kind: 'tm:ltm:virtual:virtualcollectionstate', + selfLink: 'https://localhost/mgmt/tm/ltm/virtual?ver=14.1.0' + } + } + ] + }, + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should collect virtual servers and stats', + endpointList: { + virtualServers: { + name: 'virtualServers', + path: '/mgmt/tm/ltm/virtual?$select=name,kind,partition,fullPath,destination' + }, + virtualServersStats: { + name: 'virtualServersStats', + path: '/mgmt/tm/ltm/virtual/stats' + } + }, + expectedData: { + virtualServers: { + items: [ + { + kind: 'tm:ltm:virtual:virtualstate', + name: 'default', + partition: 'Common', + fullPath: '/Common/default', + destination: '/Common/172.16.100.17:53' + }, + { + kind: 'tm:ltm:virtual:virtualstate', + name: 'vs_with_pool', + partition: 'Common', + fullPath: '/Common/vs_with_pool', + destination: '/Common/10.12.12.49:8443' + } + ] + }, + virtualServersStats: { + '/Common/default/stats': { + 'clientside.bitsIn': 0, + 'clientside.bitsOut': 0, + 'clientside.curConns': 0, + 'clientside.evictedConns': 0, + 'clientside.maxConns': 0, + 'clientside.pktsIn': 0, + 'clientside.pktsOut': 0, + 'clientside.slowKilled': 0, + 'clientside.totConns': 0, + cmpEnableMode: 'all-cpus', + cmpEnabled: 'enabled', + csMaxConnDur: 0, + csMeanConnDur: 0, + csMinConnDur: 0, + destination: '172.16.100.17:53', + 'ephemeral.bitsIn': 0, + 'ephemeral.bitsOut': 0, + 'ephemeral.curConns': 0, + 'ephemeral.evictedConns': 0, + 'ephemeral.maxConns': 0, + 'ephemeral.pktsIn': 0, + 'ephemeral.pktsOut': 0, + 'ephemeral.slowKilled': 0, + 'ephemeral.totConns': 0, + fiveMinAvgUsageRatio: 0, + fiveSecAvgUsageRatio: 0, + tmName: '/Common/default', + oneMinAvgUsageRatio: 0, + availabilityState: 'unknown', + enabledState: 'enabled', + 'status.statusReason': "The children pool member(s) either don't have service checking enabled, or service check results are not available yet", + syncookieStatus: 'not-activated', + 'syncookie.accepts': 0, + 'syncookie.hwAccepts': 0, + 'syncookie.hwSyncookies': 0, + 'syncookie.hwsyncookieInstance': 0, + 'syncookie.rejects': 0, + 'syncookie.swsyncookieInstance': 0, + 'syncookie.syncacheCurr': 0, + 'syncookie.syncacheOver': 0, + 'syncookie.syncookies': 0, + totRequests: 0, + name: '/Common/default/stats' + }, + '/Common/vs_with_pool/stats': { + 'clientside.bitsIn': 0, + 'clientside.bitsOut': 0, + 'clientside.curConns': 0, + 'clientside.evictedConns': 0, + 'clientside.maxConns': 0, + 'clientside.pktsIn': 0, + 'clientside.pktsOut': 0, + 'clientside.slowKilled': 0, + 'clientside.totConns': 0, + cmpEnableMode: 'all-cpus', + cmpEnabled: 'enabled', + csMaxConnDur: 0, + csMeanConnDur: 0, + csMinConnDur: 0, + destination: '10.12.12.49:8443', + 'ephemeral.bitsIn': 0, + 'ephemeral.bitsOut': 0, + 'ephemeral.curConns': 0, + 'ephemeral.evictedConns': 0, + 'ephemeral.maxConns': 0, + 'ephemeral.pktsIn': 0, + 'ephemeral.pktsOut': 0, + 'ephemeral.slowKilled': 0, + 'ephemeral.totConns': 0, + fiveMinAvgUsageRatio: 0, + fiveSecAvgUsageRatio: 0, + tmName: '/Common/vs_with_pool', + oneMinAvgUsageRatio: 0, + availabilityState: 'offline', + enabledState: 'enabled', + 'status.statusReason': 'The children pool member(s) are down', + syncookieStatus: 'not-activated', + 'syncookie.accepts': 0, + 'syncookie.hwAccepts': 0, + 'syncookie.hwSyncookies': 0, + 'syncookie.hwsyncookieInstance': 0, + 'syncookie.rejects': 0, + 'syncookie.swsyncookieInstance': 0, + 'syncookie.syncacheCurr': 0, + 'syncookie.syncacheOver': 0, + 'syncookie.syncookies': 0, + totRequests: 0, + name: '/Common/vs_with_pool/stats' + } + } + }, + endpoints: [ + { + endpoint: '/mgmt/tm/ltm/virtual?$select=name,kind,partition,fullPath,destination', + response: { + kind: 'tm:ltm:virtual:virtualcollectionstate', + selfLink: 'https://localhost/mgmt/tm/ltm/virtual?$select=name%2Ckind%2Cpartition%2CfullPath%2Cdestination&ver=13.1.1.4', + items: [ + { + kind: 'tm:ltm:virtual:virtualstate', + name: 'default', + partition: 'Common', + fullPath: '/Common/default', + destination: '/Common/172.16.100.17:53' + }, + { + kind: 'tm:ltm:virtual:virtualstate', + name: 'vs_with_pool', + partition: 'Common', + fullPath: '/Common/vs_with_pool', + destination: '/Common/10.12.12.49:8443' + } + ] + } + }, + { + endpoint: '/mgmt/tm/ltm/virtual/stats', + response: { + kind: 'tm:ltm:virtual:virtualcollectionstats', + selfLink: 'https://localhost/mgmt/tm/ltm/virtual/stats?ver=13.1.1.4', + entries: { + 'https://localhost/mgmt/tm/ltm/virtual/~Common~default/stats': { + nestedStats: { + kind: 'tm:ltm:virtual:virtualstats', + selfLink: 'https://localhost/mgmt/tm/ltm/virtual/~Common~default/stats?ver=13.1.1.4', + entries: { + 'clientside.bitsIn': { + value: 0 + }, + 'clientside.bitsOut': { + value: 0 + }, + 'clientside.curConns': { + value: 0 + }, + 'clientside.evictedConns': { + value: 0 + }, + 'clientside.maxConns': { + value: 0 + }, + 'clientside.pktsIn': { + value: 0 + }, + 'clientside.pktsOut': { + value: 0 + }, + 'clientside.slowKilled': { + value: 0 + }, + 'clientside.totConns': { + value: 0 + }, + cmpEnableMode: { + description: 'all-cpus' + }, + cmpEnabled: { + description: 'enabled' + }, + csMaxConnDur: { + value: 0 + }, + csMeanConnDur: { + value: 0 + }, + csMinConnDur: { + value: 0 + }, + destination: { + description: '172.16.100.17:53' + }, + 'ephemeral.bitsIn': { + value: 0 + }, + 'ephemeral.bitsOut': { + value: 0 + }, + 'ephemeral.curConns': { + value: 0 + }, + 'ephemeral.evictedConns': { + value: 0 + }, + 'ephemeral.maxConns': { + value: 0 + }, + 'ephemeral.pktsIn': { + value: 0 + }, + 'ephemeral.pktsOut': { + value: 0 + }, + 'ephemeral.slowKilled': { + value: 0 + }, + 'ephemeral.totConns': { + value: 0 + }, + fiveMinAvgUsageRatio: { + value: 0 + }, + fiveSecAvgUsageRatio: { + value: 0 + }, + tmName: { + description: '/Common/default' + }, + oneMinAvgUsageRatio: { + value: 0 + }, + 'status.availabilityState': { + description: 'unknown' + }, + 'status.enabledState': { + description: 'enabled' + }, + 'status.statusReason': { + description: "The children pool member(s) either don't have service checking enabled, or service check results are not available yet" + }, + syncookieStatus: { + description: 'not-activated' + }, + 'syncookie.accepts': { + value: 0 + }, + 'syncookie.hwAccepts': { + value: 0 + }, + 'syncookie.hwSyncookies': { + value: 0 + }, + 'syncookie.hwsyncookieInstance': { + value: 0 + }, + 'syncookie.rejects': { + value: 0 + }, + 'syncookie.swsyncookieInstance': { + value: 0 + }, + 'syncookie.syncacheCurr': { + value: 0 + }, + 'syncookie.syncacheOver': { + value: 0 + }, + 'syncookie.syncookies': { + value: 0 + }, + totRequests: { + value: 0 + } + } + } + }, + 'https://localhost/mgmt/tm/ltm/virtual/~Common~vs_with_pool/stats': { + nestedStats: { + kind: 'tm:ltm:virtual:virtualstats', + selfLink: 'https://localhost/mgmt/tm/ltm/virtual/~Common~vs_with_pool/stats?ver=13.1.1.4', + entries: { + 'clientside.bitsIn': { + value: 0 + }, + 'clientside.bitsOut': { + value: 0 + }, + 'clientside.curConns': { + value: 0 + }, + 'clientside.evictedConns': { + value: 0 + }, + 'clientside.maxConns': { + value: 0 + }, + 'clientside.pktsIn': { + value: 0 + }, + 'clientside.pktsOut': { + value: 0 + }, + 'clientside.slowKilled': { + value: 0 + }, + 'clientside.totConns': { + value: 0 + }, + cmpEnableMode: { + description: 'all-cpus' + }, + cmpEnabled: { + description: 'enabled' + }, + csMaxConnDur: { + value: 0 + }, + csMeanConnDur: { + value: 0 + }, + csMinConnDur: { + value: 0 + }, + destination: { + description: '10.12.12.49:8443' + }, + 'ephemeral.bitsIn': { + value: 0 + }, + 'ephemeral.bitsOut': { + value: 0 + }, + 'ephemeral.curConns': { + value: 0 + }, + 'ephemeral.evictedConns': { + value: 0 + }, + 'ephemeral.maxConns': { + value: 0 + }, + 'ephemeral.pktsIn': { + value: 0 + }, + 'ephemeral.pktsOut': { + value: 0 + }, + 'ephemeral.slowKilled': { + value: 0 + }, + 'ephemeral.totConns': { + value: 0 + }, + fiveMinAvgUsageRatio: { + value: 0 + }, + fiveSecAvgUsageRatio: { + value: 0 + }, + tmName: { + description: '/Common/vs_with_pool' + }, + oneMinAvgUsageRatio: { + value: 0 + }, + 'status.availabilityState': { + description: 'offline' + }, + 'status.enabledState': { + description: 'enabled' + }, + 'status.statusReason': { + description: 'The children pool member(s) are down' + }, + syncookieStatus: { + description: 'not-activated' + }, + 'syncookie.accepts': { + value: 0 + }, + 'syncookie.hwAccepts': { + value: 0 + }, + 'syncookie.hwSyncookies': { + value: 0 + }, + 'syncookie.hwsyncookieInstance': { + value: 0 + }, + 'syncookie.rejects': { + value: 0 + }, + 'syncookie.swsyncookieInstance': { + value: 0 + }, + 'syncookie.syncacheCurr': { + value: 0 + }, + 'syncookie.syncacheOver': { + value: 0 + }, + 'syncookie.syncookies': { + value: 0 + }, + totRequests: { + value: 0 + } + } + } + } + } + } + } + ] + } + ] + } +}; diff --git a/test/unit/dataFilterTests.js b/test/unit/dataFilterTests.js index bf427fc9..7d051054 100644 --- a/test/unit/dataFilterTests.js +++ b/test/unit/dataFilterTests.js @@ -8,11 +8,19 @@ 'use strict'; -const assert = require('assert'); +/* eslint-disable import/order */ -const dataFilter = require('../../src/lib/dataFilter.js'); -const dataFilterTestsData = require('./dataFilterTestsData.js'); +require('./shared/restoreCache')(); +const chai = require('chai'); +const chaiAsPromised = require('chai-as-promised'); + +const dataFilterTestsData = require('./dataFilterTestsData'); +const dataFilter = require('../../src/lib/dataFilter'); +const testUtil = require('./shared/util'); + +chai.use(chaiAsPromised); +const assert = chai.assert; describe('Data Filter', () => { describe('DataFilter', () => { @@ -20,10 +28,18 @@ describe('Data Filter', () => { const consumerConfig = { type: 'Kafka' }; + const data = { + data: { + tmstats: {} + } + }; + const expected = { tmstats: true }; const filter = new dataFilter.DataFilter(consumerConfig); + const filteredData = filter.apply(data); - assert.deepEqual(filter.blacklist, expected); + assert.deepStrictEqual(filter.blacklist, expected); + assert.deepStrictEqual(filteredData, { data: {} }); }); it('should not blacklist tmstats if consumer is Splunk legacy', () => { @@ -33,18 +49,23 @@ describe('Data Filter', () => { format: 'legacy' } }; + const data = { + data: { + tmstats: {} + } + }; const expected = {}; const filter = new dataFilter.DataFilter(consumerConfig); + const filteredData = filter.apply(data); - assert.deepEqual(filter.blacklist, expected); + assert.deepStrictEqual(filter.blacklist, expected); + assert.deepStrictEqual(filteredData, data); }); }); describe('handleAction', () => { - const getCallableIt = testConf => (testConf.testOpts && testConf.testOpts.only ? it.only : it); - dataFilterTestsData.handleAction.forEach((testConf) => { - getCallableIt(testConf)(testConf.name, () => { + testUtil.getCallableIt(testConf)(testConf.name, () => { dataFilter.handleAction(testConf.dataCtx, testConf.actionCtx); assert.deepStrictEqual(testConf.dataCtx, testConf.expectedCtx); }); diff --git a/test/unit/dataFilterTestsData.js b/test/unit/dataFilterTestsData.js index 34c8750e..f3b592b3 100644 --- a/test/unit/dataFilterTestsData.js +++ b/test/unit/dataFilterTestsData.js @@ -69,6 +69,36 @@ module.exports = { } }, // TEST RELATED DATA STARTS HERE + { + name: 'should execute action when ifAnyMatch is valid', + actionCtx: { + enable: true, + includeData: {}, + locations: { + tag: 'tag' + }, + ifAnyMatch: [ + { + foo: 'bar' + }, + { + foo: 'baz' + } + ] + }, + dataCtx: { + data: { + foo: 'baz', + tag: 'tag' + } + }, + expectedCtx: { + data: { + tag: 'tag' + } + } + }, + // TEST RELATED DATA STARTS HERE { name: 'should not execute action when ifAllMatch is invalid', actionCtx: { @@ -95,6 +125,39 @@ module.exports = { } }, // TEST RELATED DATA STARTS HERE + { + name: 'should not execute action when ifAnyMatch is invalid', + actionCtx: { + enable: true, + includeData: {}, + locations: { + tag: 'tag' + }, + ifAnyMatch: [ + { + foo: 'foo', + tag: 'untagged' + }, + { + foo: 'foo', + tag: 'notag' + } + ] + }, + dataCtx: { + data: { + foo: 'bar', + tag: 'tag' + } + }, + expectedCtx: { + data: { + foo: 'bar', + tag: 'tag' + } + } + }, + // TEST RELATED DATA STARTS HERE { name: 'should return only included keys (example 1)', dataCtx: { diff --git a/test/unit/dataPipelineTests.js b/test/unit/dataPipelineTests.js index 16877709..52b02d62 100644 --- a/test/unit/dataPipelineTests.js +++ b/test/unit/dataPipelineTests.js @@ -8,20 +8,24 @@ 'use strict'; +/* eslint-disable import/order */ + +require('./shared/restoreCache')(); +require('./shared/disableAjv'); // forwarder imports config with ajv + const chai = require('chai'); const chaiAsPromised = require('chai-as-promised'); const sinon = require('sinon'); -const dataFilter = require('../../src/lib/dataFilter.js'); -const dataPipeline = require('../../src/lib/dataPipeline.js'); -const dataTagging = require('../../src/lib/dataTagging.js'); -const forwarder = require('../../src/lib/forwarder.js'); -const EVENT_TYPES = require('../../src/lib/constants.js').EVENT_TYPES; +const dataFilter = require('../../src/lib/dataFilter'); +const dataPipeline = require('../../src/lib/dataPipeline'); +const dataTagging = require('../../src/lib/dataTagging'); +const EVENT_TYPES = require('../../src/lib/constants').EVENT_TYPES; +const forwarder = require('../../src/lib/forwarder'); chai.use(chaiAsPromised); const assert = chai.assert; - describe('Data Pipeline', () => { let forwardFlag; let forwardedData; @@ -51,6 +55,7 @@ describe('Data Pipeline', () => { handleActionsData.push({ dataCtx, actionCtx }); }); }); + afterEach(() => { sinon.restore(); }); @@ -169,7 +174,7 @@ describe('Data Pipeline', () => { }; return dataPipeline.process(dataCtx, options) .then(() => { - assert.deepEqual( + assert.deepStrictEqual( handleActionsData, [ { diff --git a/test/unit/dataTaggingTests.js b/test/unit/dataTaggingTests.js index a5dbb595..8778a3fd 100644 --- a/test/unit/dataTaggingTests.js +++ b/test/unit/dataTaggingTests.js @@ -8,19 +8,30 @@ 'use strict'; -const assert = require('assert'); +/* eslint-disable import/order */ + +require('./shared/restoreCache')(); + +const chai = require('chai'); +const chaiAsPromised = require('chai-as-promised'); const dataTagging = require('../../src/lib/dataTagging'); -const dataTaggingTestsData = require('./dataTaggingTestsData.js'); +const dataTaggingTestsData = require('./dataTaggingTestsData'); +const testUtil = require('./shared/util'); +chai.use(chaiAsPromised); +const assert = chai.assert; describe('Data Tagging', () => { describe('handleAction', () => { - const getCallableIt = testConf => (testConf.testOpts && testConf.testOpts.only ? it.only : it); - dataTaggingTestsData.handleAction.forEach((testConf) => { - getCallableIt(testConf)(testConf.name, () => { - dataTagging.handleAction(testConf.dataCtx, testConf.actionCtx, testConf.deviceCtx); + testUtil.getCallableIt(testConf)(testConf.name, () => { + const deviceCtx = testConf.deviceCtx || { + deviceVersion: '13.0.0.0', + provisioning: { ltm: { name: 'ltm', level: 'nominal' } } + }; + + dataTagging.handleAction(testConf.dataCtx, testConf.actionCtx, deviceCtx); assert.deepStrictEqual(testConf.dataCtx, testConf.expectedCtx); }); }); diff --git a/test/unit/dataTaggingTestsData.js b/test/unit/dataTaggingTestsData.js index db2962e5..f60e5b47 100644 --- a/test/unit/dataTaggingTestsData.js +++ b/test/unit/dataTaggingTestsData.js @@ -8,7 +8,7 @@ 'use strict'; -const EVENT_TYPES = require('../../src/lib/constants.js').EVENT_TYPES; +const EVENT_TYPES = require('../../src/lib/constants').EVENT_TYPES; /* eslint-disable no-useless-escape */ @@ -105,6 +105,35 @@ module.exports = { } }, // TEST RELATED DATA STARTS HERE + { + name: 'should execute action when ifAnyMatch is valid', + actionCtx: { + enable: true, + setTag: { + tag: 'tag' + }, + ifAnyMatch: [ + { + foo: 'bar' + }, + { + foo: 'baz' + } + ] + }, + dataCtx: { + data: { + foo: 'baz' + } + }, + expectedCtx: { + data: { + foo: 'baz', + tag: 'tag' + } + } + }, + // TEST RELATED DATA STARTS HERE { name: 'should not execute action when ifAllMatch is invalid', actionCtx: { @@ -128,6 +157,34 @@ module.exports = { } }, // TEST RELATED DATA STARTS HERE + { + name: 'should not execute action when ifAnyMatch is invalid', + actionCtx: { + enable: true, + setTag: { + tag: 'tag' + }, + ifAnyMatch: [ + { + foo: 'foo' + }, + { + foo: 'fooz' + } + ] + }, + dataCtx: { + data: { + foo: 'bar' + } + }, + expectedCtx: { + data: { + foo: 'bar' + } + } + }, + // TEST RELATED DATA STARTS HERE { name: 'should add tags to the default locations', dataCtx: { @@ -816,6 +873,51 @@ module.exports = { } }, // TEST RELATED DATA STARTS HERE + { + name: 'should set tenant and application tags to tmstats', + dataCtx: { + data: { + tmstats: { + virtualServerStat: { + '/tenant1/application1/vs1': {}, + '/tenant2/application1/vs2': {}, + vs3: {}, + vs4: {} + } + }, + telemetryEventCategory: EVENT_TYPES.SYSTEM_POLLER + }, + type: EVENT_TYPES.SYSTEM_POLLER + }, + actionCtx: { + enable: true, + setTag: { + tenantTag: '`T`', + applicationTag: '`A`' + } + }, + expectedCtx: { + data: { + tmstats: { + virtualServerStat: { + '/tenant1/application1/vs1': { + tenantTag: 'tenant1', + applicationTag: 'application1' + }, + '/tenant2/application1/vs2': { + tenantTag: 'tenant2', + applicationTag: 'application1' + }, + vs3: {}, + vs4: {} + } + }, + telemetryEventCategory: EVENT_TYPES.SYSTEM_POLLER + }, + type: EVENT_TYPES.SYSTEM_POLLER + } + }, + // TEST RELATED DATA STARTS HERE { name: 'should add tags to event to locations', dataCtx: { diff --git a/test/unit/dataUtilTests.js b/test/unit/dataUtilTests.js index 38139b1d..bbb8e982 100644 --- a/test/unit/dataUtilTests.js +++ b/test/unit/dataUtilTests.js @@ -8,72 +8,83 @@ 'use strict'; -const assert = require('assert'); +/* eslint-disable import/order */ -const dataUtil = require('../../src/lib/dataUtil.js'); -const dataUtilTestsData = require('./dataUtilTestsData.js'); +require('./shared/restoreCache')(); -describe('Data Util', () => { - const getCallableIt = testConf => (testConf.testOpts && testConf.testOpts.only ? it.only : it); +const chai = require('chai'); +const chaiAsPromised = require('chai-as-promised'); + +const dataUtil = require('../../src/lib/dataUtil'); +const dataUtilTestsData = require('./dataUtilTestsData'); +const testUtil = require('./shared/util'); + +chai.use(chaiAsPromised); +const assert = chai.assert; +describe('Data Util', () => { describe('getMatches', () => { dataUtilTestsData.getMatches.forEach((testConf) => { - getCallableIt(testConf)(testConf.name, () => { + testUtil.getCallableIt(testConf)(testConf.name, () => { const resultCtx = dataUtil.getMatches( - testConf.dataCtx, + testConf.data, testConf.propertyCtx, testConf.propertyRegexCtx ); - assert.deepEqual(resultCtx, testConf.expectedCtx); + assert.deepStrictEqual(resultCtx, testConf.expectedCtx); }); }); }); describe('getDeepMatches', () => { dataUtilTestsData.getDeepMatches.forEach((testConf) => { - getCallableIt(testConf)(testConf.name, () => { + testUtil.getCallableIt(testConf)(testConf.name, () => { const resultCtx = dataUtil.getDeepMatches( - testConf.dataCtx, + testConf.data, testConf.propertiesCtx ); - assert.deepEqual(resultCtx, testConf.expectedCtx); + assert.deepStrictEqual(resultCtx, testConf.expectedCtx); }); }); }); describe('checkConditions', () => { - dataUtilTestsData.checkConditions.forEach((testConf) => { - getCallableIt(testConf)(testConf.name, () => { - const resultCtx = dataUtil.checkConditions( - testConf.dataCtx, - testConf.conditionsCtx - ); - assert.strictEqual(resultCtx, testConf.expectedCtx); + ['', '_ifAnyMatch', '_ifAllMatch'].forEach((matchType) => { + describe(matchType, () => { + dataUtilTestsData[`checkConditions${matchType}`].forEach((testConf) => { + testUtil.getCallableIt(testConf)(testConf.name, () => { + const resultCtx = dataUtil.checkConditions( + testConf.dataCtx, + testConf.actionsCtx + ); + assert.strictEqual(resultCtx, testConf.expectedCtx); + }); + }); }); }); }); describe('searchAnyMatches', () => { dataUtilTestsData.searchAnyMatches.forEach((testConf) => { - getCallableIt(testConf)(testConf.name, () => { + testUtil.getCallableIt(testConf)(testConf.name, () => { const resultCtx = []; const callback = (key, item) => { resultCtx.push(key); return testConf.nestedKey ? item[testConf.nestedKey] : null; }; dataUtil.searchAnyMatches( - testConf.dataCtx, + testConf.data, testConf.propertiesCtx, callback ); - assert.deepEqual(resultCtx, testConf.expectedCtx); + assert.deepStrictEqual(resultCtx, testConf.expectedCtx); }); }); }); describe('removeStrictMatches', () => { dataUtilTestsData.removeStrictMatches.forEach((testConf) => { - getCallableIt(testConf)(testConf.name, () => { + testUtil.getCallableIt(testConf)(testConf.name, () => { const callback = (key, item, getNestedKey) => { if (getNestedKey) { return testConf.nestedKey ? item[testConf.nestedKey] : null; @@ -89,11 +100,11 @@ describe('Data Util', () => { expectedCtx = expectedCtx(); } const actualRetVal = dataUtil.removeStrictMatches( - testConf.dataCtx, + testConf.data, testConf.propertiesCtx, testConf.useCallback === false ? undefined : callback ); - assert.deepStrictEqual(testConf.dataCtx, expectedCtx); + assert.deepStrictEqual(testConf.data, expectedCtx); assert.strictEqual(actualRetVal, testConf.expectedRetVal); }); }); @@ -102,7 +113,7 @@ describe('Data Util', () => { [true, false].forEach((strictVal) => { describe(`preserveStrictMatches - strict=${strictVal}`, () => { dataUtilTestsData[`preserveStrictMatches_strict_${strictVal}`].forEach((testConf) => { - getCallableIt(testConf)(testConf.name, () => { + testUtil.getCallableIt(testConf)(testConf.name, () => { const callback = (key, item, getNestedKey) => { if (getNestedKey) { return testConf.nestedKey ? item[testConf.nestedKey] : null; @@ -118,12 +129,12 @@ describe('Data Util', () => { expectedCtx = expectedCtx(); } const actualRetVal = dataUtil.preserveStrictMatches( - testConf.dataCtx, + testConf.data, testConf.propertiesCtx, strictVal, testConf.useCallback === false ? undefined : callback ); - assert.deepStrictEqual(testConf.dataCtx, expectedCtx); + assert.deepStrictEqual(testConf.data, expectedCtx); assert.strictEqual(actualRetVal, testConf.expectedRetVal); }); }); diff --git a/test/unit/dataUtilTestsData.js b/test/unit/dataUtilTestsData.js index c21623d5..4699f27a 100644 --- a/test/unit/dataUtilTestsData.js +++ b/test/unit/dataUtilTestsData.js @@ -21,162 +21,778 @@ module.exports = { checkConditions: [ // TEST RELATED DATA STARTS HERE { - name: 'should determine data doesn\'t pass conditions (example 1)', + name: 'should only execute ifAllMatch, if ifAllMatch and ifAnyMatch happen to be in same action', dataCtx: { - virtualServers: { - virtual1: { - 'serverside.bitsIn': true + data: { + block1: { + b1_i1: { + value1: 'doesmatch' + } + } + } + }, + actionsCtx: { + ifAnyMatch: [{ + block1: { + '.*': { + value1: 'doesmatch' + } + } + }], + ifAllMatch: { + block1: { + value1: 'notpresent' + } + } + }, + expectedCtx: false + } + ], + checkConditions_ifAnyMatch: [ + // TEST RELATED DATA STARTS HERE + { + name: 'should pass when any ifAnyMatch item matches data', + dataCtx: { + data: { + virtualServers: { + '/test/gjd_ftp': { + 'serverside.bitsIn': true, + enabledState: 'enabled' + }, + virtual2: { + 'serverside.bitsIn': true + } }, - virtual2: { - 'serverside.bitsIn': true + system: { + hostname: 'bigip.example.com' } - }, - httpProfiles: { - httpProfile1: { - cookiePersistInserts: 1 + } + }, + actionsCtx: { + ifAnyMatch: [ + { + virtualServers: { + '/test/gjd_ftp': { + enabledState: 'enabled' + } + } + }, + { + virtualServers: { + '/test/gjd_ftp': { + enabledState: 'disabled' + } + } + } + ] + }, + expectedCtx: true + }, + // TEST RELATED DATA STARTS HERE + { + name: 'should pass when second ifAnyMatch item matches data', + dataCtx: { + data: { + virtualServers: { + virtual1: { + enabledState: 'enabled' + }, + virtual2: { + enabledState: 'enabled' + } + }, + system: { + hostname: 'bigip.example.com' } - }, - system: { - hostname: 'bigip.example.com' } }, - conditionsCtx: { - httpProfiles: { - '.*': { - cookiePersistInserts: 0 + actionsCtx: { + ifAnyMatch: [ + { + virtualServers: { + '.*': { + enabledState: 'disabled' + } + } + }, + { + virtualServers: { + '.*': { + enabledState: 'enabled' + } + } + } + ] + }, + expectedCtx: true + }, + // TEST RELATED DATA STARTS HERE + { + name: 'should pass when there are multiple matches', + dataCtx: { + data: { + virtualServers: { + one: { + enabledState: 'enabled', + otherKey: 'val1' + }, + two: { + enabledState: 'disabled', + otherKey: 'val1' + } + }, + system: { + hostname: 'bigip.example.com' + } + } + }, + actionsCtx: { + ifAnyMatch: [ + { + virtualServers: { + one: { + enabledState: 'enabled', + otherKey: 'val1' + } + } + }, + { + virtualServers: { + two: { + enabledState: 'disabled', + otherKey: 'val1' + } + } + } + ] + }, + expectedCtx: true + }, + // TEST RELATED DATA STARTS HERE + { + name: 'should pass when ifAnyMatch is empty array', + dataCtx: { + data: { + system: { + hostname: 'bigip.example.com' } - }, - system: { - hostname: 'something' } }, + actionsCtx: { + ifAnyMatch: [] + }, + expectedCtx: true + }, + // TEST RELATED DATA STARTS HERE + { + name: 'should pass when ifAnyMatch contains empty object', + dataCtx: { + data: { + system: { + hostname: 'bigip.example.com' + } + } + }, + actionsCtx: { + ifAnyMatch: [{}] + }, + expectedCtx: true + }, + // TEST RELATED DATA STARTS HERE + { + name: 'should pass when matching against array values', + dataCtx: { + data: { + aWideIps: { + '/Common/www.aone.tstest.com': { + wipType: 'A', + aliases: ['www.aone.com', 'www.cone.com'] + }, + '/Common/www.atwo.tstest.com': { + wipType: 'A', + aliases: ['www.atwo.com', 'www.ctwo.com'] + } + } + } + }, + actionsCtx: { + ifAnyMatch: [ + { + aWideIps: { + '/Common/www.aone.tstest.com': { + aliases: ['www.aone.com', 'www.cone.com'] + } + } + } + ] + }, + expectedCtx: true + }, + // TEST RELATED DATA STARTS HERE + { + name: 'should pass when matching against unsorted array values', + dataCtx: { + data: { + aWideIps: { + '/Common/www.aone.tstest.com': { + wipType: 'A', + aliases: ['www.aone.com', 'www.cone.com'] + }, + '/Common/www.atwo.tstest.com': { + wipType: 'A', + aliases: ['www.atwo.com', 'www.ctwo.com'] + } + } + } + }, + actionsCtx: { + ifAnyMatch: [ + { + aWideIps: { + '/Common/www.aone.tstest.com': { + aliases: ['www.cone.com', 'www.aone.com'] + } + } + } + ] + }, + expectedCtx: true + }, + // TEST RELATED DATA STARTS HERE + { + name: 'should pass when matching against array regexes', + dataCtx: { + data: { + aWideIps: { + '/Common/www.aone.tstest.com': { + wipType: 'A', + aliases: ['www.aone.com', 'www.cone.com'] + }, + '/Common/www.atwo.tstest.com': { + wipType: 'A', + aliases: ['www.atwo.com', 'www.ctwo.com'] + } + } + } + }, + actionsCtx: { + ifAnyMatch: [ + { + aWideIps: { + '/Common/www.atwo.tstest.com': { + aliases: ['atwo.com$', 'ctwo.com$'] + } + } + } + ] + }, + expectedCtx: true + }, + // TEST RELATED DATA STARTS HERE + { + name: 'should pass when any constant value matches data value', + dataCtx: { + data: { + virtualServers: { + '/test/gjd_ftp': { + 'serverside.bitsIn': true, + enabledState: 'enabled' + }, + virtual2: { + 'serverside.bitsIn': true + } + }, + system: { + hostname: 'bigip.example.com' + } + } + }, + actionsCtx: { + ifAnyMatch: [{ + virtualServers: { + '/test/gjd_ftp': { + enabledState: 'enabled' + } + } + }] + }, + expectedCtx: true + }, + // TEST RELATED DATA STARTS HERE + { + name: 'should pass with multiple keys in ifAnyMatch', + dataCtx: { + data: { + virtualServers: { + '/test/gjd_ftp': { + 'serverside.bitsIn': true, + enabledState: 'enabled' + }, + virtual2: { + 'serverside.bitsIn': true + } + }, + system: { + hostname: 'bigip.example.com' + } + } + }, + actionsCtx: { + ifAnyMatch: [{ + virtualServers: { + '/test/gjd_ftp': { + enabledState: 'enabled' + } + }, + system: { + hostname: 'bigip.example.com' + } + }] + }, + expectedCtx: true + }, + // TEST RELATED DATA STARTS HERE + { + name: 'should not pass if any sub-item in ifAnyMatch does not match', + dataCtx: { + data: { + virtualServers: { + '/test/gjd_ftp': { + 'serverside.bitsIn': true, + enabledState: 'enabled' + }, + virtual2: { + 'serverside.bitsIn': true + } + }, + system: { + hostname: 'bigip.example.com' + } + } + }, + actionsCtx: { + ifAnyMatch: [{ + virtualServers: { + '/test/gjd_ftp': { + enabledState: 'disabled' + } + }, + system: { + hostname: 'bigip.example.com' + } + }] + }, expectedCtx: false }, // TEST RELATED DATA STARTS HERE { - name: 'should determine data doesn\'t pass conditions (example 2)', + name: 'shouldn\'t pass when no match is made', dataCtx: { - virtualServers: { - virtual1: { - 'serverside.bitsIn': true + data: { + virtualServers: { + '/test/gjd_ftp': { + 'serverside.bitsIn': true, + enabledState: 'enabled' + }, + virtual2: { + 'serverside.bitsIn': true + } }, - virtual2: { - 'serverside.bitsIn': true + system: { + hostname: 'bigip.example.com' } - }, - httpProfiles: { - httpProfile1: { - cookiePersistInserts: 1 + } + }, + actionsCtx: { + ifAnyMatch: [{ + virtualServers: { + '/test/gjd_ftp': { + enabledState: 'disabled' + } + }, + system: { + hostname: 'bigip1.example.com' + } + }] + }, + expectedCtx: false + }, + // TEST RELATED DATA STARTS HERE + { + name: 'shouldn\'t pass when no data is present', + dataCtx: { + data: {} + }, + actionsCtx: { + ifAnyMatch: [{ + virtualServers: { + '/test/gjd_ftp': { + enabledState: 'disabled' + } + } + }] + }, + expectedCtx: false + }, + // TEST RELATED DATA STARTS HERE + { + name: 'shouldn\'t pass when array values do not match', + dataCtx: { + data: { + aWideIps: { + '/Common/www.aone.tstest.com': { + wipType: 'A', + aliases: ['www.aone.com', 'www.cone.com'] + }, + '/Common/www.atwo.tstest.com': { + wipType: 'A', + aliases: ['www.atwo.com', 'www.ctwo.com'] + } } - }, - system: { - hostname: 'bigip.example.com' } }, - conditionsCtx: { - clientSSL: { - '.*': { - cookiePersistInserts: 0 + actionsCtx: { + ifAnyMatch: [{ + aWideIps: { + '.*': { + aliases: 'www.none.com' + } + } + }] + }, + expectedCtx: false + }, + // TEST RELATED DATA STARTS HERE + { + name: 'should pass when a single regex matches', + dataCtx: { + data: { + virtualServers: { + '/test/gjd_ftp': { + 'serverside.bitsIn': true, + enabledState: 'enabled' + }, + virtual2: { + 'serverside.bitsIn': true + } + }, + system: { + hostname: 'bigip.example.com' } } }, + actionsCtx: { + ifAnyMatch: [{ + system: { + hostname: '^bigip.exa' + } + }] + }, + expectedCtx: true + }, + // TEST RELATED DATA STARTS HERE + { + name: 'shouldn\'t pass when the regex does not match', + dataCtx: { + data: { + virtualServers: { + '/test/gjd_ftp': { + 'serverside.bitsIn': true, + enabledState: 'enabled' + }, + virtual2: { + 'serverside.bitsIn': true + } + }, + system: { + hostname: 'bigip.example.com' + } + } + }, + actionsCtx: { + ifAnyMatch: [{ + system: { + hostname: '^example' + } + }] + }, expectedCtx: false }, // TEST RELATED DATA STARTS HERE { - name: 'should determine that data passes conditions', + name: 'shouldn\'t pass when value does not match', dataCtx: { - httpProfiles: { - http1: { - getReqs: 10 + data: { + system: { + hostname: 'bigip.example.com', + version: '14.1.0' + } + } + }, + actionsCtx: { + ifAnyMatch: [{ + system: { + version: 'shouldnotmatch' + } + }] + }, + expectedCtx: false + }, + // TEST RELATED DATA STARTS HERE + { + name: 'shouldn\'t reject on bad regex', + dataCtx: { + data: { + system: { + hostname: 'bigip.example.com', + version: '14.1.0' + } + } + }, + actionsCtx: { + ifAnyMatch: [{ + system: { + version: '*' + } + }] + }, + expectedCtx: false + }, + // TEST RELATED DATA STARTS HERE + { + name: 'shouldn\'t pass when literal \'object\' only matches Javascript object', + dataCtx: { + data: { + virtualServers: { + '/test/gjd_ftp': { + 'serverside.bitsIn': true, + enabledState: 'enabled' + }, + virtual2: { + 'serverside.bitsIn': true + } }, - http2: { - getReqs: 10 + system: { + hostname: 'bigip.example.com' } - }, - system: { - hostname: 'bigip.example.com', - version: '14.0.0.4' } }, - conditionsCtx: { - 'ht*': { - '.*': { - getReqs: 10 + actionsCtx: { + ifAnyMatch: [{ + virtualServers: 'object' + }] + }, + expectedCtx: false + }, + // TEST RELATED DATA STARTS HERE + { + name: 'should pass if \'object\' is used as matching value', + dataCtx: { + data: { + virtualServers: { + objectName: { + 'serverside.bitsIn': true + }, + virtual2: { + 'serverside.bitsIn': true + } + }, + system: { + hostname: 'bigip.example.com' } - }, - system: { - version: '14*' } }, + actionsCtx: { + ifAnyMatch: [{ + virtualServers: { + 'object.*': { + 'serverside.bitsIn': true + } + } + }] + }, expectedCtx: true + } + ], + checkConditions_ifAllMatch: [ + // TEST RELATED DATA STARTS HERE + { + name: 'should determine data doesn\'t pass conditions (example 1)', + dataCtx: { + data: { + virtualServers: { + virtual1: { + 'serverside.bitsIn': true + }, + virtual2: { + 'serverside.bitsIn': true + } + }, + httpProfiles: { + httpProfile1: { + cookiePersistInserts: 1 + } + }, + system: { + hostname: 'bigip.example.com' + } + } + }, + actionsCtx: { + ifAllMatch: { + httpProfiles: { + '.*': { + cookiePersistInserts: 0 + } + }, + system: { + hostname: 'something' + } + } + }, + expectedCtx: false }, // TEST RELATED DATA STARTS HERE { - name: 'should process arrays correctly (example 1)', + name: 'should determine data doesn\'t pass conditions (example 2)', dataCtx: { - httpProfiles: [ - { - http1: { - getReqs: 10 + data: { + virtualServers: { + virtual1: { + 'serverside.bitsIn': true + }, + virtual2: { + 'serverside.bitsIn': true } }, - { - http2: { - getReqs: 10 + httpProfiles: { + httpProfile1: { + cookiePersistInserts: 1 } + }, + system: { + hostname: 'bigip.example.com' } - ], - system: { - hostname: 'bigip.example.com', - version: '14.0.0.4' } }, - conditionsCtx: { - 'ht*': { - '.*': { + actionsCtx: { + ifAllMatch: { + clientSSL: { '.*': { - getReqs: 10 + cookiePersistInserts: 0 } } - }, - system: { - version: '14*' } }, - expectedCtx: true + expectedCtx: false }, // TEST RELATED DATA STARTS HERE { - name: 'should process arrays correctly (example 2)', + name: 'should determine that data passes conditions', dataCtx: { - httpProfiles: [ - { + data: { + httpProfiles: { http1: { - getReqs: 20 + getReqs: 10 + }, + http2: { + getReqs: 10 } }, - { - http2: { + system: { + hostname: 'bigip.example.com', + version: '14.0.0.4' + } + } + }, + actionsCtx: { + ifAllMatch: { + 'ht*': { + '.*': { getReqs: 10 } + }, + system: { + version: '14*' } - ], - system: { - hostname: 'bigip.example.com', - version: '14.0.0.4' } }, - conditionsCtx: { - 'ht*': { - 0: { + expectedCtx: true + }, + // TEST RELATED DATA STARTS HERE + { + name: 'should process arrays correctly (example 1)', + dataCtx: { + data: { + httpProfiles: [ + { + http1: { + getReqs: 10 + } + }, + { + http2: { + getReqs: 10 + } + } + ], + system: { + hostname: 'bigip.example.com', + version: '14.0.0.4' + } + } + }, + actionsCtx: { + ifAllMatch: { + 'ht*': { '.*': { - getReqs: 20 + '.*': { + getReqs: 10 + } } }, - 1: { - http2: { - getReqs: 10 + system: { + version: '14*' + } + } + }, + expectedCtx: true + }, + // TEST RELATED DATA STARTS HERE + { + name: 'should process arrays correctly (example 2)', + dataCtx: { + data: { + httpProfiles: [ + { + http1: { + getReqs: 20 + } + }, + { + http2: { + getReqs: 10 + } + } + ], + system: { + hostname: 'bigip.example.com', + version: '14.0.0.4' + } + } + }, + actionsCtx: { + ifAllMatch: { + 'ht*': { + 0: { + '.*': { + getReqs: 20 + } + }, + 1: { + http2: { + getReqs: 10 + } } } } @@ -187,15 +803,19 @@ module.exports = { { name: 'should work with events (example 1)', dataCtx: { - event_source: 'request_logging', - event_timestamp: '2019-01-01:01:01.000Z', - hostname: 'hostname', - virtual_name: 'app_vs', - telemetryEventCategory: 'defaultEvent' + data: { + event_source: 'request_logging', + event_timestamp: '2019-01-01:01:01.000Z', + hostname: 'hostname', + virtual_name: 'app_vs', + telemetryEventCategory: 'defaultEvent' + } }, - conditionsCtx: { - telemetryEventCategory: 'defaultEvent', - tenant: 'app' + actionsCtx: { + ifAllMatch: { + telemetryEventCategory: 'defaultEvent', + tenant: 'app' + } }, expectedCtx: false }, @@ -203,24 +823,78 @@ module.exports = { { name: 'should work with events (example 2)', dataCtx: { - event_source: 'request_logging', - event_timestamp: '2019-01-01:01:01.000Z', - hostname: 'hostname', - virtual_name: 'app_vs', - telemetryEventCategory: 'defaultEvent' + data: { + event_source: 'request_logging', + event_timestamp: '2019-01-01:01:01.000Z', + hostname: 'hostname', + virtual_name: 'app_vs', + telemetryEventCategory: 'defaultEvent' + } }, - conditionsCtx: { - telemetryEventCategory: 'defaultEvent', - virtual_name: 'app_vs' + actionsCtx: { + ifAllMatch: { + telemetryEventCategory: 'defaultEvent', + virtual_name: 'app_vs' + } }, expectedCtx: true + }, + // TEST RELATED DATA STARTS HERE + { + name: 'shouldn\'t reject on bad regex', + dataCtx: { + data: { + system: { + hostname: 'bigip.example.com', + version: '14.1.0' + } + } + }, + actionsCtx: { + ifAllMatch: { + system: { + version: '*' + } + } + }, + expectedCtx: false + }, + // TEST RELATED DATA STARTS HERE + { + name: 'should return successful when negation regexes are used', + dataCtx: { + data: { + Entity: 'TcpStat' + } + }, + actionsCtx: { + ifAllMatch: { + Entity: '^(?!OffBoxAll|^VS).*' + } + }, + expectedCtx: true + }, + // TEST RELATED DATA STARTS HERE + { + name: 'should not match when negation regexes are used', + dataCtx: { + data: { + Entity: 'OffBoxAll' + } + }, + actionsCtx: { + ifAllMatch: { + Entity: '^(?!OffBoxAll|^VS).*' + } + }, + expectedCtx: false } ], getMatches: [ // TEST RELATED DATA STARTS HERE { name: 'should return what matches the property when it is a literal string', - dataCtx: { + data: { system: {}, httpProfiles: {}, virtualServers: {} @@ -231,7 +905,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should return what matches the property when a regex is used (example 1)', - dataCtx: { + data: { httpProfiles: {}, virtualServers: {}, httpProfiles2: {} @@ -242,7 +916,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should return what matches the property when a regex is used (example 2)', - dataCtx: { + data: { '/dev': {}, '/dev/shm': {}, '/var': {}, @@ -263,7 +937,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should return no matches', - dataCtx: { + data: { virtualServers: {}, httpProfiles: {} }, @@ -273,42 +947,42 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should work with array when index (integer) exists', - dataCtx: ['vs1'], + data: ['vs1'], propertyCtx: 0, expectedCtx: [0] }, // TEST RELATED DATA STARTS HERE { name: 'should work with array when index (string) exists', - dataCtx: ['vs1'], + data: ['vs1'], propertyCtx: '0', expectedCtx: ['0'] }, // TEST RELATED DATA STARTS HERE { name: 'should work with array when index (integer) not exists', - dataCtx: [], + data: [], propertyCtx: 0, expectedCtx: [] }, // TEST RELATED DATA STARTS HERE { name: 'should work with array when index (string) not exists (example 1)', - dataCtx: [], + data: [], propertyCtx: '0', expectedCtx: [] }, // TEST RELATED DATA STARTS HERE { name: 'should work with array when index (string) not exists (example 2)', - dataCtx: ['vs1'], + data: ['vs1'], propertyCtx: 'noResult', expectedCtx: [] }, // TEST RELATED DATA STARTS HERE { name: 'should check property param against regex patterers from the data keys (example 1)', - dataCtx: { + data: { irtualServer: true, ttpProfile: true }, @@ -319,7 +993,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should check property param against regex patterns from the data keys (example 2)', - dataCtx: { + data: { irtualServer: true, Server: true }, @@ -330,7 +1004,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should check property param against regex patterns from the data keys (example 3)', - dataCtx: { + data: { irtualServer: true, server: true }, @@ -341,7 +1015,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should check property param against regex patterns from the data keys (example 4)', - dataCtx: { + data: { ttpProfile: true }, propertyCtx: 'virtualServers', @@ -353,7 +1027,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should match specified properties', - dataCtx: { + data: { virtualServers: { virtual1: {}, virtual2: {} @@ -438,7 +1112,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should not match bad properties', - dataCtx: { + data: { system: {}, virtualServers: { virtual1: {} @@ -456,7 +1130,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should be able to access nested data by key', - dataCtx: { + data: { system: { nestedKey: 'nestedData', nestedData: { @@ -477,7 +1151,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should match specified properties (example 1)', - dataCtx: { + data: { system: { hostname: 'hostname' } @@ -494,7 +1168,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should match specified properties (example 2)', - dataCtx: { + data: { system: { hostname: 'hostname' } @@ -507,7 +1181,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should match specified properties (example 3)', - dataCtx: { + data: { system: { hostname: 'hostname' } @@ -522,7 +1196,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should match specified properties (example 4)', - dataCtx: { + data: { system: { hostname: 'hostname' } @@ -537,7 +1211,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should match specified properties (example 5)', - dataCtx: { + data: { system: { hostname: 'hostname' } @@ -552,7 +1226,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should not match specified properties', - dataCtx: { + data: { system: { hostname: 'hostname' } @@ -567,7 +1241,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should be able work with arrays (example 1)', - dataCtx: { + data: { virtualServers: [ { vs1: { @@ -590,12 +1264,12 @@ module.exports = { } } }, - expectedCtx: ['virtualServers', 0, 'vs1', 'ip'] + expectedCtx: ['virtualServers', '0', 'vs1', 'ip'] }, // TEST RELATED DATA STARTS HERE { name: 'should be able work with arrays (example 2)', - dataCtx: { + data: { virtualServers: [ { vs1: { @@ -623,7 +1297,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should be able work with arrays (example 3)', - dataCtx: { + data: { virtualServers: [ { vs1: { @@ -651,7 +1325,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should be able work with regex', - dataCtx: { + data: { virtualServers: [ { vs1: { @@ -674,13 +1348,40 @@ module.exports = { } }, expectedCtx: ['virtualServers', 'system', 'hostname'] + }, + // TEST RELATED DATA STARTS HERE + { + name: 'should be able to accept array of property contexts', + data: { + system: { + hostname: 'hostname', + nestedKey1: 'value1' + }, + otherSystem: { + keyOne: 'one', + keyTwo: 'two' + } + }, + propertiesCtx: [ + { + system: { + hostname: true + } + }, + { + otherSystem: { + keyTwo: 'two' + } + } + ], + expectedCtx: ['system', 'hostname', 'otherSystem', 'keyTwo'] } ], removeStrictMatches: [ // TEST RELATED DATA STARTS HERE { name: 'should be able to access nested data by key (example 1)', - dataCtx: { + data: { system: { nestedKey: 'nested', nested: { @@ -706,7 +1407,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should be able to access nested data by key (example 2)', - dataCtx: { + data: { system: { nestedKey: 'nested', nested: { @@ -724,7 +1425,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should keep nested data if not empty (example 1)', - dataCtx: { + data: { system: { nestedKey: 'nested', nested: { @@ -753,7 +1454,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should keep nested data if not empty (example 2)', - dataCtx: { + data: { system: { hostname: 'hostname', version: 'version' @@ -775,7 +1476,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should remove nested key only', - dataCtx: { + data: { system: { hostname: 'hostname' } @@ -793,7 +1494,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should remove parent object despite on nested data', - dataCtx: { + data: { system: { hostname: 'hostname' } @@ -807,7 +1508,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should work with regex (example 1)', - dataCtx: { + data: { system: { hostname: 'hostname' } @@ -821,7 +1522,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should work with regex (example 2)', - dataCtx: { + data: { system: { hostname: 'hostname' }, @@ -849,7 +1550,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should remove nothing if no matches', - dataCtx: { + data: { system: { hostname: 'hostname' }, @@ -879,7 +1580,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should work with array', - dataCtx: { + data: { virtualServers: [ { ip: 'x.x.x.x', @@ -913,7 +1614,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should work with array - preserve access by index to elements', - dataCtx: { + data: { virtualServers: [ { ip: 'x.x.x.x' @@ -957,7 +1658,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should work without callback', - dataCtx: { + data: { system: { hostname: 'hostname' }, @@ -988,7 +1689,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should be able to access nested data by key', - dataCtx: { + data: { system: { nestedKey: 'nested', nested: { @@ -1015,7 +1716,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should keep nested data', - dataCtx: { + data: { system: { hostname: 'hostname' }, @@ -1036,7 +1737,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should work with regex (example 1)', - dataCtx: { + data: { system: { hostname: 'hostname' }, @@ -1073,7 +1774,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should work with regex (example 2)', - dataCtx: { + data: { system: { hostname: 'hostname' }, @@ -1099,7 +1800,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should keep nested data if not empty (example 1)', - dataCtx: { + data: { system: { nestedKey: 'nested', nested: { @@ -1130,7 +1831,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should keep nested data if not empty (example 2)', - dataCtx: { + data: { system: { hostname: 'hostname', version: 'version' @@ -1155,7 +1856,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should keep nested data if not empty (example 3)', - dataCtx: { + data: { system: { hostname: 'hostname', version: 'version' @@ -1182,7 +1883,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should remove parent object despite on nested data', - dataCtx: { + data: { system: { hostname: 'hostname' }, @@ -1204,7 +1905,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should remove everything if no matches', - dataCtx: { + data: { system: { hostname: 'hostname' }, @@ -1224,7 +1925,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should keep only matched keys', - dataCtx: { + data: { system: { hostname: 'hostname' }, @@ -1248,7 +1949,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should work with array', - dataCtx: { + data: { virtualServers: [ { ip: 'x.x.x.x', @@ -1286,7 +1987,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should work with array - preserve access by index to elements', - dataCtx: { + data: { virtualServers: [ { ip: 'x.x.x.x' @@ -1330,7 +2031,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should work without callback', - dataCtx: { + data: { system: { hostname: 'hostname' }, @@ -1363,7 +2064,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should be able to access nested data by key', - dataCtx: { + data: { system: { nestedKey: 'nested', nested: { @@ -1390,7 +2091,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should keep nested data', - dataCtx: { + data: { system: { hostname: 'hostname' }, @@ -1411,7 +2112,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should work with regex (example 1)', - dataCtx: { + data: { system: { hostname: 'hostname' }, @@ -1448,7 +2149,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should work with regex (example 2)', - dataCtx: { + data: { system: { hostname: 'hostname' }, @@ -1474,7 +2175,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should keep nested data if not empty (example 1)', - dataCtx: { + data: { system: { nestedKey: 'nested', nested: { @@ -1505,7 +2206,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should keep nested data if not empty (example 2)', - dataCtx: { + data: { system: { hostname: 'hostname', version: 'version' @@ -1530,7 +2231,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should remove parent object despite on nested data', - dataCtx: { + data: { system: { hostname: 'hostname' }, @@ -1552,7 +2253,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should remove everything if no matches (example 1)', - dataCtx: { + data: { system: { hostname: 'hostname' }, @@ -1572,7 +2273,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should remove everything if no matches (example 2)', - dataCtx: { + data: { system: { hostname: 'hostname' }, @@ -1596,7 +2297,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should keep only matched keys', - dataCtx: { + data: { system: { hostname: 'hostname' }, @@ -1618,7 +2319,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should work with array', - dataCtx: { + data: { virtualServers: [ { ip: 'x.x.x.x', @@ -1660,7 +2361,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should work with array - preserve access by index to elements', - dataCtx: { + data: { virtualServers: [ { ip: 'x.x.x.x' @@ -1704,7 +2405,7 @@ module.exports = { // TEST RELATED DATA STARTS HERE { name: 'should work without callback', - dataCtx: { + data: { system: { hostname: 'hostname' }, diff --git a/test/unit/datetimeUtilTests.js b/test/unit/datetimeUtilTests.js index e4f87e03..93a4390f 100644 --- a/test/unit/datetimeUtilTests.js +++ b/test/unit/datetimeUtilTests.js @@ -8,11 +8,19 @@ 'use strict'; -const assert = require('assert'); -const fileLogger = require('../winstonLogger.js').logger; -const constants = require('../../src/lib/constants.js'); -const datetimeUtil = require('../../src/lib/datetimeUtil.js'); +/* eslint-disable import/order */ +require('./shared/restoreCache')(); + +const chai = require('chai'); +const chaiAsPromised = require('chai-as-promised'); + +const constants = require('../../src/lib/constants'); +const datetimeUtil = require('../../src/lib/datetimeUtil'); +const fileLogger = require('../winstonLogger').logger; + +chai.use(chaiAsPromised); +const assert = chai.assert; describe('Date and Time utils', () => { describe('getLastDayOfMonth', () => { @@ -342,10 +350,11 @@ describe('Date and Time utils', () => { || (start > end && (start <= time || time <= end)); } - function standardCheck(first, second) { - // check that second start not earlier first ends - if (second <= first) { - const errMsg = `Date ${second.toISOString()} should be > ${first.toISOString()}`; + function standardCheck(first, second, strict) { + // strict is true then secondDate should be strictly greater than firstDate + // strict is false then secondDate should be greater or equal to firstDate + if (!(strict ? second > first : second >= first)) { + const errMsg = `Date ${second.toISOString()} should be ${strict ? '>' : '>='} ${first.toISOString()}`; throw new Error(errMsg); } } @@ -405,14 +414,15 @@ describe('Date and Time utils', () => { const secondEnd = getEndDate(second, schedule); const secondStart = getStartDate(second, schedule); + // first date should be in boundaries (inclusively) standardCheck(firstStart, first); standardCheck(first, firstEnd); - // standardCheck(firstEnd, secondStart); + // second date should be in boundaries (inclusively) standardCheck(secondStart, second); standardCheck(second, secondEnd); const startDistance = (secondStart - firstStart) / _MS_PER_DAY; - const endDistance = (secondStart - firstStart) / _MS_PER_DAY; + const endDistance = (secondEnd - firstEnd) / _MS_PER_DAY; if (!(startDistance > 0.8 && startDistance < 1.2 && endDistance > 0.8 && endDistance < 1.2)) { const errMsg = `Dates ${first.toISOString()} and ${second.toISOString()} are not in daily schedule`; throw new Error(errMsg); @@ -465,20 +475,18 @@ describe('Date and Time utils', () => { + ` (start date = ${startDate || 'random date'})`, () => { startDate = startDate ? new Date(startDate) : startDate; const validator = scheduleValidators[testSet.schedule.frequency]; - const dates = [datetimeUtil.getNextFireDate(testSet.schedule, startDate, false, true)]; - let firstDate = dates[0]; + let firstDate = datetimeUtil.getNextFireDate(testSet.schedule, startDate, false, true); - for (let j = 0; j < 100; j += 1) { + for (let j = 0; j < 1000; j += 1) { const secondDate = datetimeUtil.getNextFireDate(testSet.schedule, firstDate, false, true); - dates.push(secondDate); try { - standardCheck(firstDate, secondDate); + // secondDate should be strictly greater than firstDate + standardCheck(firstDate, secondDate, true); checkTimeRanges([firstDate, secondDate], testSet.schedule); // frequency specific validation(s) validator(testSet.schedule, new Date(firstDate), new Date(secondDate)); } catch (err) { - // eslint-disable-next-line no-console - fileLogger.debug('List of dates', dates); + fileLogger.debug(`Failed on ${j} iteration: ${err}`); throw err; } firstDate = secondDate; diff --git a/test/unit/declarationTests.js b/test/unit/declarationTests.js index 91b81df9..424b5887 100644 --- a/test/unit/declarationTests.js +++ b/test/unit/declarationTests.js @@ -8,20 +8,23 @@ 'use strict'; +/* eslint-disable import/order */ + +require('./shared/restoreCache')(); + const chai = require('chai'); const chaiAsPromised = require('chai-as-promised'); const fs = require('fs'); const sinon = require('sinon'); +const config = require('../../src/lib/config'); +const constants = require('../../src/lib/constants'); +const deviceUtil = require('../../src/lib/deviceUtil'); +const util = require('../../src/lib/util'); + chai.use(chaiAsPromised); const assert = chai.assert; -const config = require('../../src/lib/config.js'); -const constants = require('../../src/lib/constants.js'); -const deviceUtil = require('../../src/lib/deviceUtil.js'); -const util = require('../../src/lib/util.js'); - - describe('Declarations', () => { let encryptSecretStub; let getDeviceTypeStub; @@ -29,11 +32,11 @@ describe('Declarations', () => { beforeEach(() => { encryptSecretStub = sinon.stub(deviceUtil, 'encryptSecret'); - encryptSecretStub.callsFake(() => Promise.resolve('foo')); + encryptSecretStub.resolves('$M$foo'); getDeviceTypeStub = sinon.stub(deviceUtil, 'getDeviceType'); - getDeviceTypeStub.callsFake(() => Promise.resolve(constants.BIG_IP_DEVICE_TYPE)); + getDeviceTypeStub.resolves(constants.DEVICE_TYPE.BIG_IP); networkCheckStub = sinon.stub(util, 'networkCheck'); - networkCheckStub.callsFake(() => Promise.resolve()); + networkCheckStub.resolves(); }); afterEach(() => { sinon.restore(); @@ -133,7 +136,7 @@ describe('Declarations', () => { assert.strictEqual(proxy.allowSelfSignedCert, true); assert.strictEqual(proxy.enableHostConnectivityCheck, false); assert.strictEqual(proxy.username, 'username'); - assert.strictEqual(proxy.passphrase.cipherText, 'foo'); + assert.strictEqual(proxy.passphrase.cipherText, '$M$foo'); }); }); @@ -568,7 +571,7 @@ describe('Declarations', () => { describe('f5secret', () => { it('should fail cipherText with wrong device type', () => { - deviceUtil.getDeviceType = () => Promise.resolve(constants.CONTAINER_DEVICE_TYPE); + getDeviceTypeStub.resolves(constants.DEVICE_TYPE.CONTAINER); const data = { class: 'Telemetry', My_Poller: { @@ -583,7 +586,7 @@ describe('Declarations', () => { }); it('should not re-encrypt', () => { - const cipher = '$M$foo'; + const cipher = '$M$fo02'; const data = { class: 'Telemetry', My_Poller: { @@ -601,7 +604,7 @@ describe('Declarations', () => { }); it('should base64 decode cipherText', () => { - deviceUtil.encryptSecret = secret => Promise.resolve(secret); + encryptSecretStub.resolvesArg(0); const cipher = 'ZjVzZWNyZXQ='; // f5secret const data = { class: 'Telemetry', @@ -618,6 +621,33 @@ describe('Declarations', () => { assert.strictEqual(data.My_Poller.passphrase.cipherText, 'f5secret'); }); }); + + it('should fail when cipherText protected by SecureVault but is not encrypted', () => { + const data = { + class: 'Telemetry', + My_Poller: { + class: 'Telemetry_System_Poller', + passphrase: { + cipherText: 'mycipher', + protected: 'SecureVault' + } + } + }; + return assert.isRejected(config.validate(data), /should be encrypted by BIG-IP when.*protected.*SecureVault/); + }); + + it('should fail when cipherText or environmentVar missed', () => { + const data = { + class: 'Telemetry', + My_Poller: { + class: 'Telemetry_System_Poller', + passphrase: { + protected: 'SecureVault' + } + } + }; + return assert.isRejected(config.validate(data), /missing cipherText or environmentVar/); + }); }); describe('f5expand', () => { @@ -755,8 +785,8 @@ describe('Declarations', () => { }); it('should expand pointer (object)', () => { - const resolvedSecret = 'bar'; - deviceUtil.encryptSecret = () => Promise.resolve(resolvedSecret); + const resolvedSecret = '$M$bar'; + encryptSecretStub.resolves(resolvedSecret); const expectedValue = { class: 'Secret', @@ -797,12 +827,12 @@ describe('Declarations', () => { .then((validated) => { assert.deepEqual(validated.My_Consumer.path, expectedValue); assert.deepEqual(validated.My_Consumer.headers[0].value, expectedValue); - return config.validate(validated); + return assert.isFulfilled(config.validate(validated)); }); }); it('should fail pointer (object) with additional chars', () => { - deviceUtil.encryptSecret = secret => Promise.resolve(secret); + encryptSecretStub.resolvesArg(0); const data = { class: 'Telemetry', @@ -864,7 +894,7 @@ describe('Declarations', () => { it('should fail host network check', () => { const errMsg = 'failed network check'; - networkCheckStub.callsFake(() => Promise.reject(new Error(errMsg))); + networkCheckStub.rejects(new Error(errMsg)); const data = { class: 'Telemetry', @@ -878,10 +908,227 @@ describe('Declarations', () => { return assert.isRejected(config.validate(data), new RegExp(errMsg)); }); }); + + describe('declarationClassProp', () => { + it('should resolve full item path based on class and prop name', () => { + const data = { + class: 'Telemetry', + My_Endpoints: { + class: 'Telemetry_Endpoints', + items: { + testA: { + name: 'a', + path: '/1/a' + }, + a: { + name: 'a', + path: 'something/a' + } + } + }, + My_System: { + class: 'Telemetry_System', + systemPoller: { + interval: 100, + endpointList: [ + 'My_Endpoints/a' + ] + } + } + }; + return assert.isFulfilled(config.validate(data)); + }); + + it('should trim leading and trailing backslashes', () => { + const data = { + class: 'Telemetry', + My_Endpoints: { + class: 'Telemetry_Endpoints', + items: { + testA: { + name: 'a', + path: '/1/a' + }, + a: { + name: 'a', + path: 'something/a' + } + } + }, + My_System: { + class: 'Telemetry_System', + systemPoller: { + interval: 100, + endpointList: [ + '/My_Endpoints/a/' + ] + } + } + }; + return assert.isFulfilled(config.validate(data)); + }); + + it('should return error when full item path cannot be resolved', () => { + const data = { + class: 'Telemetry', + My_Endpoints: { + class: 'Telemetry_Endpoints', + items: { + testA: { + name: 'a', + path: '/1/a' + } + } + }, + My_System: { + class: 'Telemetry_System', + systemPoller: { + interval: 100, + endpointList: [ + 'My_Endpoints/testA', + 'My_Endpoints/i_dont_exist' + ] + } + } + }; + const errMsg = 'Unable to find \\"My_Endpoints/items/i_dont_exist\\"'; + return assert.isRejected(config.validate(data), errMsg); + }); + + it('should return error when value is not valid declarationClassProp', () => { + const data = { + class: 'Telemetry', + My_Endpoints: { + class: 'Telemetry_Endpoints', + items: { + testA: { + name: 'a', + path: '/1/a' + } + } + }, + My_System: { + class: 'Telemetry_System', + systemPoller: { + interval: 100, + endpointList: [ + 'Non_Existing' + ] + } + } + }; + const errMsg = '\\"Non_Existing\\" does not follow format \\"ObjectName/key1\\"'; + return assert.isRejected(config.validate(data), errMsg); + }); + + it('should return error when object referenced is not correct class', () => { + const data = { + class: 'Telemetry', + My_Endpoints: { + class: 'Telemetry_System' + }, + My_System: { + class: 'Telemetry_System', + systemPoller: { + interval: 100, + endpointList: [ + 'My_Endpoints/something' + ] + } + } + }; + const errMsg = '\\"My_Endpoints\\" must be of object type and class \\"Telemetry_Endpoints\\"'; + return assert.isRejected(config.validate(data), errMsg); + }); + + it('should fail to resolve when instance property matches class property', () => { + // TODO: this behavior should be fixed in future releases + const declaration = { + class: 'Telemetry', + My_Endpoints: { + class: 'Telemetry_Endpoints', + items: { + items: { + name: 'a', + path: '/1/a' + } + } + }, + My_System: { + class: 'Telemetry_System', + systemPoller: { + interval: 100, + endpointList: [ + 'My_Endpoints/items' + ] + } + } + }; + return assert.isFulfilled(config.validate(declaration) + .then((data) => { + assert.notStrictEqual(data.My_System.systemPoller.endpointList[0], 'My_Endpoints/items/items'); + })); + }); + }); + + describe('declarationClass', () => { + it('should pass when object exists', () => { + const data = { + class: 'Telemetry', + My_Poller: { + class: 'Telemetry_System_Poller' + }, + My_System: { + class: 'Telemetry_System', + systemPoller: 'My_Poller' + } + }; + return assert.isFulfilled(config.validate(data)); + }); + + it('should return error when object referenced is not correct class', () => { + const data = { + class: 'Telemetry', + My_Poller: { + class: 'Telemetry_System' + }, + My_System: { + class: 'Telemetry_System', + systemPoller: 'My_Poller' + } + }; + const errMsg = 'declaration with name \\"My_Poller\\" and class \\"Telemetry_System_Poller\\" doesn\'t exist'; + return assert.isRejected(config.validate(data), errMsg); + }); + + it('should return error when object referenced is not "object" type', () => { + const data = { + class: 'Telemetry', + My_System: { + class: 'Telemetry_System', + systemPoller: 'class' + } + }; + const errMsg = 'declaration with name \\"class\\" and class \\"Telemetry_System_Poller\\" doesn\'t exist'; + return assert.isRejected(config.validate(data), errMsg); + }); + + it('should return error when object not exist', () => { + const data = { + class: 'Telemetry', + My_System: { + class: 'Telemetry_System', + systemPoller: 'Non_Existing_Poller' + } + }; + const errMsg = 'declaration with name \\"Non_Existing_Poller\\" and class \\"Telemetry_System_Poller\\" doesn\'t exist'; + return assert.isRejected(config.validate(data), errMsg); + }); + }); }); describe('Telemetry_System_Poller', () => { - it('should pass miminal declaration', () => { + it('should pass minimal declaration', () => { const data = { class: 'Telemetry', My_Poller: { @@ -894,7 +1141,7 @@ describe('Declarations', () => { assert.notStrictEqual(poller, undefined); assert.strictEqual(poller.class, 'Telemetry_System_Poller'); assert.strictEqual(poller.enable, true); - assert.strictEqual(poller.trace, false); + assert.strictEqual(poller.trace, undefined); assert.strictEqual(poller.interval, 300); assert.deepStrictEqual(poller.actions, [{ enable: true, setTag: { tenant: '`T`', application: '`A`' } }]); assert.strictEqual(poller.actions[0].ifAllMAtch, undefined); @@ -902,10 +1149,11 @@ describe('Declarations', () => { assert.strictEqual(poller.host, 'localhost'); assert.strictEqual(poller.port, 8100); assert.strictEqual(poller.protocol, 'http'); - assert.strictEqual(poller.allowSelfSignedCert, undefined); + assert.strictEqual(poller.allowSelfSignedCert, false); assert.strictEqual(poller.enableHostConnectivityCheck, undefined); assert.strictEqual(poller.username, undefined); assert.strictEqual(poller.passphrase, undefined); + assert.strictEqual(poller.endpointList, undefined); }); }); @@ -971,6 +1219,31 @@ describe('Declarations', () => { location: 'system_location' } } + }, + { + enable: true, + includeData: {}, + locations: { + virtualServers: true + }, + ifAnyMatch: [ + { + system: { + hostname: 'bigip1.example.com' + } + }, + { + system: { + hostname: 'bigip2.example.com' + } + } + ] + } + ], + endpointList: [ + { + name: 'myEndpoint', + path: 'myPath' } ] } @@ -990,7 +1263,7 @@ describe('Declarations', () => { assert.strictEqual(poller.allowSelfSignedCert, true); assert.strictEqual(poller.enableHostConnectivityCheck, false); assert.strictEqual(poller.username, 'username'); - assert.strictEqual(poller.passphrase.cipherText, 'foo'); + assert.strictEqual(poller.passphrase.cipherText, '$M$foo'); assert.strictEqual(poller.actions[0].enable, true); // setTag action assert.deepStrictEqual(poller.actions[0].setTag, { tag1: 'tag1 value', tag2: {} }); @@ -1004,103 +1277,401 @@ describe('Declarations', () => { assert.deepStrictEqual(poller.actions[2].excludeData, {}); assert.deepStrictEqual(poller.actions[2].locations, { pools: true }); assert.deepStrictEqual(poller.actions[2].ifAllMatch, { system: { location: 'system_location' } }); + // ifAnyMatch with includeData + assert.deepStrictEqual(poller.actions[3].includeData, {}); + assert.deepStrictEqual(poller.actions[3].locations, { virtualServers: true }); + assert.deepStrictEqual( + poller.actions[3].ifAnyMatch, + [{ system: { hostname: 'bigip1.example.com' } }, { system: { hostname: 'bigip2.example.com' } }] + ); + // endpointList + assert.deepStrictEqual( + poller.endpointList, + [ + { + enable: true, + name: 'myEndpoint', + path: 'myPath' + } + ] + ); }); }); - it('should not allow additional properties', () => { + it('should not allow ifAnyMatch and ifAllMatch in same action', () => { const data = { class: 'Telemetry', My_Poller: { class: 'Telemetry_System_Poller', - someProp: 'someValue' - } - }; - return assert.isRejected(config.validate(data), /someProp.*should NOT have additional properties/); - }); - }); - - describe('Telemetry_Listener', () => { - it('should pass miminal declaration', () => { - const data = { - class: 'Telemetry', - My_Listener: { - class: 'Telemetry_Listener' - } - }; - return config.validate(data) - .then((validConfig) => { - const listener = validConfig.My_Listener; - assert.notStrictEqual(listener, undefined); - assert.strictEqual(listener.class, 'Telemetry_Listener'); - assert.strictEqual(listener.enable, true); - assert.strictEqual(listener.trace, false); - assert.strictEqual(listener.port, 6514); - assert.deepStrictEqual(listener.actions, [{ enable: true, setTag: { tenant: '`T`', application: '`A`' } }]); - assert.deepStrictEqual(listener.match, ''); - }); - }); - - it('should pass full declaration', () => { - const data = { - class: 'Telemetry', - My_Listener: { - class: 'Telemetry_Listener', - enable: true, - trace: true, - port: 5000, - tag: { - tenant: '`B`', - application: '`C`' - }, - match: 'matchSomething', actions: [ { - enable: true, setTag: { - tag1: 'tag1 value', - tag2: {} + tag1: 'tag1 value' }, ifAllMatch: { system: { location: 'system_location' } }, + ifAnyMatch: [{ + system: { + hostname: 'system_hostname' + } + }], locations: { virtualServers: { '.*': true } } - }, + } + ] + } + }; + return assert.isRejected(config.validate(data), /should NOT be valid/); + }); + + it('should not allow an ifAnyMatch block that is not an array', () => { + const data = { + class: 'Telemetry', + My_Poller: { + class: 'Telemetry_System_Poller', + actions: [ { - enable: true, - includeData: {}, - locations: { - system: true + setTag: { + tag1: 'tag1 value' }, - ifAllMatch: { - system: { - location: 'system_location' - } - } - }, - { - enable: true, - excludeData: {}, - locations: { - pools: true + ifAnyMatch: { + top: 'level value' }, - ifAllMatch: { - system: { - location: 'system_location' + locations: { + virtualServers: { + '.*': true } } } ] } }; - return config.validate(data) - .then((validConfig) => { - const listener = validConfig.My_Listener; + return assert.isRejected(config.validate(data), /should be array/); + }); + + it('should not allow additional properties', () => { + const data = { + class: 'Telemetry', + My_Poller: { + class: 'Telemetry_System_Poller', + someProp: 'someValue' + } + }; + return assert.isRejected(config.validate(data), /someProp.*should NOT have additional properties/); + }); + + describe('interval', () => { + it('should restrict minimum to 1 when endpointList is specified', () => { + const data = { + class: 'Telemetry', + My_Poller: { + class: 'Telemetry_System_Poller', + interval: 0, + endpointList: { + items: { + testA: { name: 'a', path: 'some/a' } + } + } + } + }; + const errMsg = /interval\/minimum.*should be >= 1/; + return assert.isRejected(config.validate(data), errMsg); + }); + + it('should restrict minimum to 60 when endpointList is NOT specified', () => { + const data = { + class: 'Telemetry', + My_Poller: { + class: 'Telemetry_System_Poller', + interval: 10 + } + }; + const errMsg = /interval\/minimum.*should be >= 60/; + return assert.isRejected(config.validate(data), errMsg); + }); + + it('should restrict maximum to 6000 when endpointList is NOT specified', () => { + const data = { + class: 'Telemetry', + My_Poller: { + class: 'Telemetry_System_Poller', + interval: 60001 + } + }; + const errMsg = /interval\/maximum.*should be <= 6000/; + return assert.isRejected(config.validate(data), errMsg); + }); + + it('should not restrict maximum to 6000 when endpointList is specified', () => { + const data = { + class: 'Telemetry', + My_Poller: { + class: 'Telemetry_System_Poller', + interval: 100000, + endpointList: { + items: { + testA: { name: 'a', path: 'some/a' } + } + } + } + }; + return config.validate(data) + .then(validated => assert.strictEqual(validated.My_Poller.interval, 100000)); + }); + }); + + describe('endpointList', () => { + it('should allow endpointList as array (with different item types)', () => { + const data = { + class: 'Telemetry', + My_Endpoints1: { + class: 'Telemetry_Endpoints', + enable: true, + items: { + testA: { + name: 'a', + path: '/test/a', + enable: false + }, + testB: { + name: 'b', + path: '/test/b' + } + } + }, + My_Endpoints2: { + class: 'Telemetry_Endpoints', + enable: true, + basePath: '/testing', + items: { + testC: { + name: 'c', + path: '/item/c', + enable: false + }, + testD: { + name: 'd', + path: '/item/d' + } + } + }, + My_Poller: { + class: 'Telemetry_System_Poller', + endpointList: [ + 'My_Endpoints1', + 'My_Endpoints2/testD', + { + name: 'anEndpoint', + path: 'aPath' + }, + { + basePath: '/myBase/', + items: { + myEndpoint: { + name: 'myEndpoint', + path: 'myPath' + } + } + } + ] + } + }; + return config.validate(data) + .then((validConfig) => { + const poller = validConfig.My_Poller; + assert.deepStrictEqual( + poller.endpointList, + [ + 'My_Endpoints1', + 'My_Endpoints2/testD', + { + name: 'anEndpoint', + path: 'aPath', + enable: true + }, + { + enable: true, + basePath: '/myBase/', + items: { + myEndpoint: { + name: 'myEndpoint', + path: 'myPath', + enable: true + } + } + } + ] + ); + }); + }); + + it('should require endpointList to have at least one item if array', () => { + const data = { + class: 'Telemetry', + My_Poller: { + class: 'Telemetry_System_Poller', + endpointList: [] + } + }; + const errMsg = 'should NOT have fewer than 1 items'; + return assert.isRejected(config.validate(data), errMsg); + }); + + it('should not allow endpointList to be empty object', () => { + const data = { + class: 'Telemetry', + My_Poller: { + class: 'Telemetry_System_Poller', + endpointList: {} + } + }; + const errMsg = 'should have required property \'items\''; + return assert.isRejected(config.validate(data), errMsg); + }); + + it('should allow endpointList as Telemetry_Endpoints (as single reference)', () => { + const data = { + class: 'Telemetry', + My_Endpoints: { + class: 'Telemetry_Endpoints', + enable: true, + items: { + testA: { name: 'a', path: 'some/a' } + } + }, + My_Poller: { + class: 'Telemetry_System_Poller', + endpointList: 'My_Endpoints' + } + }; + return config.validate(data) + .then((validConfig) => { + const poller = validConfig.My_Poller; + assert.deepStrictEqual(poller.endpointList, 'My_Endpoints'); + }); + }); + + it('should allow endpointList as Telemetry_Endpoints (as inline object)', () => { + const data = { + class: 'Telemetry', + My_Poller: { + class: 'Telemetry_System_Poller', + endpointList: { + enable: true, + items: { + testA: { name: 'a', path: 'some/a' } + } + } + } + }; + return config.validate(data) + .then((validConfig) => { + const poller = validConfig.My_Poller; + assert.deepStrictEqual(poller.endpointList, + { + enable: true, + items: { + testA: { + enable: true, + name: 'a', + path: 'some/a' + } + }, + basePath: '' + }); + }); + }); + }); + }); + + describe('Telemetry_Listener', () => { + it('should pass minimal declaration', () => { + const data = { + class: 'Telemetry', + My_Listener: { + class: 'Telemetry_Listener' + } + }; + return config.validate(data) + .then((validConfig) => { + const listener = validConfig.My_Listener; + assert.notStrictEqual(listener, undefined); + assert.strictEqual(listener.class, 'Telemetry_Listener'); + assert.strictEqual(listener.enable, true); + assert.strictEqual(listener.trace, false); + assert.strictEqual(listener.port, 6514); + assert.deepStrictEqual(listener.actions, [{ enable: true, setTag: { tenant: '`T`', application: '`A`' } }]); + assert.deepStrictEqual(listener.match, ''); + }); + }); + + it('should pass full declaration', () => { + const data = { + class: 'Telemetry', + My_Listener: { + class: 'Telemetry_Listener', + enable: true, + trace: true, + port: 5000, + tag: { + tenant: '`B`', + application: '`C`' + }, + match: 'matchSomething', + actions: [ + { + enable: true, + setTag: { + tag1: 'tag1 value', + tag2: {} + }, + ifAllMatch: { + system: { + location: 'system_location' + } + }, + locations: { + virtualServers: { + '.*': true + } + } + }, + { + enable: true, + includeData: {}, + locations: { + system: true + }, + ifAllMatch: { + system: { + location: 'system_location' + } + } + }, + { + enable: true, + excludeData: {}, + locations: { + pools: true + }, + ifAllMatch: { + system: { + location: 'system_location' + } + } + } + ] + } + }; + return config.validate(data) + .then((validConfig) => { + const listener = validConfig.My_Listener; assert.notStrictEqual(listener, undefined); assert.strictEqual(listener.class, 'Telemetry_Listener'); assert.strictEqual(listener.enable, true); @@ -1137,7 +1708,7 @@ describe('Declarations', () => { }); describe('Telemetry_iHealth_Poller', () => { - it('should pass miminal declaration', () => { + it('should pass minimal declaration', () => { const data = { class: 'Telemetry', My_iHealth_Poller: { @@ -1160,7 +1731,7 @@ describe('Declarations', () => { assert.notStrictEqual(poller, undefined); assert.strictEqual(poller.class, 'Telemetry_iHealth_Poller'); assert.strictEqual(poller.username, 'username'); - assert.strictEqual(poller.passphrase.cipherText, 'foo'); + assert.strictEqual(poller.passphrase.cipherText, '$M$foo'); assert.deepStrictEqual(poller.interval, { timeWindow: { start: '00:00', @@ -1207,7 +1778,7 @@ describe('Declarations', () => { assert.notStrictEqual(poller, undefined); assert.strictEqual(poller.class, 'Telemetry_iHealth_Poller'); assert.strictEqual(poller.username, 'username'); - assert.strictEqual(poller.passphrase.cipherText, 'foo'); + assert.strictEqual(poller.passphrase.cipherText, '$M$foo'); assert.deepStrictEqual(poller.interval, { frequency: 'weekly', day: 1, @@ -1223,7 +1794,7 @@ describe('Declarations', () => { assert.strictEqual(proxy.allowSelfSignedCert, true); assert.strictEqual(proxy.enableHostConnectivityCheck, false); assert.strictEqual(proxy.username, 'username'); - assert.strictEqual(proxy.passphrase.cipherText, 'foo'); + assert.strictEqual(proxy.passphrase.cipherText, '$M$foo'); }); }); @@ -1435,7 +2006,7 @@ describe('Declarations', () => { }); describe('Telemetry_System', () => { - it('should pass miminal declaration', () => { + it('should pass minimal declaration', () => { const data = { class: 'Telemetry', My_System: { @@ -1448,11 +2019,11 @@ describe('Declarations', () => { assert.notStrictEqual(system, undefined); assert.strictEqual(system.class, 'Telemetry_System'); assert.strictEqual(system.enable, true); - assert.strictEqual(system.trace, false); + assert.strictEqual(system.trace, undefined); assert.strictEqual(system.host, 'localhost'); assert.strictEqual(system.port, 8100); assert.strictEqual(system.protocol, 'http'); - assert.strictEqual(system.allowSelfSignedCert, undefined); + assert.strictEqual(system.allowSelfSignedCert, false); assert.strictEqual(system.enableHostConnectivityCheck, undefined); assert.strictEqual(system.username, undefined); assert.strictEqual(system.passphrase, undefined); @@ -1462,6 +2033,22 @@ describe('Declarations', () => { it('should pass full declaration', () => { const data = { class: 'Telemetry', + My_Poller: { + class: 'Telemetry_System_Poller' + }, + My_iHealth_Poller: { + class: 'Telemetry_iHealth_Poller', + username: 'username', + passphrase: { + cipherText: 'passphrase' + }, + interval: { + timeWindow: { + start: '00:00', + end: '03:00' + } + } + }, My_System: { class: 'Telemetry_System', enable: true, @@ -1474,7 +2061,14 @@ describe('Declarations', () => { username: 'username', passphrase: { cipherText: 'passphrase' - } + }, + systemPoller: [ + 'My_Poller', + { + interval: 100 + } + ], + iHealthPoller: 'My_iHealth_Poller' } }; return config.validate(data) @@ -1490,7 +2084,24 @@ describe('Declarations', () => { assert.strictEqual(system.allowSelfSignedCert, true); assert.strictEqual(system.enableHostConnectivityCheck, false); assert.strictEqual(system.username, 'username'); - assert.strictEqual(system.passphrase.cipherText, 'foo'); + assert.strictEqual(system.passphrase.cipherText, '$M$foo'); + assert.strictEqual(system.iHealthPoller, 'My_iHealth_Poller'); + assert.deepStrictEqual(system.systemPoller, [ + 'My_Poller', + { + actions: [ + { + enable: true, + setTag: { + application: '`A`', + tenant: '`T`' + } + } + ], + enable: true, + interval: 100 + } + ]); }); }); @@ -1558,7 +2169,7 @@ describe('Declarations', () => { const poller = validConfig.My_System.systemPoller; assert.notStrictEqual(poller, undefined); assert.strictEqual(poller.enable, true); - assert.strictEqual(poller.trace, false); + assert.strictEqual(poller.trace, undefined); assert.strictEqual(poller.interval, 300); assert.deepStrictEqual(poller.actions, [{ enable: true, setTag: { tenant: '`T`', application: '`A`' } }]); assert.strictEqual(poller.actions[0].ifAllMAtch, undefined); @@ -1630,7 +2241,7 @@ describe('Declarations', () => { }); }); - it('should not-allow to attach inline System Poller declaration with specified host', () => { + it('should not allow to attach inline System Poller declaration with additional properties', () => { const data = { class: 'Telemetry', My_System: { @@ -1667,7 +2278,7 @@ describe('Declarations', () => { const poller = validConfig.My_System.iHealthPoller; assert.notStrictEqual(poller, undefined); assert.strictEqual(poller.username, 'username'); - assert.strictEqual(poller.passphrase.cipherText, 'foo'); + assert.strictEqual(poller.passphrase.cipherText, '$M$foo'); assert.deepStrictEqual(poller.interval, { timeWindow: { start: '00:00', @@ -1715,7 +2326,7 @@ describe('Declarations', () => { const poller = validConfig.My_System.iHealthPoller; assert.notStrictEqual(poller, undefined); assert.strictEqual(poller.username, 'username'); - assert.strictEqual(poller.passphrase.cipherText, 'foo'); + assert.strictEqual(poller.passphrase.cipherText, '$M$foo'); assert.deepStrictEqual(poller.interval, { frequency: 'weekly', day: 1, @@ -1731,10 +2342,33 @@ describe('Declarations', () => { assert.strictEqual(proxy.allowSelfSignedCert, true); assert.strictEqual(proxy.enableHostConnectivityCheck, false); assert.strictEqual(proxy.username, 'username'); - assert.strictEqual(proxy.passphrase.cipherText, 'foo'); + assert.strictEqual(proxy.passphrase.cipherText, '$M$foo'); }); }); + it('should not allow to attach inline iHealth Poller declaration with additional properties', () => { + const data = { + class: 'Telemetry', + My_System: { + class: 'Telemetry_System', + iHealthPoller: { + something: true, + username: 'username', + passphrase: { + cipherText: 'passphrase' + }, + interval: { + timeWindow: { + start: '00:00', + end: '03:00' + } + } + } + } + }; + return assert.isRejected(config.validate(data), /iHealthPoller.*should NOT have additional properties/); + }); + it('should allow to attach inline declaration for System Poller and iHealth Poller', () => { const data = { class: 'Telemetry', @@ -1759,5 +2393,328 @@ describe('Declarations', () => { }; return assert.isFulfilled(config.validate(data)); }); + + it('should allow systemPoller as an array (inline object)', () => { + const data = { + class: 'Telemetry', + My_System: { + class: 'Telemetry_System', + systemPoller: [ + { + interval: 1440, + trace: true + }, + { + interval: 90 + } + ] + } + }; + return config.validate(data) + .then((validConfig) => { + const poller = validConfig.My_System.systemPoller; + assert.deepStrictEqual(poller, + [ + { + actions: [{ + enable: true, + setTag: { application: '`A`', tenant: '`T`' } + }], + trace: true, + interval: 1440, + enable: true + }, + { + actions: [{ + enable: true, + setTag: { application: '`A`', tenant: '`T`' } + }], + interval: 90, + enable: true + } + ]); + }); + }); + + it('should allow systemPoller as an array of references', () => { + const data = { + class: 'Telemetry', + My_System: { + class: 'Telemetry_System', + systemPoller: [ + 'Poller_1', + 'Poller_2' + ] + }, + Poller_1: { + class: 'Telemetry_System_Poller', + interval: 80 + }, + Poller_2: { + class: 'Telemetry_System_Poller', + interval: 100, + trace: true + } + }; + return assert.isFulfilled(config.validate(data)); + }); + + it('should allow systemPoller as an array (mixed ref and object)', () => { + const data = { + class: 'Telemetry', + My_System: { + class: 'Telemetry_System', + systemPoller: [ + 'Poller_1', + { + interval: 299 + } + ] + }, + Poller_1: { + class: 'Telemetry_System_Poller', + interval: 80 + } + }; + return assert.isFulfilled(config.validate(data)); + }); + + it('should not allow a systemPoller as empty array', () => { + const data = { + class: 'Telemetry', + My_System: { + class: 'Telemetry_System', + systemPoller: [] + } + }; + const errMsg = 'should NOT have fewer than 1 items'; + return assert.isRejected(config.validate(data), errMsg); + }); + + it('should not allow a systemPoller with empty endpointList object', () => { + const data = { + class: 'Telemetry', + My_System: { + class: 'Telemetry_System', + systemPoller: { + interval: 120, + endpointList: {} + } + } + }; + const errMsg = 'should have required property \'items\''; + return assert.isRejected(config.validate(data), errMsg); + }); + + it('should not allow a systemPoller with empty items in endpointList object', () => { + const data = { + class: 'Telemetry', + My_System: { + class: 'Telemetry_System', + systemPoller: { + interval: 120, + endpointList: { + items: {} + } + } + } + }; + const errMsg = 'should NOT have fewer than 1 properties'; + return assert.isRejected(config.validate(data), errMsg); + }); + + it('should not allow a systemPoller with empty endpointList array', () => { + const data = { + class: 'Telemetry', + My_System: { + class: 'Telemetry_System', + systemPoller: { + interval: 120, + endpointList: [] + } + } + }; + const errMsg = 'should NOT have fewer than 1 items'; + return assert.isRejected(config.validate(data), errMsg); + }); + + it('should not allow a systemPoller with empty items in endpointList array', () => { + const data = { + class: 'Telemetry', + My_System: { + class: 'Telemetry_System', + systemPoller: { + interval: 120, + endpointList: [ + { + items: {} + } + ] + } + } + }; + const errMsg = 'should NOT have fewer than 1 properties'; + return assert.isRejected(config.validate(data), errMsg); + }); + }); + + describe('Telemetry_Endpoints', () => { + it('should pass minimal declaration', () => { + const data = { + class: 'Telemetry', + My_Endpoints: { + class: 'Telemetry_Endpoints', + items: { + test: { + path: '/test/path' + } + } + } + }; + return config.validate(data) + .then((validConfig) => { + const endpoints = validConfig.My_Endpoints; + assert.deepStrictEqual(endpoints.items, { + test: { + enable: true, + path: '/test/path' + } + }); + // check defaults + assert.strictEqual(endpoints.enable, true); + }); + }); + + it('should not allow additional properties', () => { + const data = { + class: 'Telemetry', + My_Endpoints: { + class: 'Telemetry_Endpoints', + items: { + test: { + name: 'test', + path: '/test/path' + } + }, + something: true + } + }; + return assert.isRejected(config.validate(data), /something.*should NOT have additional properties/); + }); + + it('should allow full declaration', () => { + const data = { + class: 'Telemetry', + My_Endpoints: { + class: 'Telemetry_Endpoints', + enable: true, + basePath: '/some/base', + items: { + a: { + name: 'testA', + path: '/test/A' + }, + b: { + name: 'testB', + path: '/test/B', + enable: false + } + } + } + }; + + return config.validate(data) + .then((validConfig) => { + const endpoints = validConfig.My_Endpoints; + assert.deepStrictEqual(endpoints.items, { + a: { + enable: true, + name: 'testA', + path: '/test/A' + }, + b: { + name: 'testB', + path: '/test/B', + enable: false + } + }); + }); + }); + + it('should not allow empty items', () => { + const data = { + class: 'Telemetry', + My_Endpoints: { + class: 'Telemetry_Endpoints', + items: {} + } + }; + return assert.isRejected(config.validate(data), /\/My_Endpoints\/items.*items\/minProperties.*limit":1/); + }); + + it('should not allow items that are not of type object', () => { + const data = { + class: 'Telemetry', + My_Endpoints: { + class: 'Telemetry_Endpoints', + enable: true, + items: [ + 'endpoint' + ] + } + }; + return assert.isRejected(config.validate(data), /\/My_Endpoints\/items.*should be object/); + }); + + it('should not allow additional properties in items', () => { + const data = { + class: 'Telemetry', + My_Endpoints: { + class: 'Telemetry_Endpoints', + enable: true, + items: { + first: { + name: 'myEndpoint', + path: 'myPath', + something: 'else' + } + } + } + }; + return assert.isRejected(config.validate(data), /\/My_Endpoints\/items.*should NOT have additional properties/); + }); + + it('should not allow empty name', () => { + const data = { + class: 'Telemetry', + My_Endpoints: { + class: 'Telemetry_Endpoints', + enable: true, + items: { + first: { + name: '', + path: 'path' + } + } + } + }; + return assert.isRejected(config.validate(data), /\/My_Endpoints\/items\/first\/name.*should NOT be shorter than/); + }); + + it('should not allow empty path', () => { + const data = { + class: 'Telemetry', + My_Endpoints: { + class: 'Telemetry_Endpoints', + enable: true, + items: { + first: { + path: '' + } + } + } + }; + return assert.isRejected(config.validate(data), /\/My_Endpoints\/items\/first\/path.*should NOT be shorter than/); + }); }); }); diff --git a/test/unit/deviceUtilTests.js b/test/unit/deviceUtilTests.js index f3592d1f..e56540fd 100644 --- a/test/unit/deviceUtilTests.js +++ b/test/unit/deviceUtilTests.js @@ -8,750 +8,773 @@ 'use strict'; -const assert = require('assert'); +/* eslint-disable import/order */ + +require('./shared/restoreCache')(); + +const chai = require('chai'); +const chaiAsPromised = require('chai-as-promised'); +const childProcess = require('child_process'); +const crypto = require('crypto'); const os = require('os'); const fs = require('fs'); +const nock = require('nock'); +const request = require('request'); +const sinon = require('sinon'); const urllib = require('url'); -const constants = require('../../src/lib/constants.js'); - -/* eslint-disable global-require */ +const constants = require('../../src/lib/constants'); +const deviceUtil = require('../../src/lib/deviceUtil'); +const deviceUtilTestsData = require('./deviceUtilTestsData'); +const testUtil = require('./shared/util'); -let parseURL; -if (process.versions.node.startsWith('4.')) { - parseURL = urllib.parse; -} else { - parseURL = url => new urllib.URL(url); -} +chai.use(chaiAsPromised); +const assert = chai.assert; describe('Device Util', () => { - let deviceUtil; - let childProcess; - let request; - - const setupRequestMock = (res, body, mockOpts) => { - mockOpts = mockOpts || {}; - ['get', 'post', 'delete'].forEach((method) => { - request[method] = (opts, cb) => { - cb(mockOpts.err, res, mockOpts.toJSON === false ? body : JSON.stringify(body)); - }; - }); - }; - - before(() => { - deviceUtil = require('../../src/lib/deviceUtil.js'); - childProcess = require('child_process'); - request = require('request'); + afterEach(() => { + testUtil.checkNockActiveMocks(nock, assert); + nock.cleanAll(); + sinon.restore(); }); - after(() => { - Object.keys(require.cache).forEach((key) => { - delete require.cache[key]; + + describe('Host Device Info', () => { + beforeEach(() => { + deviceUtil.clearHostDeviceInfo(); }); - }); - it('should get BIG-IP device type', () => { - childProcess.exec = (cmd, cb) => { cb(null, cmd, null); }; + it('should gather device info', () => { + sinon.stub(deviceUtil, 'getDeviceType').resolves(constants.DEVICE_TYPE.BIG_IP); + sinon.stub(deviceUtil, 'getDeviceVersion').resolves({ version: '14.0.0' }); + return deviceUtil.gatherHostDeviceInfo() + .then(() => { + assert.deepStrictEqual( + deviceUtil.getHostDeviceInfo(), + { + TYPE: 'BIG-IP', + VERSION: { version: '14.0.0' }, + RETRIEVE_SECRETS_FROM_TMSH: false + } + ); + }); + }); - const BIG_IP_DEVICE_TYPE = constants.BIG_IP_DEVICE_TYPE; - return deviceUtil.getDeviceType() - .then((data) => { - assert.strictEqual(data, BIG_IP_DEVICE_TYPE, 'incorrect device type'); - return Promise.resolve(); - }) - .catch(err => Promise.reject(err)); - }); + it('should set and get info by key', () => { + deviceUtil.setHostDeviceInfo('key1', 'value1'); + deviceUtil.setHostDeviceInfo('key2', { value2: 10 }); + assert.strictEqual(deviceUtil.getHostDeviceInfo('key1'), 'value1'); + assert.deepStrictEqual(deviceUtil.getHostDeviceInfo('key2'), { value2: 10 }); + }); - it('should get container device type', () => { - childProcess.exec = (cmd, cb) => { cb(new Error('foo'), null, null); }; + it('should remove key', () => { + deviceUtil.setHostDeviceInfo('key1', 'value1'); + assert.strictEqual(deviceUtil.getHostDeviceInfo('key1'), 'value1'); + deviceUtil.clearHostDeviceInfo('key1'); + assert.strictEqual(deviceUtil.getHostDeviceInfo('key1'), undefined); + assert.deepStrictEqual(deviceUtil.getHostDeviceInfo(), {}); + }); - const CONTAINER_DEVICE_TYPE = constants.CONTAINER_DEVICE_TYPE; - return deviceUtil.getDeviceType() - .then((data) => { - assert.strictEqual(data, CONTAINER_DEVICE_TYPE, 'incorrect device type'); - return Promise.resolve(); - }) - .catch(err => Promise.reject(err)); + it('should remove keys', () => { + deviceUtil.setHostDeviceInfo('key1', 'value1'); + deviceUtil.setHostDeviceInfo('key2', 'value2'); + assert.strictEqual(deviceUtil.getHostDeviceInfo('key1'), 'value1'); + assert.strictEqual(deviceUtil.getHostDeviceInfo('key2'), 'value2'); + deviceUtil.clearHostDeviceInfo('key1', 'key2'); + assert.strictEqual(deviceUtil.getHostDeviceInfo('key1'), undefined); + assert.strictEqual(deviceUtil.getHostDeviceInfo('key2'), undefined); + assert.deepStrictEqual(deviceUtil.getHostDeviceInfo(), {}); + }); }); - it('should fail on non-valid response on attempt to download file', () => { - const mockBody = 'somedata'; - const mockHeaders = { - 'content-length': mockBody.length - }; - const mockRes = { statusCode: 200, statusMessage: 'message', headers: mockHeaders }; - setupRequestMock(mockRes, mockBody); + describe('.getDeviceType()', () => { + beforeEach(() => { + deviceUtil.clearHostDeviceInfo(); + }); - return deviceUtil.downloadFileFromDevice('/wrong/path/to/file', constants.LOCAL_HOST, '/uri/to/path') - .then(() => { - assert.fail('Should throw an error'); - }) - .catch((err) => { - if (err.code === 'ERR_ASSERTION') return Promise.reject(err); - if (/HTTP Error:/.test(err)) return Promise.resolve(); - assert.fail(err); - return Promise.reject(err); + it('should get container device type when /VERSION file is absent', () => { + sinon.stub(fs, 'readFile').callsFake((first, cb) => { + cb(new Error('foo'), null); }); - }); - - it('should able to download file to provided stream', () => { - const expectedData = 'somedata'; - const mockHeaders = { - 'content-range': `0-${expectedData.length - 1}/${expectedData.length}`, - 'content-length': expectedData.length - }; - const mockRes = { statusCode: 200, statusMessage: 'message', headers: mockHeaders }; - const mockBody = Buffer.from(expectedData); - request.get = (opts, cb) => { - cb(null, mockRes, mockBody); - }; - - const dstPath = `${os.tmpdir()}/testDownloadFileUserStream`; - const dst = fs.createWriteStream(dstPath); + return assert.becomes( + deviceUtil.getDeviceType(), + constants.DEVICE_TYPE.CONTAINER, + 'incorrect device type, should be CONTAINER' + ); + }); - return deviceUtil.downloadFileFromDevice(dst, constants.LOCAL_HOST, '/uri/to/path') - .then(() => { - const contents = fs.readFileSync(dstPath); - assert.ok(contents.equals(mockBody), 'should equal to origin Buffer'); + it('should get container device type when /VERSION has no desired data', () => { + sinon.stub(fs, 'readFile').callsFake((first, cb) => { + cb(null, deviceUtilTestsData.getDeviceType.incorrectData); }); - }); + return assert.becomes( + deviceUtil.getDeviceType(), + constants.DEVICE_TYPE.CONTAINER, + 'incorrect device type, should be CONTAINER' + ); + }); - it('should fail to download file when content-range is invalid', () => { - const expectedData = 'somedata'; - const mockHeaders = { - 'content-range': `0-${expectedData.length - 3}/${expectedData.length - 2}`, - 'content-length': expectedData.length - }; - const mockRes = { statusCode: 200, statusMessage: 'message', headers: mockHeaders }; - request.get = (opts, cb) => { - cb(null, mockRes, Buffer.from(expectedData)); - }; + it('should get BIG-IP device type', () => { + sinon.stub(fs, 'readFile').callsFake((first, cb) => { + cb(null, deviceUtilTestsData.getDeviceType.correctData); + }); + return assert.becomes( + deviceUtil.getDeviceType(), + constants.DEVICE_TYPE.BIG_IP, + 'incorrect device type, should be BIG-IP' + ); + }); - const dstPath = `${os.tmpdir()}/testDownloadFileUserStream`; + it('should process /VERSION file correctly when readFile returns Buffer instead of String', () => { + sinon.stub(fs, 'readFile').callsFake((first, cb) => { + cb(null, Buffer.from(deviceUtilTestsData.getDeviceType.correctData)); + }); + return assert.becomes( + deviceUtil.getDeviceType(), + constants.DEVICE_TYPE.BIG_IP, + 'incorrect device type, should be BIG-IP' + ); + }); - return deviceUtil.downloadFileFromDevice(dstPath, constants.LOCAL_HOST, '/uri/to/path') - .then(() => { - assert.fail('Should throw an error'); - }) - .catch((err) => { - if (err.code === 'ERR_ASSERTION') return Promise.reject(err); - if (/Exceeded expected size/.test(err)) return Promise.resolve(); - assert.fail(err); - return Promise.reject(err); + it('should read result from cache', () => { + const readFileStub = sinon.stub(fs, 'readFile'); + readFileStub.callsFake((first, cb) => { + cb(null, deviceUtilTestsData.getDeviceType.correctData); }); + sinon.stub(deviceUtil, 'getDeviceVersion').resolves({ version: '14.0.0' }); + return deviceUtil.gatherHostDeviceInfo() + .then(() => deviceUtil.getDeviceType()) + .then((deviceType) => { + assert.strictEqual(deviceType, constants.DEVICE_TYPE.BIG_IP, 'incorrect device type, should be BIG-IP'); + assert.strictEqual(readFileStub.callCount, 1); + }); + }); }); - it('should fail to download file (response\' code !== 200)', () => { - const expectedData = 'somedata'; - const mockHeaders = { - 'content-range': `0-${expectedData.length - 1}/${expectedData.length}`, - 'content-length': expectedData.length - }; - const mockRes = { statusCode: 404, statusMessage: 'message', headers: mockHeaders }; - request.get = (opts, cb) => { - cb(null, mockRes, Buffer.from(expectedData)); - }; - + describe('.downloadFileFromDevice()', () => { const dstPath = `${os.tmpdir()}/testDownloadFileUserStream`; - const dst = fs.createWriteStream(dstPath); - - return deviceUtil.downloadFileFromDevice(dst, constants.LOCAL_HOST, '/uri/to/path') - .then(() => { - assert.fail('Should throw an error'); - }) - .catch((err) => { - if (err.code === 'ERR_ASSERTION') return Promise.reject(err); - if (/Exceeded number of attempts on HTTP error/.test(err)) return Promise.resolve(); - assert.fail(err); - return Promise.reject(err); - }); - }); + const cleanUp = () => { + if (fs.existsSync(dstPath)) { + fs.unlinkSync(dstPath); + } + }; - it('should fail on attempt to execute invalid unix command', () => { - assert.throws( - () => { - deviceUtil.runTMUtilUnixCommand('cp'); - }, - (err) => { - if ((err instanceof Error) && /invalid command/.test(err)) { - return true; + beforeEach(cleanUp); + afterEach(cleanUp); + + it('should fail to write data to file', () => { + const response = 'response'; + testUtil.mockEndpoints([{ + endpoint: '/uri/to/path', + response, + responseHeaders: { + 'content-range': `0-${response.length - 1}/${response.length}`, + 'content-length': response.length } - return false; - }, - 'unexpected error' - ); - }); + }]); + return assert.isRejected( + deviceUtil.downloadFileFromDevice('/non-existing/path', constants.LOCAL_HOST, '/uri/to/path'), + /downloadFileFromDevice.*no such file or directory/ + ); + }); - it('should fail on attempt to list non-existing folder', () => { - const mockRes = { statusCode: 200, statusMessage: 'message' }; - const mockBody = { commandResult: '/bin/ls: cannot access /config1: No such file or directory\n' }; - setupRequestMock(mockRes, mockBody); + it('should fail on invalid response on attempt to download file', () => { + const response = 'response'; + testUtil.mockEndpoints([{ + endpoint: '/uri/to/path', + response, + responseHeaders: { + 'content-length': response.length + } + }]); + return assert.isRejected( + deviceUtil.downloadFileFromDevice(fs.createWriteStream(dstPath), constants.LOCAL_HOST, '/uri/to/path'), + /HTTP Error:/ + ); + }); - return deviceUtil.runTMUtilUnixCommand('ls', '/config1', constants.LOCAL_HOST) - .then(() => { - assert.fail('Should throw an error'); - }) - .catch((err) => { - if (err.code === 'ERR_ASSERTION') return Promise.reject(err); - if (/No such file or directory/.test(err)) return Promise.resolve(); - assert.fail(err); - return Promise.reject(err); - }); - }); + it('should able to download file to provided stream', () => { + const response = 'response'; + testUtil.mockEndpoints([{ + endpoint: '/uri/to/path', + response, + responseHeaders: { + 'content-range': `0-${response.length - 1}/${response.length}`, + 'content-length': response.length + } + }]); + return deviceUtil.downloadFileFromDevice(dstPath, constants.LOCAL_HOST, '/uri/to/path') + .then(() => { + const contents = fs.readFileSync(dstPath); + assert.ok(contents.equals(Buffer.from(response)), 'should equal to origin Buffer'); + }); + }); - it('should fail on attempt to move non-existing folder', () => { - const mockRes = { statusCode: 200, statusMessage: 'message' }; - const mockBody = { commandResult: 'some error here' }; - setupRequestMock(mockRes, mockBody); + it('should fail to download file when content-range is invalid', () => { + const response = 'response'; + testUtil.mockEndpoints([{ + endpoint: '/uri/to/path', + response, + responseHeaders: { + 'content-range': `0-${response.length - 3}/${response.length - 2}`, + 'content-length': response.length + }, + options: { + times: 2 + } + }]); + return assert.isRejected( + deviceUtil.downloadFileFromDevice(dstPath, constants.LOCAL_HOST, '/uri/to/path'), + /Exceeded expected size/ + ); + }); - return deviceUtil.runTMUtilUnixCommand('mv', '/config1', constants.LOCAL_HOST) - .then(() => { - assert.fail('Should throw an error'); - }) - .catch((err) => { - if (err.code === 'ERR_ASSERTION') return Promise.reject(err); - if (/some error here/.test(err)) return Promise.resolve(); - assert.fail(err); - return Promise.reject(err); - }); + it('should fail to download file (response code !== 200)', () => { + const response = 'response'; + testUtil.mockEndpoints([{ + endpoint: '/uri/to/path', + code: 404, + response, + responseHeaders: { + 'content-range': `0-${response.length - 1}/${response.length}`, + 'content-length': response.length + }, + options: { + times: 5 + } + }]); + return assert.isRejected( + deviceUtil.downloadFileFromDevice(dstPath, constants.LOCAL_HOST, '/uri/to/path'), + /Exceeded number of attempts on HTTP error/ + ); + }); }); - it('should fail on attempt to remove non-existing folder', () => { - const mockRes = { statusCode: 200, statusMessage: 'message' }; - const mockBody = { commandResult: 'some error here' }; - setupRequestMock(mockRes, mockBody); - - return deviceUtil.runTMUtilUnixCommand('rm', '/config1', constants.LOCAL_HOST) - .then(() => { - assert.fail('Should throw an error'); - }) - .catch((err) => { - if (err.code === 'ERR_ASSERTION') return Promise.reject(err); - if (/some error here/.test(err)) return Promise.resolve(); - assert.fail(err); - return Promise.reject(err); - }); - }); + describe('.runTMUtilUnixCommand()', () => { + it('should fail on attempt to execute invalid unix command', () => assert.throws( + () => deviceUtil.runTMUtilUnixCommand('cp'), + /runTMUtilUnixCommand: invalid command/ + )); + + it('should fail on attempt to list non-existing folder', () => { + testUtil.mockEndpoints([{ + endpoint: '/mgmt/tm/util/unix-ls', + method: 'post', + response: { + commandResult: '/bin/ls: cannot access /config1: No such file or directory\n' + } + }]); + return assert.isRejected( + deviceUtil.runTMUtilUnixCommand('ls', '/config1', constants.LOCAL_HOST), + /No such file or directory/ + ); + }); - it('should pass on attempt to remove/move folder', () => { - const mockRes = { statusCode: 200, statusMessage: 'message' }; - const mockBody = {}; - setupRequestMock(mockRes, mockBody); + it('should fail on attempt to move non-existing folder', () => { + testUtil.mockEndpoints([{ + endpoint: '/mgmt/tm/util/unix-mv', + method: 'post', + response: { + commandResult: 'some error here' + } + }]); + return assert.isRejected( + deviceUtil.runTMUtilUnixCommand('mv', '/config1', constants.LOCAL_HOST), + /some error here/ + ); + }); - return deviceUtil.runTMUtilUnixCommand('rm', '/config1', constants.LOCAL_HOST) - .then(() => deviceUtil.runTMUtilUnixCommand('mv', '/config1', constants.LOCAL_HOST)); - }); + it('should fail on attempt to remove non-existing folder', () => { + testUtil.mockEndpoints([{ + endpoint: '/mgmt/tm/util/unix-rm', + method: 'post', + response: { + commandResult: 'some error here' + } + }]); + return assert.isRejected( + deviceUtil.runTMUtilUnixCommand('rm', '/config1', constants.LOCAL_HOST), + /some error here/ + ); + }); - it('should pass on attempt to list folder', () => { - const mockRes = { statusCode: 200, statusMessage: 'message' }; - const mockBody = { commandResult: 'something' }; - setupRequestMock(mockRes, mockBody); + it('should pass on attempt to remove/move folder', () => { + testUtil.mockEndpoints([{ + endpoint: /\/mgmt\/tm\/util\/unix-(rm|mv)/, + method: 'post', + response: {}, + options: { + times: 2 + } + }]); + return assert.isFulfilled(deviceUtil.runTMUtilUnixCommand('rm', '/config1', constants.LOCAL_HOST) + .then(() => deviceUtil.runTMUtilUnixCommand('mv', '/config1', constants.LOCAL_HOST))); + }); - return deviceUtil.runTMUtilUnixCommand('ls', '/config1', constants.LOCAL_HOST); + it('should pass on attempt to list folder', () => { + testUtil.mockEndpoints([{ + endpoint: '/mgmt/tm/util/unix-ls', + method: 'post', + response: { + commandResult: 'something' + } + }]); + return assert.becomes( + deviceUtil.runTMUtilUnixCommand('ls', '/config1', constants.LOCAL_HOST), + 'something' + ); + }); }); - it('should return device version', () => { - const mockRes = { statusCode: 200, statusMessage: 'message' }; - const mockBody = { - entries: { - someKey: { - nestedStats: { - entries: { - version: { - description: '14.1.0' - }, - BuildInfo: { - description: '0.0.1' + describe('.getDeviceVersion()', () => { + it('should return device version', () => { + testUtil.mockEndpoints([{ + endpoint: '/mgmt/tm/sys/version', + response: { + entries: { + someKey: { + nestedStats: { + entries: { + version: { + description: '14.1.0' + }, + BuildInfo: { + description: '0.0.1' + } + } } } } } - } - }; - const expected = { - version: '14.1.0', - buildInfo: '0.0.1' - }; - setupRequestMock(mockRes, mockBody); - return deviceUtil.getDeviceVersion(constants.LOCAL_HOST) - .then((data) => { - assert.deepEqual(data, expected); - return Promise.resolve(); - }) - .catch(err => Promise.reject(err)); - }); - - it('should fail on return device version', () => { - const mockRes = { statusCode: 400, statusMessage: 'error' }; - const mockBody = {}; - setupRequestMock(mockRes, mockBody); - - return deviceUtil.getDeviceVersion(constants.LOCAL_HOST) - .then(() => { - assert.fail('Should throw an error'); - }) - .catch((err) => { - if (err.code === 'ERR_ASSERTION') return Promise.reject(err); - if (/getDeviceVersion:/.test(err)) return Promise.resolve(); - assert.fail(err); - return Promise.reject(err); - }); - }); - - it('should preserve device\'s default port, protocol, HTTP method and etc.', () => { - const uri = '/uri/something'; - const authToken = 'token'; - - request.get = (opts, cb) => { - const parsedURL = parseURL(opts.uri); - - assert.strictEqual(parsedURL.pathname, uri); - assert.strictEqual(parsedURL.protocol.slice(0, -1), constants.DEVICE_DEFAULT_PROTOCOL); - assert.strictEqual(parseInt(parsedURL.port, 10), constants.DEVICE_DEFAULT_PORT); - assert.strictEqual(opts.headers['x-f5-auth-token'], authToken); - assert.strictEqual(opts.headers['User-Agent'], constants.USER_AGENT); - - const mockRes = { statusCode: 200, statusMessage: 'mockMessage' }; - const mockBody = { text: uri }; - cb(null, mockRes, mockBody); - }; - - const opts = { - headers: { - 'x-f5-auth-token': authToken - }, - credentials: { - token: 'newToken', - username: 'username' - } - }; - return deviceUtil.makeDeviceRequest('1.1.1.1', uri, opts) - .then((body) => { - assert.strictEqual(body.text, uri); - Promise.resolve(); - }) - .catch(err => Promise.reject(err)); - }); - - it('should use token instead of username', () => { - const authToken = 'validToken'; - - request.get = (opts, cb) => { - assert.strictEqual(opts.headers['x-f5-auth-token'], authToken); - - const mockRes = { statusCode: 200, statusMessage: 'mockMessage' }; - const mockBody = { text: 'success' }; - cb(null, mockRes, mockBody); - }; - - const opts = { - credentials: { - token: authToken - } - }; - return deviceUtil.makeDeviceRequest('1.1.1.1', '/', opts) - .then((body) => { - assert.strictEqual(body.text, 'success'); - Promise.resolve(); - }) - .catch(err => Promise.reject(err)); - }); - - it('should corretly encode username for auth header', () => { - const username = 'username'; - const valid = `Basic ${Buffer.from(`${username}:`).toString('base64')}`; - - request.get = (opts, cb) => { - assert.strictEqual(opts.headers.Authorization, valid); - - const mockRes = { statusCode: 200, statusMessage: 'mockMessage' }; - const mockBody = { text: 'success' }; - cb(null, mockRes, mockBody); - }; - - const opts = { - credentials: { - username - } - }; - return deviceUtil.makeDeviceRequest('1.1.1.1', '/', opts) - .then((body) => { - assert.strictEqual(body.text, 'success'); - Promise.resolve(); - }) - .catch(err => Promise.reject(err)); - }); - - it('should execute shell command', () => { - const mockRes = { statusCode: 200, statusMessage: 'message' }; - const mockBody = { commandResult: 'somestring' }; - setupRequestMock(mockRes, mockBody); + }]); + const expected = { + version: '14.1.0', + buildInfo: '0.0.1' + }; + return assert.becomes( + deviceUtil.getDeviceVersion(constants.LOCAL_HOST), + expected + ); + }); - return deviceUtil.executeShellCommandOnDevice(constants.LOCAL_HOST, 'echo somestring') - .then((data) => { - assert.strictEqual(data, mockBody.commandResult); - return Promise.resolve(); - }) - .catch(err => Promise.reject(err)); + it('should fail on return device version', () => { + testUtil.mockEndpoints([{ + endpoint: '/mgmt/tm/sys/version', + code: 400, + response: {} + }]); + return assert.isRejected( + deviceUtil.getDeviceVersion(constants.LOCAL_HOST), + /getDeviceVersion:/ + ); + }); }); - it('should fail on execute shell command', () => { - const mockRes = { statusCode: 404, statusMessage: 'message' }; - const mockBody = { commandResult: 'somestring' }; - setupRequestMock(mockRes, mockBody); - - return deviceUtil.executeShellCommandOnDevice(constants.LOCAL_HOST, 'echo somestring') - .then(() => { - assert.fail('Should throw an error'); - }) - .catch((err) => { - if (err.code === 'ERR_ASSERTION') return Promise.reject(err); - if (/executeShellCommandOnDevice:/.test(err)) return Promise.resolve(); - assert.fail(err); - return Promise.reject(err); - }); - }); + describe('.makeDeviceRequest()', () => { + it('should preserve device\'s default port, protocol, HTTP method and etc.', () => { + testUtil.mockEndpoints( + [{ + endpoint: '/uri/something', + requestHeaders: { + 'x-f5-auth-token': 'authToken', + 'User-Agent': constants.USER_AGENT + }, + response: 'something' + }], + { + host: '1.1.1.1', + port: constants.DEVICE_DEFAULT_PORT, + proto: constants.DEVICE_DEFAULT_PROTOCOL + } + ); + const opts = { + headers: { + 'x-f5-auth-token': 'authToken' + }, + credentials: { + token: 'newToken', + username: 'username' + } + }; + return assert.becomes( + deviceUtil.makeDeviceRequest('1.1.1.1', '/uri/something', opts), + 'something' + ); + }); - it('should fail to get an auth token', () => { - const token = 'atoken'; - const mockRes = { statusCode: 404, statusMessage: 'message' }; - const mockBody = { token: { token } }; - setupRequestMock(mockRes, mockBody); + it('should use token instead of username', () => { + testUtil.mockEndpoints([{ + endpoint: '/', + requestHeaders: { + 'x-f5-auth-token': 'validToken' + }, + response: 'something' + }]); + const opts = { + credentials: { + token: 'validToken' + } + }; + return assert.becomes( + deviceUtil.makeDeviceRequest('localhost', '/', opts), + 'something' + ); + }); - return deviceUtil.getAuthToken('example.com', 'admin', 'password') - .then(() => { - assert.fail('Should throw an error'); - }) - .catch((err) => { - if (err.code === 'ERR_ASSERTION') return Promise.reject(err); - if (/requestAuthToken:/.test(err)) return Promise.resolve(); - assert.fail(err); - return Promise.reject(err); - }); + it('should correctly encode username for auth header', () => { + testUtil.mockEndpoints([{ + endpoint: '/', + requestHeaders: { + Authorization: `Basic ${Buffer.from('username:').toString('base64')}` + }, + response: 'something' + }]); + const opts = { + credentials: { + username: 'username' + } + }; + return assert.becomes( + deviceUtil.makeDeviceRequest('localhost', '/', opts), + 'something' + ); + }); }); - it('should get an auth token', () => { - const token = 'atoken'; - const mockRes = { statusCode: 200, statusMessage: 'message' }; - const mockBody = { token: { token } }; - setupRequestMock(mockRes, mockBody); - - return deviceUtil.getAuthToken('example.com', 'admin', 'password') - .then((data) => { - assert.strictEqual(data.token, token); - return Promise.resolve(); - }) - .catch(err => Promise.reject(err)); - }); + describe('.executeShellCommandOnDevice()', () => { + it('should execute shell command', () => { + testUtil.mockEndpoints([{ + endpoint: '/mgmt/tm/util/bash', + method: 'post', + request: { + command: 'run', + utilCmdArgs: '-c "echo something"' + }, + response: { + commandResult: 'something' + } + }]); + return assert.becomes( + deviceUtil.executeShellCommandOnDevice(constants.LOCAL_HOST, 'echo something'), + 'something' + ); + }); - it('should return null auth token for localhost', () => { - const expected = null; - return deviceUtil.getAuthToken('localhost') - .then((data) => { - assert.strictEqual(data.token, expected); - return Promise.resolve(); - }) - .catch(err => Promise.reject(err)); + it('should fail on execute shell command', () => { + testUtil.mockEndpoints([{ + endpoint: '/mgmt/tm/util/bash', + code: 400, + method: 'post', + request: { + command: 'run', + utilCmdArgs: '-c "echo something"' + } + }]); + return assert.isRejected( + deviceUtil.executeShellCommandOnDevice(constants.LOCAL_HOST, 'echo something'), + /executeShellCommandOnDevice:/ + ); + }); }); - it('should fail to get auth token when no username and/or no password', () => deviceUtil.getAuthToken('example.com') - .then(() => { - assert.fail('Should throw an error'); - }) - .catch((err) => { - if (err.code === 'ERR_ASSERTION') return Promise.reject(err); - if (/getAuthToken: Username/.test(err)) return Promise.resolve(); - assert.fail(err); - return Promise.reject(err); - }) - .then(() => deviceUtil.getAuthToken('example.com')) - .then(() => { - assert.fail('Should throw an error'); - }) - .catch((err) => { - if (err.code === 'ERR_ASSERTION') return Promise.reject(err); - if (/getAuthToken: Username/.test(err)) return Promise.resolve(); - assert.fail(err); - return Promise.reject(err); - })); - - it('should encrypt secret and retrieve it via REST API when software version is 14.0.0', () => { - const secret = 'asecret'; - const mockRes = { statusCode: 200, statusMessage: 'message' }; - const mockBody = { - secret, - entries: { - someKey: { - nestedStats: { - entries: { - version: { - description: '14.0.0' - }, - BuildInfo: { - description: '0.0.1' - } - } + describe('.getAuthToken()', () => { + it('should fail to get an auth token', () => { + testUtil.mockEndpoints( + [{ + endpoint: '/mgmt/shared/authn/login', + code: 404, + method: 'post', + request: { + username: 'username', + password: 'password', + loginProviderName: 'tmos' } + }], + { + host: 'example.com' } - } - }; - setupRequestMock(mockRes, mockBody); - - return deviceUtil.encryptSecret('foo') - .then((data) => { - assert.strictEqual(data, secret); - return Promise.resolve(); - }) - .catch(err => Promise.reject(err)); - }); + ); + return assert.isRejected( + deviceUtil.getAuthToken('example.com', 'username', 'password'), + /requestAuthToken:/ + ); + }); - it('should encrypt data that is 1k characters long', () => { - const secret = 'abcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabc' - + 'abcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabca' - + 'bcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabc' - + 'abcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcab' - + 'cabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcab' - + 'cabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcab' - + 'cabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcab' - + 'cabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcab' - + 'cabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcab' - + 'cabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcab' - + 'cabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcab' - + 'cabcabcabcabcabcabcabcabca'; - const mockRes = { statusCode: 200, statusMessage: 'message' }; - const mockBody = { - secret: 'encryptedData', - entries: { - someKey: { - nestedStats: { - entries: { - version: { - description: '14.0.0' - }, - BuildInfo: { - description: '0.0.1' - } + it('should get an auth token', () => { + testUtil.mockEndpoints( + [{ + endpoint: '/mgmt/shared/authn/login', + code: 200, + method: 'post', + request: { + username: 'username', + password: 'password', + loginProviderName: 'tmos' + }, + response: { + token: { + token: 'token' } } + }], + { + host: 'example.com' } - } - }; - setupRequestMock(mockRes, mockBody); + ); + return assert.becomes( + deviceUtil.getAuthToken('example.com', 'username', 'password'), + { token: 'token' } + ); + }); - return deviceUtil.encryptSecret(secret) - .then((data) => { - assert.strictEqual(data, 'encryptedData,encryptedData'); - return Promise.resolve(); - }) - .catch(err => Promise.reject(err)); + it('should return null auth token for localhost', () => assert.becomes( + deviceUtil.getAuthToken('localhost'), + { token: null } + )); + + it('should fail to get auth token when no username and/or no password', () => assert.isRejected( + deviceUtil.getAuthToken('example.com'), + /getAuthToken: Username/ + )); }); - it('should encrypt secret and retrieve it via REST API when software version is 15.0.0', () => { - const secret = 'asecret'; - const mockRes = { statusCode: 200, statusMessage: 'message' }; - const mockBody = { - secret, - entries: { - someKey: { - nestedStats: { - entries: { - version: { - description: '15.0.0' - }, - BuildInfo: { - description: '0.0.1' - } - } - } - } - } - }; - setupRequestMock(mockRes, mockBody); + describe('.encryptSecret()', () => { + beforeEach(() => { + sinon.stub(crypto, 'randomBytes').returns('test'); + deviceUtil.clearHostDeviceInfo(); + }); - return deviceUtil.encryptSecret('foo') - .then((data) => { - assert.strictEqual(data, secret); - return Promise.resolve(); - }) - .catch(err => Promise.reject(err)); - }); + it('should use cached device info on attempt to encrypt data', () => { + testUtil.mockEndpoints(deviceUtilTestsData.encryptSecret['encrypt-14.0.0']); + sinon.stub(deviceUtil, 'getDeviceType').resolves(constants.DEVICE_TYPE.BIG_IP); + return deviceUtil.gatherHostDeviceInfo() + .then(() => deviceUtil.encryptSecret('foo')) + .then((encryptedData) => { + assert.strictEqual(encryptedData, 'secret'); + }); + }); - it('should error during encrypt secret', () => { - const mockRes = { statusCode: 400, statusMessage: 'message' }; - setupRequestMock(mockRes, {}); + it('should encrypt secret and retrieve it via REST API when software version is 14.0.0', () => { + testUtil.mockEndpoints(deviceUtilTestsData.encryptSecret['encrypt-14.0.0']); + return assert.becomes( + deviceUtil.encryptSecret('foo', true), + 'secret' + ); + }); - return deviceUtil.encryptSecret('foo') - .then(() => { - assert.fail('Should throw an error'); - }) - .catch((err) => { - if (err.code === 'ERR_ASSERTION') return Promise.reject(err); - if (/Bad status code: 400 message/.test(err)) return Promise.resolve(); - assert.fail(err); - return Promise.reject(err); - }); - }); + it('should encrypt secret and retrieve it from device via TMSH when software version is 14.1.x', () => { + testUtil.mockEndpoints(deviceUtilTestsData.encryptSecret['encrypt-14.1.x']); + return assert.becomes( + deviceUtil.encryptSecret('foo', true), + 'secret' + ); + }); - it('should encrypt secret and retreive it from device via TMSH when software version is 14.1.x', () => { - const invalidSecret = { secret: 'invalidSecret' }; - const validSecret = 'secret'; - const tmshResp = { commandResult: `auth radius-server telemetry_delete_me {\n secret ${validSecret}\n}` }; - - const mockRes = { statusCode: 200, statusMessage: 'message' }; - const mockBody = { - invalidSecret, - entries: { - someKey: { - nestedStats: { - entries: { - version: { - description: '14.1.0' - }, - BuildInfo: { - description: '0.0.1' - } - } - } + it('should encrypt secret and retrieve it via REST API when software version is 15.0.0', () => { + testUtil.mockEndpoints(deviceUtilTestsData.encryptSecret['encrypt-15.0.0']); + return assert.becomes( + deviceUtil.encryptSecret('foo', true), + 'secret' + ); + }); + + it('should encrypt data that is 1k characters long', () => { + testUtil.mockEndpoints(deviceUtilTestsData.encryptSecret.encrypt1kSecret); + const secret = 'abcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabc' + + 'abcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabca' + + 'bcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabc' + + 'abcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcab' + + 'cabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcab' + + 'cabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcab' + + 'cabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcab' + + 'cabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcab' + + 'cabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcab' + + 'cabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcab' + + 'cabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcab' + + 'cabcabcabcabcabcabcabcabca'; + return assert.becomes( + deviceUtil.encryptSecret(secret, true), + 'secret,secret' + ); + }); + + it('should chunk large secrets and preserve newlines when encrypting secrets', () => { + const radiusRequests = []; + testUtil.mockEndpoints([{ + endpoint: '/mgmt/tm/ltm/auth/radius-server', + method: 'post', + request: (body) => { + radiusRequests.push(body.secret); + return true; + }, + response: { + secret: 'secret' + }, + options: { + times: 2 } - } - }; - const requestHandler = (opts, cb) => { - const parsedURL = parseURL(opts.uri); + }]); + testUtil.mockEndpoints(deviceUtilTestsData.encryptSecret.encrypt1kSecretWithNewLines); + // secret that is > 500 characters, with newlines + const largeSecret = 'largeSecret123\nlargeSecret123\nlargeSecret123\nlargeSecret123\nlargeSecret123\n' + + 'largeSecret123\nlargeSecret123\nlargeSecret123\nlargeSecret123\nlargeSecret123\nlargeSecret123\n' + + 'largeSecret123\nlargeSecret123\nlargeSecret123\nlargeSecret123\nlargeSecret123\nlargeSecret123\n' + + 'largeSecret123\nlargeSecret123\nlargeSecret123\nlargeSecret123\nlargeSecret123\nlargeSecret123\n' + + 'largeSecret123\nlargeSecret123\nlargeSecret123\nlargeSecret123\nlargeSecret123\nlargeSecret123\n' + + 'largeSecret123\nlargeSecret123\nlargeSecret123\nlargeSecret123\nlargeSecret123\nlargeSecret123\n' + + 'largeSecret123\nlargeSecret123\nlargeSecret123\nlargeSecret123\nlargeSecret123\nlargeSecret123\n' + + 'largeSecret123\nlargeSecret123\nlargeSecret123\nlargeSecret123\nlargeSecret123\nlargeSecret123\n' + + 'largeSecret123\nlargeSecret123\nlargeSecret123\nlargeSecret123\nlargeSecret123\nlargeSecret123\n' + + 'largeSecret123\nlargeSecret123\nlargeSecret123\nlargeSecret123\nlargeSecret123\nlargeSecret123\n'; + + return deviceUtil.encryptSecret(largeSecret, true) + .then(() => { + const requestSecret = radiusRequests[0]; + assert.strictEqual(radiusRequests.length, 2, 'largeSecret should be in 2 chunks'); + assert.strictEqual(requestSecret.length, 500, 'length of chunk should be 500'); + assert.ok(new RegExp(/\n/).test(requestSecret), 'newlines should be preserved'); + }); + }); - let body = mockBody; - if (parsedURL.pathname === '/mgmt/tm/util/bash') { - body = tmshResp; - } - cb(null, mockRes, body); - }; - request.post = requestHandler; - request.get = requestHandler; + it('should fail when unable to encrypt secret', () => { + testUtil.mockEndpoints(deviceUtilTestsData.encryptSecret.errorResponseExample); + return assert.isRejected( + deviceUtil.encryptSecret('foo', true), + /Bad status code: 400/ + ); + }); - return deviceUtil.encryptSecret('foo') - .then((data) => { - assert.strictEqual(data, validSecret); - return Promise.resolve(); - }) - .catch(err => Promise.reject(err)); + it('should fail when encrypted secret has comma', () => { + testUtil.mockEndpoints(deviceUtilTestsData.encryptSecret.errorWhenResponseHasComma); + return assert.isRejected( + deviceUtil.encryptSecret('foo', true), + /Encrypted data should not have a comma in it/ + ); + }); }); - it('should decrypt secret', () => { - const secret = 'asecret'; - childProcess.execFile = (cmd, args, cb) => { cb(null, secret, null); }; + describe('.decryptSecret()', () => { + it('should decrypt secret', () => { + sinon.stub(childProcess, 'execFile').callsFake((cmd, args, cb) => { + cb(null, 'secret', null); + }); + return assert.becomes( + deviceUtil.decryptSecret('foo'), + 'secret' + ); + }); - return deviceUtil.decryptSecret('foo') - .then((data) => { - assert.strictEqual(data, secret); - return Promise.resolve(); - }) - .catch(err => Promise.reject(err)); + it('should fail when unable to decrypt secret', () => { + sinon.stub(childProcess, 'execFile').callsFake((cmd, args, cb) => { + cb(new Error('decrypt error'), null, 'stderr'); + }); + return assert.isRejected( + deviceUtil.decryptSecret('foo'), + /decryptSecret exec error.*decrypt error.*stderr/ + ); + }); }); - it('should decrypt all secrets', () => { - const secret = 'asecret'; - childProcess.execFile = (cmd, args, cb) => { cb(null, secret, null); }; - process.env.MY_SECRET_TEST_VAR = secret; - - const obj = { - My_Consumer: { - class: 'Consumer', - passphrase: { - class: 'Secret', - cipherText: 'foo' - } - }, - My_Consumer2: { - class: 'Consumer', - passphrase: { - class: 'Secret', - environmentVar: 'MY_SECRET_TEST_VAR' - } - }, - My_Consumer3: { - class: 'Consumer', - passphrase: { - class: 'Secret', - environmentVar: 'VAR_THAT_DOES_NOT_EXIST' - } - }, - My_Consumer4: { - class: 'Consumer', - passphrase: { - class: 'Secret', - someUnknownKey: 'foo' + describe('.decryptAllSecrets()', () => { + it('should decrypt all secrets', () => { + sinon.stub(childProcess, 'execFile').callsFake((cmd, args, cb) => { + cb(null, 'secret', null); + }); + sinon.stub(process, 'env').value({ MY_SECRET_TEST_VAR: 'envSecret' }); + + const declaration = { + My_Consumer: { + class: 'Consumer', + passphrase: { + class: 'Secret', + cipherText: 'foo' + } + }, + My_Consumer2: { + class: 'Consumer', + passphrase: { + class: 'Secret', + environmentVar: 'MY_SECRET_TEST_VAR' + } + }, + My_Consumer3: { + class: 'Consumer', + passphrase: { + class: 'Secret', + environmentVar: 'VAR_THAT_DOES_NOT_EXIST' + } + }, + My_Consumer4: { + class: 'Consumer', + passphrase: { + class: 'Secret', + someUnknownKey: 'foo' + } + }, + My_Consumer5: { + class: 'Consumer', + otherkey: { + class: 'Secret', + cipherText: 'foo' + } } - }, - My_Consumer5: { - class: 'Consumer', - otherkey: { - class: 'Secret', - cipherText: 'foo' + }; + const expected = { + My_Consumer: { + class: 'Consumer', + passphrase: 'secret' + }, + My_Consumer2: { + class: 'Consumer', + passphrase: 'envSecret' + }, + My_Consumer3: { + class: 'Consumer', + passphrase: null + }, + My_Consumer4: { + class: 'Consumer', + passphrase: null + }, + My_Consumer5: { + class: 'Consumer', + otherkey: 'secret' } - } - }; - const decryptedObj = { - My_Consumer: { - class: 'Consumer', - passphrase: secret - }, - My_Consumer2: { - class: 'Consumer', - passphrase: secret - }, - My_Consumer3: { - class: 'Consumer', - passphrase: null - }, - My_Consumer4: { - class: 'Consumer', - passphrase: null - }, - My_Consumer5: { - class: 'Consumer', - otherkey: secret - } - }; - - return deviceUtil.decryptAllSecrets(obj) - .then((data) => { - assert.deepEqual(data, decryptedObj); - return Promise.resolve(); - }) - .catch(err => Promise.reject(err)); - }); - - it('should fail when subPath passed without parition to transformTMOSobjectName', () => { - try { - deviceUtil.transformTMOSobjectName('', '', 'subPath'); - assert.fail('Should throw an error'); - } catch (err) { - if (!/transformTMOSobjectName:/.test(err)) assert.fail(err); - } + }; + return assert.becomes( + deviceUtil.decryptAllSecrets(declaration), + expected + ); + }); }); - it('should correctly transform TMOS object nane', () => { - assert.strictEqual(deviceUtil.transformTMOSobjectName('partition', 'name', 'subPath'), '~partition~subPath~name'); - assert.strictEqual(deviceUtil.transformTMOSobjectName('partition', '/name/name', 'subPath'), '~partition~subPath~~name~name'); - assert.strictEqual(deviceUtil.transformTMOSobjectName('partition', '/name/name', 'subPath'), '~partition~subPath~~name~name'); - assert.strictEqual(deviceUtil.transformTMOSobjectName('', 'name'), 'name'); + describe('.transformTMOSobjectName()', () => { + it('should fail when subPath passed without partition', () => assert.throws( + () => deviceUtil.transformTMOSobjectName('', '', 'subPath'), + /transformTMOSobjectName:/ + )); + + it('should correctly transform TMOS object name', () => { + assert.strictEqual(deviceUtil.transformTMOSobjectName('partition', 'name', 'subPath'), '~partition~subPath~name'); + assert.strictEqual(deviceUtil.transformTMOSobjectName('partition', '/name/name', 'subPath'), '~partition~subPath~~name~name'); + assert.strictEqual(deviceUtil.transformTMOSobjectName('partition', '/name/name', 'subPath'), '~partition~subPath~~name~name'); + assert.strictEqual(deviceUtil.transformTMOSobjectName('', 'name'), 'name'); + }); }); }); - // purpose: validate util (DeviceAsyncCLI) describe('Device Util (DeviceAsyncCLI)', () => { - let deviceUtil; - let request; - - before(() => { - deviceUtil = require('../../src/lib/deviceUtil.js'); - request = require('request'); - }); - after(() => { - Object.keys(require.cache).forEach((key) => { - delete require.cache[key]; - }); + afterEach(() => { + sinon.restore(); }); + let parseURL; + if (process.versions.node.startsWith('4.')) { + parseURL = urllib.parse; + } else { + parseURL = url => new urllib.URL(url); + } + const testScriptName = 'testScriptName'; const testScriptCode = 'testScriptCode'; const testTaskID = 'taskID'; @@ -1230,7 +1253,7 @@ describe('Device Util (DeviceAsyncCLI)', () => { const responder = mockedResponse(uris, options); mockedHTTPmethods.forEach((method) => { - request[method] = responder; + sinon.stub(request, method).callsFake(responder); }); return runMethodTesting(testParams, options); }); @@ -1250,7 +1273,7 @@ describe('Device Util (DeviceAsyncCLI)', () => { cb(null, response, responseBody); }; mockedHTTPmethods.forEach((method) => { - request[method] = responder; + sinon.stub(request, method).callsFake(responder); }); // ideally all testSets should be covered @@ -1274,7 +1297,7 @@ describe('Device Util (DeviceAsyncCLI)', () => { cb(null, response, responseBody); }; mockedHTTPmethods.forEach((method) => { - request[method] = responder; + sinon.stub(request, method).callsFake(responder); }); // ideally all testSets should be covered @@ -1298,7 +1321,7 @@ describe('Device Util (DeviceAsyncCLI)', () => { cb(null, response, responseBody); }; mockedHTTPmethods.forEach((method) => { - request[method] = responder; + sinon.stub(request, method).callsFake(responder); }); // ideally all testSets should be covered @@ -1324,22 +1347,14 @@ describe('Device Util (DeviceAsyncCLI)', () => { cb(null, response, responseBody); }; mockedHTTPmethods.forEach((method) => { - request[method] = responder; + sinon.stub(request, method).callsFake(responder); }); - let err; + // ideally all testSets should be covered const dacli = new deviceUtil.DeviceAsyncCLI('localhost'); dacli.scriptName = testScriptName; dacli.retryDelay = 0; - return dacli.execute('command') - .catch((e) => { - err = e; - }) - .then(() => { - if (!err) { - assert.fail('Error expected'); - } - }); + return assert.isRejected(dacli.execute('command')); }); it('should parse init params correctly', () => { @@ -1356,18 +1371,18 @@ describe('Device Util (DeviceAsyncCLI)', () => { assert.strictEqual(dacli.options.opts, 'opts'); dacli = new deviceUtil.DeviceAsyncCLI(); - assert.deepEqual(dacli.options, { connection: {}, credentials: {} }); + assert.deepStrictEqual(dacli.options, { connection: {}, credentials: {} }); dacli = new deviceUtil.DeviceAsyncCLI({}); - assert.deepEqual(dacli.options, { connection: {}, credentials: {} }); + assert.deepStrictEqual(dacli.options, { connection: {}, credentials: {} }); dacli = new deviceUtil.DeviceAsyncCLI({ something: 'something' }); - assert.deepEqual(dacli.options, { connection: {}, credentials: {}, something: 'something' }); + assert.deepStrictEqual(dacli.options, { connection: {}, credentials: {}, something: 'something' }); dacli = new deviceUtil.DeviceAsyncCLI({ connection: { port: 80 }, credentials: { token: 'token' } }); - assert.deepEqual(dacli.options, { connection: { port: 80 }, credentials: { token: 'token' } }); + assert.deepStrictEqual(dacli.options, { connection: { port: 80 }, credentials: { token: 'token' } }); dacli = new deviceUtil.DeviceAsyncCLI({ connection: { port: 80 } }); - assert.deepEqual(dacli.options, { connection: { port: 80 }, credentials: {} }); + assert.deepStrictEqual(dacli.options, { connection: { port: 80 }, credentials: {} }); }); }); diff --git a/test/unit/deviceUtilTestsData.js b/test/unit/deviceUtilTestsData.js new file mode 100644 index 00000000..3871705d --- /dev/null +++ b/test/unit/deviceUtilTestsData.js @@ -0,0 +1,277 @@ +/* + * Copyright 2020. F5 Networks, Inc. See End User License Agreement ('EULA') for + * license terms. Notwithstanding anything to the contrary in the EULA, Licensee + * may copy and modify this software product for its internal business purposes. + * Further, Licensee may upload, publish and distribute the modified version of + * the software product on devcentral.f5.com. + */ + +'use strict'; + +function encryptEncode(data) { + return data.toString('base64').replace(/[+]/g, '-').replace(/\x2f/g, '_'); +} + +module.exports = { + getDeviceType: { + correctData: 'Edition: Final\nProduct: BIG-IP\nVersion: 14.1.0\n', + incorrectData: 'Edition: Final\nProduct: BIG-IQ\nVersion: 14.1.0\n' + }, + encryptSecret: { + 'encrypt-14.0.0': [ + { + endpoint: '/mgmt/tm/ltm/auth/radius-server', + method: 'post', + request: { + name: `telemetry_delete_me_${encryptEncode('test')}`, + secret: 'foo', + server: 'foo' + }, + response: { + secret: 'secret' + } + }, + { + endpoint: '/mgmt/tm/sys/version', + response: { + entries: { + someKey: { + nestedStats: { + entries: { + version: { + description: '14.0.0' + }, + BuildInfo: { + description: '0.0.1' + } + } + } + } + } + } + }, + { + endpoint: `/mgmt/tm/ltm/auth/radius-server/telemetry_delete_me_${encryptEncode('test')}`, + method: 'delete' + } + ], + 'encrypt-14.1.x': [ + { + endpoint: '/mgmt/tm/ltm/auth/radius-server', + method: 'post', + request: { + name: `telemetry_delete_me_${encryptEncode('test')}`, + secret: 'foo', + server: 'foo' + }, + response: { + secret: 'secret' + } + }, + { + endpoint: '/mgmt/tm/sys/version', + response: { + entries: { + someKey: { + nestedStats: { + entries: { + version: { + description: '14.1.0' + }, + BuildInfo: { + description: '0.0.1' + } + } + } + } + } + } + }, + { + endpoint: `/mgmt/tm/ltm/auth/radius-server/telemetry_delete_me_${encryptEncode('test')}`, + method: 'delete' + }, + { + endpoint: '/mgmt/tm/util/bash', + method: 'post', + request: { + command: 'run', + utilCmdArgs: `-c "tmsh -a list auth radius-server telemetry_delete_me_${encryptEncode('test')} secret"` + }, + response: { + commandResult: 'auth radius-server telemetry_delete_me {\n secret secret\n}' + } + } + ], + 'encrypt-15.0.0': [ + { + endpoint: '/mgmt/tm/ltm/auth/radius-server', + method: 'post', + request: { + name: `telemetry_delete_me_${encryptEncode('test')}`, + secret: 'foo', + server: 'foo' + }, + response: { + secret: 'secret' + } + }, + { + endpoint: '/mgmt/tm/sys/version', + response: { + entries: { + someKey: { + nestedStats: { + entries: { + version: { + description: '15.0.0' + }, + BuildInfo: { + description: '0.0.1' + } + } + } + } + } + } + }, + { + endpoint: `/mgmt/tm/ltm/auth/radius-server/telemetry_delete_me_${encryptEncode('test')}`, + method: 'delete' + } + ], + errorResponseExample: [ + { + endpoint: '/mgmt/tm/ltm/auth/radius-server', + method: 'post', + code: 400 + }, + { + endpoint: `/mgmt/tm/ltm/auth/radius-server/telemetry_delete_me_${encryptEncode('test')}`, + method: 'delete' + }, + { + endpoint: '/mgmt/tm/sys/version', + response: { + entries: { + someKey: { + nestedStats: { + entries: { + version: { + description: '15.0.0' + }, + BuildInfo: { + description: '0.0.1' + } + } + } + } + } + } + } + ], + encrypt1kSecret: [ + { + endpoint: '/mgmt/tm/ltm/auth/radius-server', + method: 'post', + response: { + secret: 'secret' + }, + options: { + times: 2 + } + }, + { + endpoint: '/mgmt/tm/sys/version', + response: { + entries: { + someKey: { + nestedStats: { + entries: { + version: { + description: '14.0.0' + }, + BuildInfo: { + description: '0.0.1' + } + } + } + } + } + } + }, + { + endpoint: `/mgmt/tm/ltm/auth/radius-server/telemetry_delete_me_${encryptEncode('test')}`, + method: 'delete', + options: { + times: 2 + } + } + ], + encrypt1kSecretWithNewLines: [ + { + endpoint: '/mgmt/tm/sys/version', + response: { + entries: { + someKey: { + nestedStats: { + entries: { + version: { + description: '14.0.0' + }, + BuildInfo: { + description: '0.0.1' + } + } + } + } + } + } + }, + { + endpoint: `/mgmt/tm/ltm/auth/radius-server/telemetry_delete_me_${encryptEncode('test')}`, + method: 'delete', + options: { + times: 2 + } + } + ], + errorWhenResponseHasComma: [ + { + endpoint: '/mgmt/tm/ltm/auth/radius-server', + method: 'post', + request: { + name: `telemetry_delete_me_${encryptEncode('test')}`, + secret: 'foo', + server: 'foo' + }, + response: { + secret: 'secret,secret' + } + }, + { + endpoint: '/mgmt/tm/sys/version', + response: { + entries: { + someKey: { + nestedStats: { + entries: { + version: { + description: '14.0.0' + }, + BuildInfo: { + description: '0.0.1' + } + } + } + } + } + } + }, + { + endpoint: `/mgmt/tm/ltm/auth/radius-server/telemetry_delete_me_${encryptEncode('test')}`, + method: 'delete' + } + ] + } +}; diff --git a/test/unit/endpointLoaderTests.js b/test/unit/endpointLoaderTests.js index 6a03b041..3d84a7e6 100644 --- a/test/unit/endpointLoaderTests.js +++ b/test/unit/endpointLoaderTests.js @@ -8,97 +8,103 @@ 'use strict'; -const sinon = require('sinon'); +/* eslint-disable import/order */ + +require('./shared/restoreCache')(); + const chai = require('chai'); const chaiAsPromised = require('chai-as-promised'); +const nock = require('nock'); +const sinon = require('sinon'); + +const EndpointLoader = require('../../src/lib/endpointLoader'); +const deviceUtil = require('../../src/lib/deviceUtil'); +const endpointLoaderTestsData = require('./endpointLoaderTestsData'); +const testUtil = require('./shared/util'); chai.use(chaiAsPromised); const assert = chai.assert; -const EndpointLoader = require('../../src/lib/endpointLoader.js'); -const deviceUtil = require('../../src/lib/deviceUtil.js'); - describe('Endpoint Loader', () => { + let eLoader; + + beforeEach(() => { + eLoader = new EndpointLoader(); + }); + afterEach(() => { + testUtil.checkNockActiveMocks(nock, assert); + nock.cleanAll(); sinon.restore(); }); - it('should set defaults', () => { - const eLoader = new EndpointLoader(); - assert.strictEqual(eLoader.host, 'localhost'); - assert.deepStrictEqual(eLoader.options, { credentials: {}, connection: {} }); - assert.strictEqual(eLoader.endpoints, null); - assert.deepStrictEqual(eLoader.cachedResponse, {}); - }); + describe('constructor', () => { + it('should set defaults', () => { + assert.strictEqual(eLoader.host, 'localhost'); + assert.deepStrictEqual(eLoader.options.credentials, {}); + assert.deepStrictEqual(eLoader.options.connection, {}); + assert.strictEqual(eLoader.endpoints, null); + assert.deepStrictEqual(eLoader.cachedResponse, {}); + }); - it('should set host when argument is string', () => { - const eLoader = new EndpointLoader('10.10.0.1'); - assert.strictEqual(eLoader.host, '10.10.0.1'); - }); + it('should set host when argument is string', () => { + eLoader = new EndpointLoader('10.10.0.1'); + assert.strictEqual(eLoader.host, '10.10.0.1'); + }); - it('should set options when argument is object', () => { - const eLoader = new EndpointLoader({ foo: 'bar' }); - assert.deepStrictEqual(eLoader.options, { - foo: 'bar', - credentials: {}, - connection: {} + it('should set options when argument is object', () => { + eLoader = new EndpointLoader({ foo: 'bar' }); + assert.strictEqual(eLoader.options.foo, 'bar'); + assert.deepStrictEqual(eLoader.options.credentials, {}); + assert.deepStrictEqual(eLoader.options.connection, {}); }); }); - describe('setEndpoints', () => { + describe('.setEndpoints()', () => { it('should set endpoints', () => { - const eLoader = new EndpointLoader(); const expected = { foo: { name: 'foo', body: 'bar' }, '/hello/world': { - endpoint: '/hello/world', + path: '/hello/world', body: 'Hello World!' } }; - eLoader.setEndpoints([ { name: 'foo', body: 'bar' }, { - endpoint: '/hello/world', + path: '/hello/world', body: 'Hello World!' } ]); - assert.deepStrictEqual(eLoader.endpoints, expected); }); it('should overwrite endpoints', () => { - const eLoader = new EndpointLoader(); const expected = { bar: { name: 'bar' } }; - eLoader.endpoints = { foo: {} }; - eLoader.setEndpoints([ { name: 'bar' } ]); - assert.deepStrictEqual(eLoader.endpoints, expected); }); }); - describe('setEndpoints', () => { + describe('.auth()', () => { it('should not change token if already exists and resolve', () => { - const eLoader = new EndpointLoader({ credentials: { token: '56789' } }); - sinon.stub(deviceUtil, 'getAuthToken').resolves({ token: '12345' }); - + eLoader = new EndpointLoader({ credentials: { token: '56789' } }); return eLoader.auth() .then(() => { assert.strictEqual( @@ -110,10 +116,7 @@ describe('Endpoint Loader', () => { }); it('should save the token and resolve', () => { - const eLoader = new EndpointLoader(); - sinon.stub(deviceUtil, 'getAuthToken').resolves({ token: '12345' }); - return eLoader.auth() .then(() => { assert.strictEqual( @@ -125,16 +128,12 @@ describe('Endpoint Loader', () => { }); it('should reject with error if getAuthToken fails', () => { - const eLoader = new EndpointLoader(); - const error = new Error('getAuthToken: Username and password required'); - - sinon.stub(deviceUtil, 'getAuthToken').rejects(error); - - assert.isRejected(eLoader.auth(error)); + sinon.stub(deviceUtil, 'getAuthToken').rejects(new Error('some error')); + return assert.isRejected(eLoader.auth(), /some error/); }); it('should pass connection and credential information to getAuthToken', () => { - const eLoader = new EndpointLoader({ + eLoader = new EndpointLoader({ credentials: { username: 'admin', passphrase: '12345' @@ -146,7 +145,6 @@ describe('Endpoint Loader', () => { }); let getAuthTokenArgs; - sinon.stub(deviceUtil, 'getAuthToken').callsFake((host, username, password, options) => { getAuthTokenArgs = { host, username, password, options @@ -169,50 +167,133 @@ describe('Endpoint Loader', () => { }); }); - describe('loadEndpoint', () => { + describe('.replaceBodyVars()', () => { + it('should replace string in body (object)', () => { + const body = { + command: 'run', + utilCmdArgs: '-c "echo $replaceMe"' + }; + return assert.deepStrictEqual( + eLoader.replaceBodyVars(body, { '\\$replaceMe': 'Hello World' }), + { + command: 'run', + utilCmdArgs: '-c "echo Hello World"' + } + ); + }); + + it('should replace string in body (string)', () => assert.deepStrictEqual( + eLoader.replaceBodyVars('$replaceMe', { '\\$replaceMe': 'Hello World' }), + 'Hello World' + )); + }); + + describe('.getURIPath()', () => { + it('should get path from URI', () => { + assert.strictEqual( + eLoader.getURIPath('https://localhost/path/to/something?arg=value'), + '/path/to/something' + ); + }); + }); + + describe('.getData()', () => { + it('should use POST when sending body', () => { + // resolves with httOptions + sinon.stub(deviceUtil, 'makeDeviceRequest').resolvesArg(2); + return assert.becomes( + eLoader.getData('/uri', { body: 'body' }), + { + name: '/uri', + data: { + body: 'body', + method: 'POST', + credentials: { + username: undefined, + token: undefined + } + } + } + ); + }); + + it('should retry request when failed', () => { + const requestStub = sinon.stub(deviceUtil, 'makeDeviceRequest'); + requestStub.onFirstCall().rejects(new Error('some error')); + requestStub.onSecondCall().resolves(Promise.resolve({ key: 'value' })); + return eLoader.getData('/uri') + .then((data) => { + assert.ok(requestStub.calledTwice, 'should re-try request on fail'); + assert.deepStrictEqual( + data, + { + name: '/uri', + data: { key: 'value' } + } + ); + }); + }); + + it('should build url using endpointFields', () => { + // resolves with fullUri + sinon.stub(deviceUtil, 'makeDeviceRequest').resolvesArg(1); + return eLoader.getData('/uri', { endpointFields: ['field2', 'field1', 'field3'] }) + .then((data) => { + const fields = data.data.split('?')[1].split('=')[1].split(',').sort(); + assert.deepStrictEqual(fields, ['field1', 'field2', 'field3']); + }); + }); + }); + + describe('.loadEndpoint()', () => { it('should error if endpoint is not defined', () => { - const eLoader = new EndpointLoader(); eLoader.endpoints = {}; + return assert.isRejected( + eLoader.loadEndpoint('badEndpoint'), + /Endpoint not defined in file: badEndpoint/ + ); + }); - return eLoader.loadEndpoint('badEndpoint', null) - .catch(err => assert.strictEqual(err.message, 'Endpoint not defined in file: badEndpoint')); + it('should fail when unable to get data', () => { + sinon.stub(eLoader, 'getAndExpandData').rejects(new Error('some error')); + eLoader.setEndpoints([{ name: 'path' }]); + return assert.isRejected( + eLoader.loadEndpoint('path'), + /some error/ + ); }); - it('should replace strings in endpoint body if replaceStrings option is provided', () => { - const eLoader = new EndpointLoader(); - const body = { - command: 'run', - utilCmdArgs: '-c "echo Hello World"' + it('should keep endpoint untouched when need to replace keys in body', () => { + sinon.stub(eLoader, 'getAndExpandData').resolvesArg(0); + const expectedEndpointObj = { + path: '/mgmt/tm/util/bash', + body: { + command: 'run', + utilCmdArgs: '-c "echo Hello World"' + } }; - - eLoader.endpoints = { + const endpoints = { bash: { - endpoint: '/mgmt/tm/util/bash', + path: '/mgmt/tm/util/bash', body: { command: 'run', utilCmdArgs: '-c "echo $replaceMe"' } } }; - - sinon.stub(eLoader, '_getData').resolvesArg(1); - + eLoader.endpoints = testUtil.deepCopy(endpoints); return eLoader.loadEndpoint('bash', { replaceStrings: { '\\$replaceMe': 'Hello World' } }) .then((data) => { - assert.deepStrictEqual(data, { name: undefined, body, endpointFields: undefined }); - }) - .catch(() => assert.fail()); + assert.deepStrictEqual(data, expectedEndpointObj); + // verify that original endpoint not changed + assert.deepStrictEqual(eLoader.endpoints, endpoints); + }); }); it('should reply with cached response', () => { - const eLoader = new EndpointLoader(); - - eLoader.endpoints = { bash: { endpoint: '/mgmt/tm/util/bash' } }; + eLoader.endpoints = { bash: { path: '/mgmt/tm/util/bash' } }; eLoader.cachedResponse = { bash: 'Foo Bar' }; - - sinon.stub(deviceUtil, 'makeDeviceRequest').resolves('New Data'); - - return eLoader.loadEndpoint('bash', null) + return eLoader.loadEndpoint('bash') .then((data) => { assert.deepStrictEqual( data, @@ -222,28 +303,24 @@ describe('Endpoint Loader', () => { assert.deepStrictEqual(eLoader.cachedResponse.bash, 'Foo Bar', 'Should not have updated cache'); - }) - .catch(() => assert.fail()); + }); }); it('should invalidate cached response if ignoreCached is set', () => { - const eLoader = new EndpointLoader(); const expected = { name: '/mgmt/tm/util/bash', data: 'New Data' }; + sinon.stub(eLoader, 'getAndExpandData').resolves(expected); eLoader.endpoints = { bash: { - endpoint: '/mgmt/tm/util/bash', + path: '/mgmt/tm/util/bash', ignoreCached: true } }; eLoader.cachedResponse = { bash: 'Foo Bar' }; - - sinon.stub(deviceUtil, 'makeDeviceRequest').resolves('New Data'); - - return eLoader.loadEndpoint('bash', null) + return eLoader.loadEndpoint('bash') .then((data) => { assert.deepStrictEqual( data, @@ -253,134 +330,52 @@ describe('Endpoint Loader', () => { assert.deepStrictEqual(eLoader.cachedResponse.bash, expected, 'Should have updated cache'); - }) - .catch(() => assert.fail()); - }); - - describe('expandReferences', () => { - beforeEach(() => { - sinon.stub(deviceUtil, 'makeDeviceRequest').callsFake((host, uri) => { - let data; - if (uri.endsWith('anySuffix')) { - data = { - anySuffixUri: uri, - anySuffix: 'abcd12345' - }; - } else if (uri.endsWith('stats')) { - data = { - uriStats: uri, - stat: 12345 - }; - } else if (uri.includes('members')) { - data = { - uriConfig: uri, - config: 'abcd' - }; - } else { - data = { - items: [ - { - nonRefProp: 'some1', - membersReference: { link: 'members/foo' } - }, - { - nonRefProp: 'some2', - membersReference: { link: 'members/bar' } - } - ] - }; - } - return Promise.resolve(data); }); - }); - - it('should handle endpointSuffix', () => { - const expected = { - data: { - items: [ - { - nonRefProp: 'some1', - membersReference: { - anySuffixUri: 'members/foo/anySuffix', - anySuffix: 'abcd12345' - } - }, - { - nonRefProp: 'some2', - membersReference: { - anySuffixUri: 'members/bar/anySuffix', - anySuffix: 'abcd12345' - } - } - ] - }, - name: '/mgmt/tm/ltm/pool' - }; - - const eLoader = new EndpointLoader(); - eLoader.endpoints = { - pools: { - endpoint: '/mgmt/tm/ltm/pool', - expandReferences: { - membersReference: { endpointSuffix: '/anySuffix' } - } - } - }; + }); - return eLoader.loadEndpoint('pools', null) - .then((data) => { - assert.deepEqual( - data, - expected, - 'Updated response should have returned in callback' - ); - }); - }); + it('should load updated data when cache is empty/erased', () => { + const expected = { + name: '/mgmt/tm/util/bash', + data: 'New Data' + }; + sinon.stub(eLoader, 'getAndExpandData').resolves(expected); - it('should handle includeStats', () => { - const expected = { - data: { - items: [ - { - nonRefProp: 'some1', - membersReference: { - config: 'abcd', - uriConfig: 'members/foo', - stat: 12345, - uriStats: 'members/foo/stats' - } - }, - { - nonRefProp: 'some2', - membersReference: { - config: 'abcd', - uriConfig: 'members/bar', - stat: 12345, - uriStats: 'members/bar/stats' - } - } - ] - }, - name: '/mgmt/tm/ltm/pool' - }; - const eLoader = new EndpointLoader(); - eLoader.endpoints = { - pools: { - endpoint: '/mgmt/tm/ltm/pool', - expandReferences: { - membersReference: { includeStats: true } - } - } - }; + eLoader.endpoints = { + bash: { + path: '/mgmt/tm/util/bash', + ignoreCached: true + } + }; + eLoader.cachedResponse = { bash: 'Foo Bar' }; + eLoader.eraseCache(); + return eLoader.loadEndpoint('bash') + .then((data) => { + assert.deepStrictEqual( + data, + expected, + 'Updated response should have returned in callback' + ); + assert.deepStrictEqual(eLoader.cachedResponse.bash, + expected, + 'Should have updated cache'); + }); + }); + }); - return eLoader.loadEndpoint('pools', null) - .then((data) => { - assert.deepEqual( - data, - expected, - 'Updated response should have returned in callback' - ); - }); + describe('.getAndExpandData()', () => { + const checkResponse = (endpointMock, response) => { + if (!response.kind) { + throw new Error(`Endpoint '${endpointMock.path}' has no property 'kind' in response`); + } + }; + + endpointLoaderTestsData.getAndExpandData.forEach((testConf) => { + testUtil.getCallableIt(testConf)(testConf.name, () => { + testUtil.mockEndpoints(testConf.endpoints, { responseChecker: checkResponse }); + return assert.becomes( + eLoader.getAndExpandData(testConf.endpointObj), + testConf.expectedData + ); }); }); }); diff --git a/test/unit/endpointLoaderTestsData.js b/test/unit/endpointLoaderTestsData.js new file mode 100644 index 00000000..6e461b5a --- /dev/null +++ b/test/unit/endpointLoaderTestsData.js @@ -0,0 +1,649 @@ +/* + * Copyright 2020. F5 Networks, Inc. See End User License Agreement ('EULA') for + * license terms. Notwithstanding anything to the contrary in the EULA, Licensee + * may copy and modify this software product for its internal business purposes. + * Further, Licensee may upload, publish and distribute the modified version of + * the software product on devcentral.f5.com. + */ + +'use strict'; + +/** + * NOTE: DO NOT REMOVE 'kind' AND 'selfLink' PROPERTIES FROM RESPONSE's TOP LEVEL + */ +/** + * TODO: update/remove 'options: { times: XXXX }' when EndpointLoader's cache will be fixed + */ + +module.exports = { + getAndExpandData: [ + { + name: 'should get data empty data', + endpointObj: { + path: '/endpoint' + }, + endpoints: [ + { + endpoint: '/endpoint', + response: { + kind: 'endpoint:state', + selfLink: 'https://localhost/endpoint?ver=X.X.X' + } + } + ], + expectedData: { + name: '/endpoint', + data: { + kind: 'endpoint:state', + selfLink: 'https://localhost/endpoint?ver=X.X.X' + } + } + }, + { + name: 'should fetch just the data', + endpointObj: { + path: '/endpoint' + }, + endpoints: [ + { + endpoint: '/endpoint', + response: { + kind: 'endpoint:state', + selfLink: 'https://localhost/endpoint?ver=X.X.X', + items: [ + { + name: 'object1', + selfLink: 'https://localhost/endpoint/object1?ver=X.X.X', + key: 'value' + } + ] + } + } + ], + expectedData: { + name: '/endpoint', + data: { + kind: 'endpoint:state', + selfLink: 'https://localhost/endpoint?ver=X.X.X', + items: [ + { + name: 'object1', + selfLink: 'https://localhost/endpoint/object1?ver=X.X.X', + key: 'value' + } + ] + } + } + }, + { + name: 'should fetch data with stats when includeStats specified', + endpointObj: { + path: '/endpoint', + includeStats: true + }, + endpoints: [ + { + endpoint: '/endpoint', + response: { + kind: 'endpoint:state', + selfLink: 'https://localhost/endpoint?ver=X.X.X', + items: [ + { + name: 'object1', + selfLink: 'https://localhost/endpoint/object1?ver=X.X.X', + key: 'value' + } + ] + } + }, + { + endpoint: '/endpoint/object1/stats', + response: { + kind: 'endpoint:stats', + selfLink: 'https://localhost/endpoint/object1/stats?ver=X.X.X', + entries: { + 'https://localhost/endpoint/object2/stats': { + nestedStats: { + kind: 'endpoint:stats', + selfLink: 'https://localhost/endpoint/object1/stats?ver=X.X.X', + statKey: 'statValue' + } + } + } + } + } + ], + expectedData: { + name: '/endpoint', + data: { + kind: 'endpoint:state', + selfLink: 'https://localhost/endpoint?ver=X.X.X', + items: [ + { + name: 'object1', + selfLink: 'https://localhost/endpoint/object1?ver=X.X.X', + key: 'value', + kind: 'endpoint:stats', + entries: { + 'https://localhost/endpoint/object2/stats': { + nestedStats: { + kind: 'endpoint:stats', + selfLink: 'https://localhost/endpoint/object1/stats?ver=X.X.X', + statKey: 'statValue' + } + } + } + } + ] + } + } + }, + { + name: 'should expand reference', + endpointObj: { + path: '/endpoint', + expandReferences: { someRef: {} } + }, + endpoints: [ + { + endpoint: '/endpoint', + response: { + kind: 'endpoint:state', + selfLink: 'https://localhost/endpoint?ver=X.X.X', + items: [ + { + name: 'object1', + selfLink: 'https://localhost/endpoint/object1?ver=X.X.X', + key: 'value', + someRef: { + link: 'https://localhost/anotherEndpoint/refObject?ver=X.X.X' + } + } + ] + } + }, + { + endpoint: '/anotherEndpoint/refObject', + response: { + kind: 'anotherEndpoint:state', + selfLink: 'https://localhost/anotherEndpoint/refObject?ver=X.X.X', + someKey: 'someValue', + items: [ + { + nestedObjectName: 'name' + } + ] + } + } + ], + expectedData: { + name: '/endpoint', + data: { + kind: 'endpoint:state', + selfLink: 'https://localhost/endpoint?ver=X.X.X', + items: [ + { + name: 'object1', + selfLink: 'https://localhost/endpoint/object1?ver=X.X.X', + key: 'value', + someRef: { + kind: 'anotherEndpoint:state', + selfLink: 'https://localhost/anotherEndpoint/refObject?ver=X.X.X', + someKey: 'someValue', + items: [ + { + nestedObjectName: 'name' + } + ] + } + } + ] + } + } + }, + { + name: 'should expand reference using endpointSuffix', + endpointObj: { + path: '/endpoint', + expandReferences: { someRef: { endpointSuffix: '/suffix' } } + }, + endpoints: [ + { + endpoint: '/endpoint', + response: { + kind: 'endpoint:state', + selfLink: 'https://localhost/endpoint?ver=X.X.X', + items: [ + { + name: 'object1', + selfLink: 'https://localhost/endpoint/object1?ver=X.X.X', + key: 'value', + someRef: { + link: 'https://localhost/anotherEndpoint/refObject?ver=X.X.X' + } + } + ] + } + }, + { + endpoint: '/anotherEndpoint/refObject/suffix', + response: { + kind: 'anotherEndpoint:state', + selfLink: 'https://localhost/anotherEndpoint/refObject/suffix?ver=X.X.X', + someKey: 'someValue', + items: [ + { + nestedObjectName: 'name' + } + ] + } + } + ], + expectedData: { + name: '/endpoint', + data: { + kind: 'endpoint:state', + selfLink: 'https://localhost/endpoint?ver=X.X.X', + items: [ + { + name: 'object1', + selfLink: 'https://localhost/endpoint/object1?ver=X.X.X', + key: 'value', + someRef: { + kind: 'anotherEndpoint:state', + selfLink: 'https://localhost/anotherEndpoint/refObject/suffix?ver=X.X.X', + someKey: 'someValue', + items: [ + { + nestedObjectName: 'name' + } + ] + } + } + ] + } + } + }, + { + name: 'should expand reference and include reference\'s stats', + endpointObj: { + path: '/endpoint', + expandReferences: { someRef: { includeStats: true } } + }, + endpoints: [ + { + endpoint: '/endpoint', + response: { + kind: 'endpoint:state', + selfLink: 'https://localhost/endpoint?ver=X.X.X', + items: [ + { + name: 'object1', + selfLink: 'https://localhost/endpoint/object1?ver=X.X.X', + key: 'value', + someRef: { + link: 'https://localhost/anotherEndpoint/refObject?ver=X.X.X' + } + } + ] + } + }, + { + endpoint: '/anotherEndpoint/refObject/stats', + response: { + kind: 'anotherEndpoint:stats', + selfLink: 'https://localhost/anotherEndpoint/refObject/stats?ver=X.X.X', + entries: { + 'https://localhost/anotherEndpoint/refObject/stats': { + nestedStats: { + kind: 'anotherEndpoint:stats', + selfLink: 'https://localhost/anotherEndpoint/refObject/stats?ver=X.X.X', + name: 'anotherStats', + statKey: 'statValue' + } + } + } + } + }, + { + endpoint: '/anotherEndpoint/refObject', + response: { + kind: 'anotherEndpoint:state', + selfLink: 'https://localhost/anotherEndpoint/refObject?ver=X.X.X', + someKey: 'someValue', + items: [ + { + nestedObjectName: 'name' + } + ] + } + } + ], + expectedData: { + name: '/endpoint', + data: { + kind: 'endpoint:state', + selfLink: 'https://localhost/endpoint?ver=X.X.X', + items: [ + { + name: 'object1', + selfLink: 'https://localhost/endpoint/object1?ver=X.X.X', + key: 'value', + someRef: { + kind: 'anotherEndpoint:state', + selfLink: 'https://localhost/anotherEndpoint/refObject?ver=X.X.X', + entries: { + 'https://localhost/anotherEndpoint/refObject/stats': { + nestedStats: { + kind: 'anotherEndpoint:stats', + selfLink: 'https://localhost/anotherEndpoint/refObject/stats?ver=X.X.X', + name: 'anotherStats', + statKey: 'statValue' + } + } + }, + someKey: 'someValue', + items: [ + { + nestedObjectName: 'name' + } + ] + } + } + ] + } + } + }, + { + name: 'should expand reference and include reference\'s stats using endpointSuffix', + endpointObj: { + path: '/endpoint', + expandReferences: { someRef: { includeStats: true, endpointSuffix: '/suffix' } } + }, + endpoints: [ + { + endpoint: '/endpoint', + response: { + kind: 'endpoint:state', + selfLink: 'https://localhost/endpoint?ver=X.X.X', + items: [ + { + name: 'object1', + selfLink: 'https://localhost/endpoint/object1?ver=X.X.X', + key: 'value', + someRef: { + link: 'https://localhost/anotherEndpoint/refObject?ver=X.X.X' + } + } + ] + } + }, + { + endpoint: '/anotherEndpoint/refObject/suffix/stats', + response: { + kind: 'anotherEndpoint:stats', + selfLink: 'https://localhost/anotherEndpoint/refObject/suffix/stats?ver=X.X.X', + entries: { + 'https://localhost/anotherEndpoint/refObject/suffix/stats': { + nestedStats: { + kind: 'anotherEndpoint:stats', + selfLink: 'https://localhost/anotherEndpoint/refObject/suffix/stats?ver=X.X.X', + name: 'anotherStats', + statKey: 'statValue' + } + } + } + } + }, + { + endpoint: '/anotherEndpoint/refObject/suffix', + response: { + kind: 'anotherEndpoint:state', + selfLink: 'https://localhost/anotherEndpoint/refObject/suffix?ver=X.X.X', + someKey: 'someValue', + items: [ + { + nestedObjectName: 'name' + } + ] + } + } + ], + expectedData: { + name: '/endpoint', + data: { + kind: 'endpoint:state', + selfLink: 'https://localhost/endpoint?ver=X.X.X', + items: [ + { + name: 'object1', + selfLink: 'https://localhost/endpoint/object1?ver=X.X.X', + key: 'value', + someRef: { + kind: 'anotherEndpoint:state', + selfLink: 'https://localhost/anotherEndpoint/refObject/suffix?ver=X.X.X', + entries: { + 'https://localhost/anotherEndpoint/refObject/suffix/stats': { + nestedStats: { + kind: 'anotherEndpoint:stats', + selfLink: 'https://localhost/anotherEndpoint/refObject/suffix/stats?ver=X.X.X', + name: 'anotherStats', + statKey: 'statValue' + } + } + }, + someKey: 'someValue', + items: [ + { + nestedObjectName: 'name' + } + ] + } + } + ] + } + } + }, + { + name: 'should include stats, expand reference using suffix and include reference\'s stats', + endpointObj: { + path: '/endpoint', + expandReferences: { someRef: { includeStats: true } }, + includeStats: true + }, + endpoints: [ + { + endpoint: '/endpoint', + response: { + kind: 'endpoint:state', + selfLink: 'https://localhost/endpoint?ver=X.X.X', + items: [ + { + name: 'object1', + selfLink: 'https://localhost/endpoint/object1?ver=X.X.X', + key: 'value', + someRef: { + link: 'https://localhost/anotherEndpoint/refObject?ver=X.X.X' + } + } + ] + } + }, + { + endpoint: '/anotherEndpoint/refObject/stats', + response: { + kind: 'anotherEndpoint:stats', + selfLink: 'https://localhost/anotherEndpoint/refObject/stats?ver=X.X.X', + entries: { + 'https://localhost/anotherEndpoint/refObject/stats': { + nestedStats: { + kind: 'anotherEndpoint:stats', + selfLink: 'https://localhost/anotherEndpoint/refObject/stats?ver=X.X.X', + name: 'anotherStats', + statKey: 'statValue' + } + } + } + } + }, + { + endpoint: '/anotherEndpoint/refObject', + response: { + kind: 'anotherEndpoint:state', + selfLink: 'https://localhost/anotherEndpoint/refObject?ver=X.X.X', + someKey: 'someValue', + items: [ + { + nestedObjectName: 'name' + } + ] + } + }, + { + endpoint: '/endpoint/object1/stats', + response: { + kind: 'endpoint:stats', + selfLink: 'https://localhost/endpoint/object1/stats?ver=X.X.X', + entries: { + 'https://localhost/endpoint/object2/stats': { + nestedStats: { + kind: 'endpoint:stats', + selfLink: 'https://localhost/endpoint/object1/stats?ver=X.X.X', + statKey: 'statValue' + } + } + } + } + } + ], + expectedData: { + name: '/endpoint', + data: { + kind: 'endpoint:state', + selfLink: 'https://localhost/endpoint?ver=X.X.X', + items: [ + { + name: 'object1', + kind: 'endpoint:stats', + selfLink: 'https://localhost/endpoint/object1?ver=X.X.X', + key: 'value', + entries: { + 'https://localhost/endpoint/object2/stats': { + nestedStats: { + kind: 'endpoint:stats', + selfLink: 'https://localhost/endpoint/object1/stats?ver=X.X.X', + statKey: 'statValue' + } + } + }, + someRef: { + kind: 'anotherEndpoint:state', + selfLink: 'https://localhost/anotherEndpoint/refObject?ver=X.X.X', + entries: { + 'https://localhost/anotherEndpoint/refObject/stats': { + nestedStats: { + kind: 'anotherEndpoint:stats', + selfLink: 'https://localhost/anotherEndpoint/refObject/stats?ver=X.X.X', + name: 'anotherStats', + statKey: 'statValue' + } + } + }, + someKey: 'someValue', + items: [ + { + nestedObjectName: 'name' + } + ] + } + } + ] + } + } + }, + { + name: 'should not query reference objects without link property', + endpointObj: { + path: '/endpoint', + expandReferences: { someRef: { } } + }, + endpoints: [ + { + endpoint: '/endpoint', + response: { + kind: 'endpoint:state', + selfLink: 'https://localhost/endpoint?ver=X.X.X', + items: [ + { + name: 'object1', + selfLink: 'https://localhost/endpoint/object1?ver=X.X.X', + key: 'value', + someRef: { + someRefKey: 'value' + } + } + ] + } + } + ], + expectedData: { + name: '/endpoint', + data: { + kind: 'endpoint:state', + selfLink: 'https://localhost/endpoint?ver=X.X.X', + items: [ + { + name: 'object1', + selfLink: 'https://localhost/endpoint/object1?ver=X.X.X', + key: 'value', + someRef: { + someRefKey: 'value' + } + } + ] + } + } + }, + { + name: 'should not query objects without selfLink property', + endpointObj: { + path: '/endpoint', + includeStats: true + }, + endpoints: [ + { + endpoint: '/endpoint', + response: { + kind: 'endpoint:state', + selfLink: 'https://localhost/endpoint?ver=X.X.X', + items: [ + { + name: 'object1', + key: 'value', + someRef: { + someRefKey: 'value' + } + } + ] + } + } + ], + expectedData: { + name: '/endpoint', + data: { + kind: 'endpoint:state', + selfLink: 'https://localhost/endpoint?ver=X.X.X', + items: [ + { + name: 'object1', + key: 'value', + someRef: { + someRefKey: 'value' + } + } + ] + } + } + } + ] +}; diff --git a/test/unit/forwarderTests.js b/test/unit/forwarderTests.js index 27528126..f5ed1874 100644 --- a/test/unit/forwarderTests.js +++ b/test/unit/forwarderTests.js @@ -8,34 +8,36 @@ 'use strict'; -const assert = require('assert'); -const DataFilter = require('../../src/lib/dataFilter.js').DataFilter; +/* eslint-disable import/order */ -/* eslint-disable global-require */ +require('./shared/restoreCache')(); +require('./shared/disableAjv'); // consumers and forwarder import config with ajv -describe('Forwarder', () => { - let forwarder; - let consumers; +const chai = require('chai'); +const chaiAsPromised = require('chai-as-promised'); +const sinon = require('sinon'); + +const DataFilter = require('../../src/lib/dataFilter').DataFilter; +const forwarder = require('../../src/lib/forwarder'); +const consumers = require('../../src/lib/consumers'); - let actualContext; +chai.use(chaiAsPromised); +const assert = chai.assert; + +describe('Forwarder', () => { const config = { type: 'consumerType' }; const type = 'dataType'; const data = { foo: 'bar' }; - before(() => { - forwarder = require('../../src/lib/forwarder.js'); - consumers = require('../../src/lib/consumers.js'); - }); - after(() => { - Object.keys(require.cache).forEach((key) => { - delete require.cache[key]; - }); + afterEach(() => { + sinon.restore(); }); it('should forward to consumer', () => { - consumers.getConsumers = () => [ + let actualContext; + sinon.stub(consumers, 'getConsumers').returns([ { consumer: (context) => { actualContext = context; @@ -44,26 +46,21 @@ describe('Forwarder', () => { tracer: null, filter: new DataFilter({}) } - ]; - - return forwarder.forward({ type, data }) + ]); + return assert.isFulfilled(forwarder.forward({ type, data }) .then(() => { - assert.deepEqual(actualContext.event.data, data); - assert.deepEqual(actualContext.config, config); - }) - .catch(err => Promise.reject(err)); + assert.deepStrictEqual(actualContext.event.data, data); + assert.deepStrictEqual(actualContext.config, config); + })); }); it('should resolve with no consumers', () => { - consumers.getConsumers = () => null; - - return forwarder.forward({ type, data }) - .then(() => {}) - .catch(err => Promise.reject(new Error(`Should not error: ${err}`))); + sinon.stub(consumers, 'getConsumers').returns(null); + return assert.isFulfilled(forwarder.forward({ type, data })); }); it('should resolve on consumer error', () => { - consumers.getConsumers = () => [ + sinon.stub(consumers, 'getConsumers').returns([ { consumer: () => { throw new Error('foo'); @@ -72,10 +69,7 @@ describe('Forwarder', () => { tracer: null, filter: new DataFilter({}) } - ]; - - return forwarder.forward({ type, data }) - .then(() => {}) - .catch(err => Promise.reject(new Error(`Should not error: ${err}`))); + ]); + return assert.isFulfilled(forwarder.forward({ type, data })); }); }); diff --git a/test/unit/loggerTests.js b/test/unit/loggerTests.js index fe15417f..0cb65502 100644 --- a/test/unit/loggerTests.js +++ b/test/unit/loggerTests.js @@ -8,39 +8,50 @@ 'use strict'; -const assert = require('assert'); -const logger = require('../../src/lib/logger.js'); - -const logLevels = [ - 'notset', - 'debug', - 'info', - 'error' -]; -const loggedMessages = { - error: [], - info: [], - debug: [] -}; -const loggerMock = { - severe(msg) { loggedMessages.error.push(msg); }, - info(msg) { loggedMessages.info.push(msg); }, - finest(msg) { loggedMessages.debug.push(msg); } -}; - -logger.logger = loggerMock; +/* eslint-disable import/order */ + +require('./shared/restoreCache')(); + +const chai = require('chai'); +const chaiAsPromised = require('chai-as-promised'); +const sinon = require('sinon'); + +const logger = require('../../src/lib/logger'); + +chai.use(chaiAsPromised); +const assert = chai.assert; describe('Logger', () => { + const logLevels = [ + 'notset', + 'debug', + 'info', + 'error' + ]; + const loggedMessages = { + error: [], + info: [], + debug: [] + }; + const loggerMock = { + severe(msg) { loggedMessages.error.push(msg); }, + info(msg) { loggedMessages.info.push(msg); }, + finest(msg) { loggedMessages.debug.push(msg); } + }; + + before(() => { + sinon.stub(logger, 'logger').value(loggerMock); + }); + beforeEach(() => { logger.setLogLevel('info'); Object.keys(loggedMessages).forEach((msgType) => { loggedMessages[msgType] = []; }); }); + after(() => { - Object.keys(require.cache).forEach((key) => { - delete require.cache[key]; - }); + sinon.restore(); }); it('defaults: should be by default \'info\' level', () => { @@ -154,6 +165,7 @@ describe('Logger', () => { logger.info(`this contains secrets: ${JSON.stringify(decl, null, 4)}`); assert.notStrictEqual(loggedMessages.info[0].indexOf(expected), -1); }); + it('should mask secrets - passphrase (without new lines)', () => { const decl = { passphrase: 'foo' diff --git a/test/unit/normalizeConfigTests.js b/test/unit/normalizeConfigTests.js new file mode 100644 index 00000000..5b512ef2 --- /dev/null +++ b/test/unit/normalizeConfigTests.js @@ -0,0 +1,62 @@ +/* + * Copyright 2020. F5 Networks, Inc. See End User License Agreement ('EULA') for + * license terms. Notwithstanding anything to the contrary in the EULA, Licensee + * may copy and modify this software product for its internal business purposes. + * Further, Licensee may upload, publish and distribute the modified version of + * the software product on devcentral.f5.com. + */ + +'use strict'; + +/* eslint-disable import/order */ + +require('./shared/restoreCache')(); + +const chai = require('chai'); +const chaiAsPromised = require('chai-as-promised'); +const sinon = require('sinon'); + +const configWorker = require('../../src/lib/config'); +const constants = require('../../src/lib/constants'); +const deviceUtil = require('../../src/lib/deviceUtil'); +const normalizeConfig = require('../../src/lib/normalizeConfig'); +const util = require('../../src/lib/util'); + +const normalizeConfigTestsData = require('./normalizeConfigTestsData'); +const testUtil = require('./shared/util'); + +chai.use(chaiAsPromised); +const assert = chai.assert; + +describe('Configuration Normalization', () => { + const validateAndFormat = function (declaration) { + return configWorker.validate(declaration) + .then(validated => Promise.resolve(util.formatConfig(validated))) + .then(validated => deviceUtil.decryptAllSecrets(validated)); + }; + + beforeEach(() => { + sinon.stub(deviceUtil, 'encryptSecret').resolvesArg(0); + sinon.stub(deviceUtil, 'decryptSecret').resolvesArg(0); + sinon.stub(deviceUtil, 'getDeviceType').resolves(constants.DEVICE_TYPE.BIG_IP); + sinon.stub(util, 'networkCheck').resolves(); + }); + + afterEach(() => { + sinon.restore(); + }); + /* eslint-disable implicit-arrow-linebreak */ + Object.keys(normalizeConfigTestsData).forEach((testSetKey) => { + const testSet = normalizeConfigTestsData[testSetKey]; + testUtil.getCallableDescribe(testSet)(testSet.name, () => { + testSet.tests.forEach((testConf) => { + testUtil.getCallableIt(testConf)(testConf.name, () => + validateAndFormat(testConf.declaration) + .then(configData => assert.deepStrictEqual( + normalizeConfig(configData), + testConf.expected + ))); + }); + }); + }); +}); diff --git a/test/unit/normalizeConfigTestsData.js b/test/unit/normalizeConfigTestsData.js new file mode 100644 index 00000000..b6ffc19c --- /dev/null +++ b/test/unit/normalizeConfigTestsData.js @@ -0,0 +1,1031 @@ +/* + * Copyright 2018. F5 Networks, Inc. See End User License Agreement ('EULA') for + * license terms. Notwithstanding anything to the contrary in the EULA, Licensee + * may copy and modify this software product for its internal business purposes. + * Further, Licensee may upload, publish and distribute the modified version of + * the software product on devcentral.f5.com. + */ + +'use strict'; + +module.exports = { + /** + * Set of data to check actual and expected results only. + * If you need some additional check feel free to add additional + * property or write separate test. + * + * Note: you can specify 'testOpts' property on the same level as 'name'. + * Following options available: + * - only (bool) - run this test only (it.only) + * */ + /** + * TEST SET DATA STARTS HERE + * */ + emptyDeclaration: { + name: 'Empty Declaration', + tests: [ + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should normalize empty declaration', + declaration: { + class: 'Telemetry' + }, + expected: {} + } + ] + }, + /** + * TEST SET DATA STARTS HERE + * */ + keepDataUnmodified: { + name: 'Ignore certain classes', + tests: [ + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should ignore Telemetry_Listener, Telemetry_iHealth_Poller, Controls classes', + declaration: { + class: 'Telemetry', + controls: { + class: 'Controls', + logLevel: 'debug', + debug: true + }, + My_Listener: { + class: 'Telemetry_Listener' + }, + My_iHealth_Poller: { + class: 'Telemetry_iHealth_Poller', + username: 'username', + passphrase: { + cipherText: 'passphrase' + }, + interval: { + timeWindow: { + start: '00:00', + end: '03:00' + } + } + } + }, + expected: { + Controls: { + controls: { + class: 'Controls', + logLevel: 'debug', + debug: true + } + }, + Telemetry_Listener: { + My_Listener: { + class: 'Telemetry_Listener', + enable: true, + trace: false, + match: '', + port: 6514, + actions: [ + { + enable: true, + setTag: { + application: '`A`', + tenant: '`T`' + } + } + ] + } + }, + Telemetry_iHealth_Poller: { + My_iHealth_Poller: { + class: 'Telemetry_iHealth_Poller', + enable: true, + trace: false, + passphrase: 'passphrase', + username: 'username', + interval: { + frequency: 'daily', + timeWindow: { + end: '03:00', + start: '00:00' + } + } + } + } + } + } + ] + }, + /** + * TEST SET DATA STARTS HERE + * */ + systemPollerNormalization: { + name: 'Telemetry_System_Poller normalization', + tests: [ + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should convert Telemetry_System_Poller to Telemetry_System', + declaration: { + class: 'Telemetry', + My_Poller: { + class: 'Telemetry_System_Poller' + } + }, + expected: { + Telemetry_System: { + My_Poller: { + class: 'Telemetry_System', + name: 'My_Poller', + enable: true, + allowSelfSignedCert: false, + host: 'localhost', + port: 8100, + protocol: 'http', + systemPollers: [ + { + name: 'My_Poller', + enable: true, + interval: 300, + actions: [ + { + enable: true, + setTag: { + application: '`A`', + tenant: '`T`' + } + } + ] + } + ] + } + } + } + }, + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should convert Telemetry_System_Poller to Telemetry_System and copy properties', + declaration: { + class: 'Telemetry', + My_Poller: { + class: 'Telemetry_System_Poller', + enable: false, + allowSelfSignedCert: true, + trace: 'something', + username: 'username', + passphrase: { + cipherText: 'passphrase' + }, + host: '1.1.1.1', + port: 443, + interval: 10, + protocol: 'https', + tag: { + tag: 'tag' + }, + actions: [ + { + setTag: { + tag: 'tag' + } + } + ], + endpointList: { + items: { + endpoint1: { + path: 'endpoint1' + } + } + } + } + }, + expected: { + Telemetry_System: { + My_Poller: { + class: 'Telemetry_System', + name: 'My_Poller', + enable: false, + allowSelfSignedCert: true, + trace: 'something', + username: 'username', + passphrase: 'passphrase', + host: '1.1.1.1', + port: 443, + protocol: 'https', + systemPollers: [ + { + name: 'My_Poller', + enable: false, + trace: 'something', + interval: 10, + tag: { + tag: 'tag' + }, + actions: [ + { + enable: true, + setTag: { + tag: 'tag' + } + } + ], + endpointList: { + endpoint1: { + enable: true, + name: 'endpoint1', + path: '/endpoint1' + } + } + } + ] + } + } + } + } + ] + }, + /** + * TEST SET DATA STARTS HERE + * */ + systemNormalization: { + name: 'Telemetry_System normalization', + tests: [ + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should avoild name dups for anonymous pollers', + declaration: { + class: 'Telemetry', + SystemPoller_1: { + class: 'Telemetry_System_Poller' + }, + My_System_1: { + class: 'Telemetry_System', + systemPoller: [ + 'SystemPoller_1', + { + interval: 180 + } + ] + } + }, + expected: { + Telemetry_System: { + My_System_1: { + class: 'Telemetry_System', + name: 'My_System_1', + allowSelfSignedCert: false, + enable: true, + host: 'localhost', + port: 8100, + protocol: 'http', + systemPollers: [ + { + name: 'SystemPoller_1', + enable: true, + interval: 300, + actions: [ + { + enable: true, + setTag: { + application: '`A`', + tenant: '`T`' + } + } + ] + }, + { + name: 'SystemPoller_2', + enable: true, + interval: 180, + actions: [ + { + enable: true, + setTag: { + application: '`A`', + tenant: '`T`' + } + } + ] + } + ] + } + } + } + }, + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should normalize multiple Telemetry_System objects', + declaration: { + class: 'Telemetry', + My_Poller_1: { + class: 'Telemetry_System_Poller', + trace: false + }, + My_Poller_2: { + class: 'Telemetry_System_Poller', + enable: false, + allowSelfSignedCert: true, + trace: 'something', + host: '1.1.1.1', + port: 443, + interval: 10, + protocol: 'https', + actions: [ + { + setTag: { + tag: 'tag' + } + } + ], + endpointList: { + items: { + endpoint1: { + path: 'endpoint1' + } + } + } + }, + My_Poller_3: { + class: 'Telemetry_System_Poller' + }, + My_System_1: { + class: 'Telemetry_System' + }, + My_System_2: { + class: 'Telemetry_System', + allowSelfSignedCert: true, + enable: false, + trace: 'something', + host: '1.1.1.1', + port: 443, + protocol: 'https' + }, + My_System_3: { + class: 'Telemetry_System', + systemPoller: { + interval: 500 + } + }, + My_System_4: { + class: 'Telemetry_System', + systemPoller: [ + { + interval: 500 + } + ] + }, + My_System_5: { + class: 'Telemetry_System', + systemPoller: [ + { + interval: 500 + }, + 'My_Poller_3' + ] + }, + My_System_6: { + class: 'Telemetry_System', + systemPoller: [ + 'My_Poller_2', + 'My_Poller_3' + ] + }, + My_System_7: { + class: 'Telemetry_System', + systemPoller: [ + 'My_Poller_2', + 'My_Poller_3' + ] + } + }, + expected: { + Telemetry_System: { + My_Poller_1: { + class: 'Telemetry_System', + name: 'My_Poller_1', + enable: true, + allowSelfSignedCert: false, + trace: false, + host: 'localhost', + port: 8100, + protocol: 'http', + systemPollers: [ + { + name: 'My_Poller_1', + enable: true, + interval: 300, + trace: false, + actions: [ + { + enable: true, + setTag: { + application: '`A`', + tenant: '`T`' + } + } + ] + } + ] + }, + My_System_1: { + class: 'Telemetry_System', + name: 'My_System_1', + allowSelfSignedCert: false, + enable: true, + host: 'localhost', + port: 8100, + protocol: 'http', + systemPollers: [] + }, + My_System_2: { + class: 'Telemetry_System', + name: 'My_System_2', + allowSelfSignedCert: true, + enable: false, + trace: 'something', + host: '1.1.1.1', + port: 443, + protocol: 'https', + systemPollers: [] + }, + My_System_3: { + class: 'Telemetry_System', + name: 'My_System_3', + allowSelfSignedCert: false, + enable: true, + host: 'localhost', + port: 8100, + protocol: 'http', + systemPollers: [ + { + name: 'SystemPoller_1', + enable: true, + interval: 500, + actions: [ + { + enable: true, + setTag: { + application: '`A`', + tenant: '`T`' + } + } + ] + } + ] + }, + My_System_4: { + class: 'Telemetry_System', + name: 'My_System_4', + allowSelfSignedCert: false, + enable: true, + host: 'localhost', + port: 8100, + protocol: 'http', + systemPollers: [ + { + name: 'SystemPoller_1', + enable: true, + interval: 500, + actions: [ + { + enable: true, + setTag: { + application: '`A`', + tenant: '`T`' + } + } + ] + } + ] + }, + My_System_5: { + class: 'Telemetry_System', + name: 'My_System_5', + allowSelfSignedCert: false, + enable: true, + host: 'localhost', + port: 8100, + protocol: 'http', + systemPollers: [ + { + name: 'SystemPoller_1', + enable: true, + interval: 500, + actions: [ + { + enable: true, + setTag: { + application: '`A`', + tenant: '`T`' + } + } + ] + }, + { + name: 'My_Poller_3', + enable: true, + interval: 300, + actions: [ + { + enable: true, + setTag: { + application: '`A`', + tenant: '`T`' + } + } + ] + } + ] + }, + My_System_6: { + class: 'Telemetry_System', + name: 'My_System_6', + allowSelfSignedCert: false, + enable: true, + host: 'localhost', + port: 8100, + protocol: 'http', + systemPollers: [ + { + name: 'My_Poller_2', + enable: false, + trace: 'something', + interval: 10, + actions: [ + { + enable: true, + setTag: { + tag: 'tag' + } + } + ], + endpointList: { + endpoint1: { + enable: true, + name: 'endpoint1', + path: '/endpoint1' + } + } + }, + { + name: 'My_Poller_3', + enable: true, + interval: 300, + actions: [ + { + enable: true, + setTag: { + application: '`A`', + tenant: '`T`' + } + } + ] + } + ] + }, + My_System_7: { + class: 'Telemetry_System', + name: 'My_System_7', + allowSelfSignedCert: false, + enable: true, + host: 'localhost', + port: 8100, + protocol: 'http', + systemPollers: [ + { + name: 'My_Poller_2', + enable: false, + trace: 'something', + interval: 10, + actions: [ + { + enable: true, + setTag: { + tag: 'tag' + } + } + ], + endpointList: { + endpoint1: { + enable: true, + name: 'endpoint1', + path: '/endpoint1' + } + } + }, + { + name: 'My_Poller_3', + enable: true, + interval: 300, + actions: [ + { + enable: true, + setTag: { + application: '`A`', + tenant: '`T`' + } + } + ] + } + ] + } + } + } + } + ] + }, + /** + * TEST SET DATA STARTS HERE + * */ + endpointsNormalization: { + name: 'Telemetry_Endpoints normalization', + tests: [ + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should normalize inline definitions (disabled endpoint)', + declaration: { + class: 'Telemetry', + My_System: { + class: 'Telemetry_System', + systemPoller: { + endpointList: { + enable: false, + basePath: 'mgmt', + items: { + endpoint1: { + path: 'endpoint1' + } + } + } + } + } + }, + expected: { + Telemetry_System: { + My_System: { + class: 'Telemetry_System', + name: 'My_System', + enable: true, + allowSelfSignedCert: false, + host: 'localhost', + port: 8100, + protocol: 'http', + systemPollers: [ + { + name: 'SystemPoller_1', + enable: true, + interval: 300, + actions: [ + { + enable: true, + setTag: { + application: '`A`', + tenant: '`T`' + } + } + ], + endpointList: {} + } + ] + } + } + } + }, + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should normalize inline definitions (enabled endpoint)', + declaration: { + class: 'Telemetry', + My_System: { + class: 'Telemetry_System', + systemPoller: { + endpointList: { + basePath: 'mgmt/', + items: { + endpoint1: { + path: 'endpoint1' + } + } + } + } + } + }, + expected: { + Telemetry_System: { + My_System: { + class: 'Telemetry_System', + name: 'My_System', + enable: true, + allowSelfSignedCert: false, + host: 'localhost', + port: 8100, + protocol: 'http', + systemPollers: [ + { + name: 'SystemPoller_1', + enable: true, + interval: 300, + actions: [ + { + enable: true, + setTag: { + application: '`A`', + tenant: '`T`' + } + } + ], + endpointList: { + endpoint1: { + enable: true, + name: 'endpoint1', + path: '/mgmt/endpoint1' + } + } + } + ] + } + } + } + }, + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should expand references', + declaration: { + class: 'Telemetry', + Disabled_Endpoints_1: { + class: 'Telemetry_Endpoints', + enable: false, + basePath: 'basePath/', + items: { + disabledEndpoint: { + path: 'disabledEndpoint' + } + } + }, + Enabled_Endpoints_1: { + class: 'Telemetry_Endpoints', + enable: true, + basePath: 'basePath/', + items: { + enabledEndpoint1: { + path: 'enabledEndpoint1' + } + } + }, + Enabled_Endpoints_2: { + class: 'Telemetry_Endpoints', + enable: true, + basePath: 'basePath2/', + items: { + enabledEndpoint2: { + path: '/enabledEndpoint2' + } + } + }, + My_System_1: { + class: 'Telemetry_System', + systemPoller: { + endpointList: 'Enabled_Endpoints_1' + } + }, + My_System_2: { + class: 'Telemetry_System', + systemPoller: { + endpointList: [ + 'Enabled_Endpoints_1', + 'Enabled_Endpoints_2/enabledEndpoint2', + 'Enabled_Endpoints_1', + 'Enabled_Endpoints_2/enabledEndpoint2', + 'Disabled_Endpoints_1/disabledEndpoint', + 'Disabled_Endpoints_1/disabledEndpoint', + { + enable: false, + items: { + disabledEndpoint_3: { + path: 'disabledEndpoint_3' + } + } + }, + { + enable: true, + items: { + disabledEndpoint_3: { + enable: false, + path: 'disabledEndpoint_3' + } + } + }, + { + name: 'disabledEndpoint_4', + path: 'disabledEndpoint_4', + enable: false + }, + { + enable: true, + basePath: '/basePath4', + items: { + enabledEndpoint_4: { + enable: true, + path: 'enabledEndpoint_4' + }, + enabledEndpoint_4_2: { + enable: false, + path: 'enabledEndpoint_4_2' + } + } + }, + { + name: 'enabledEndpoint_5', + path: 'enabledEndpoint_5', + enable: true + } + ] + } + }, + My_System_3: { + class: 'Telemetry_System', + systemPoller: { + endpointList: [ + 'Enabled_Endpoints_1', + 'Enabled_Endpoints_2/enabledEndpoint2', + 'Enabled_Endpoints_1', + 'Enabled_Endpoints_2/enabledEndpoint2', + 'Disabled_Endpoints_1/disabledEndpoint', + 'Disabled_Endpoints_1/disabledEndpoint' + ] + } + } + }, + expected: { + Telemetry_Endpoints: { + Disabled_Endpoints_1: { + class: 'Telemetry_Endpoints', + enable: false, + basePath: 'basePath/', + items: { + disabledEndpoint: { + enable: true, + path: 'disabledEndpoint' + } + } + }, + Enabled_Endpoints_1: { + class: 'Telemetry_Endpoints', + enable: true, + basePath: '/basePath', + items: { + enabledEndpoint1: { + enable: true, + path: 'enabledEndpoint1' + } + } + }, + Enabled_Endpoints_2: { + class: 'Telemetry_Endpoints', + enable: true, + basePath: 'basePath2/', + items: { + enabledEndpoint2: { + enable: true, + path: '/enabledEndpoint2' + } + } + } + }, + Telemetry_System: { + My_System_1: { + class: 'Telemetry_System', + name: 'My_System_1', + enable: true, + allowSelfSignedCert: false, + host: 'localhost', + port: 8100, + protocol: 'http', + systemPollers: [ + { + name: 'SystemPoller_1', + enable: true, + interval: 300, + actions: [ + { + enable: true, + setTag: { + application: '`A`', + tenant: '`T`' + } + } + ], + endpointList: { + enabledEndpoint1: { + enable: true, + name: 'enabledEndpoint1', + path: '/basePath/enabledEndpoint1' + } + } + } + ] + }, + My_System_2: { + class: 'Telemetry_System', + name: 'My_System_2', + enable: true, + allowSelfSignedCert: false, + host: 'localhost', + port: 8100, + protocol: 'http', + systemPollers: [ + { + name: 'SystemPoller_1', + enable: true, + interval: 300, + actions: [ + { + enable: true, + setTag: { + application: '`A`', + tenant: '`T`' + } + } + ], + endpointList: { + enabledEndpoint1: { + enable: true, + name: 'enabledEndpoint1', + path: '/basePath/enabledEndpoint1' + }, + enabledEndpoint2: { + enable: true, + name: 'enabledEndpoint2', + path: '/basePath2/enabledEndpoint2' + }, + enabledEndpoint_4: { + enable: true, + name: 'enabledEndpoint_4', + path: '/basePath4/enabledEndpoint_4' + }, + enabledEndpoint_5: { + enable: true, + name: 'enabledEndpoint_5', + path: '/enabledEndpoint_5' + } + } + } + ] + }, + My_System_3: { + class: 'Telemetry_System', + name: 'My_System_3', + enable: true, + allowSelfSignedCert: false, + host: 'localhost', + port: 8100, + protocol: 'http', + systemPollers: [ + { + name: 'SystemPoller_1', + enable: true, + interval: 300, + actions: [ + { + enable: true, + setTag: { + application: '`A`', + tenant: '`T`' + } + } + ], + endpointList: { + enabledEndpoint1: { + enable: true, + name: 'enabledEndpoint1', + path: '/basePath/enabledEndpoint1' + }, + enabledEndpoint2: { + enable: true, + name: 'enabledEndpoint2', + path: '/basePath2/enabledEndpoint2' + } + } + } + ] + } + } + } + } + ] + } +}; diff --git a/test/unit/normalizeTests.js b/test/unit/normalizeTests.js index 05ab11a8..f1eba83a 100644 --- a/test/unit/normalizeTests.js +++ b/test/unit/normalizeTests.js @@ -8,22 +8,26 @@ 'use strict'; -const assert = require('assert'); +/* eslint-disable import/order */ + +require('./shared/restoreCache')(); + +const chai = require('chai'); +const chaiAsPromised = require('chai-as-promised'); const sinon = require('sinon'); -const normalize = require('../../src/lib/normalize.js'); -const normalizeUtil = require('../../src/lib/normalizeUtil'); const properties = require('../../src/lib/properties.json'); -const dataExamples = require('./normalizeTestsData.js'); -const EVENT_TYPES = require('../../src/lib/constants.js').EVENT_TYPES; +const normalize = require('../../src/lib/normalize'); +const normalizeUtil = require('../../src/lib/normalizeUtil'); +const EVENT_TYPES = require('../../src/lib/constants').EVENT_TYPES; +const dataExamples = require('./normalizeTestsData'); +chai.use(chaiAsPromised); +const assert = chai.assert; describe('Normalize', () => { - after(() => { + afterEach(() => { sinon.restore(); - Object.keys(require.cache).forEach((key) => { - delete require.cache[key]; - }); }); const exampleData = { @@ -78,7 +82,7 @@ describe('Normalize', () => { assert.strictEqual(result.telemetryEventCategory, eventDataExample.category); } if (eventDataExample.expectedData !== undefined) { - assert.deepEqual(result, eventDataExample.expectedData); + assert.deepStrictEqual(result, eventDataExample.expectedData); } }); }); @@ -91,7 +95,7 @@ describe('Normalize', () => { }; const result = normalize.event(event); - assert.deepEqual(result, expectedResult); + assert.deepStrictEqual(result, expectedResult); }); it('should normalize event and rename key(s)', () => { @@ -109,7 +113,7 @@ describe('Normalize', () => { } }; const result = normalize.event(event, options); - assert.deepEqual(result, expectedResult); + assert.deepStrictEqual(result, expectedResult); }); it('should normalize event and format timestamps', () => { @@ -123,7 +127,7 @@ describe('Normalize', () => { formatTimestamps: ['date_time'] }; const result = normalize.event(event, options); - assert.deepEqual(result, expectedResult); + assert.deepStrictEqual(result, expectedResult); }); }); @@ -135,7 +139,7 @@ describe('Normalize', () => { }; const result = normalize.data(data); - assert.deepEqual(result, data); + assert.deepStrictEqual(result, data); }); it('should normalize data', () => { @@ -151,7 +155,7 @@ describe('Normalize', () => { }; const result = normalize.data(exampleData); - assert.deepEqual(result, expectedResult); + assert.deepStrictEqual(result, expectedResult); }); it('should get key', () => { @@ -179,7 +183,7 @@ describe('Normalize', () => { }; const result = normalize.data(exampleData, options); - assert.deepEqual(result, expectedResult); + assert.deepStrictEqual(result, expectedResult); }); }); @@ -252,7 +256,7 @@ describe('Normalize', () => { }; const result = normalize.data(data, options); - assert.deepEqual(result, expectedResult); + assert.deepStrictEqual(result, expectedResult); }); it('should preserve array if convertArrayToMap not specified', () => { @@ -268,7 +272,7 @@ describe('Normalize', () => { }; const result = normalize.data(data, options); - assert.deepEqual(result, result); + assert.deepStrictEqual(result, result); }); it('should preserve array if skipWheKeyMissing specified and key does not exist', () => { @@ -308,7 +312,7 @@ describe('Normalize', () => { }; const result = normalize.data(data, options); - assert.deepEqual(result, expectedResult); + assert.deepStrictEqual(result, expectedResult); }); it('should return empty list if empty array and skipWhenKeyMissing is specified', () => { @@ -331,7 +335,7 @@ describe('Normalize', () => { }; const result = normalize.data(data, options); - assert.deepEqual(result, expectedResult); + assert.deepStrictEqual(result, expectedResult); }); it('should return empty object if empty array and skipWhenKeyMissing not specified', () => { @@ -353,7 +357,7 @@ describe('Normalize', () => { }; const result = normalize.data(data, options); - assert.deepEqual(result, expectedResult); + assert.deepStrictEqual(result, expectedResult); }); }); @@ -383,7 +387,7 @@ describe('Normalize', () => { }; const result = normalize.data(data, options); - assert.deepEqual(result, expectedResult); + assert.deepStrictEqual(result, expectedResult); }); it('should not flatten data with first entry when pattern and excludePattern both match', () => { @@ -418,7 +422,7 @@ describe('Normalize', () => { }; const result = normalize.data(data, options); - assert.deepEqual(result, expectedResult); + assert.deepStrictEqual(result, expectedResult); }); it('should run custom functions on first entries', () => { @@ -472,7 +476,7 @@ describe('Normalize', () => { }); const result = normalize.data(data, options); - assert.deepEqual(result, expectedResult); + assert.deepStrictEqual(result, expectedResult); }); }); @@ -499,7 +503,7 @@ describe('Normalize', () => { const expectedResult = 'name'; const result = normalize.data('named_key,key1\nname,value', options); - assert.deepEqual(result, expectedResult); + assert.deepStrictEqual(result, expectedResult); }); it('should execute catch with stack trace when thrown error from run custom function', () => { @@ -535,7 +539,7 @@ describe('Normalize', () => { normalize.data(data, options); } catch (err) { caught = true; - assert.notStrictEqual(err.message.indexOf('runCustomFunction failed'), -1); + assert.notStrictEqual(err.message.indexOf('runCustomFunction \'getAverage\' failed'), -1); } assert(caught); }); @@ -553,7 +557,8 @@ describe('Normalize', () => { key: 'value', tenant: 'Common', application: 'app.app', - foo: 'bar' + foo: 'bar', + addntlTag: 'tag' } }; const options = { @@ -567,7 +572,10 @@ describe('Normalize', () => { }, definitions: properties.definitions, opts: { - skip: ['somekey'] + skip: ['somekey'], + tags: { + addntlTag: 'tag' + } } } } @@ -575,7 +583,7 @@ describe('Normalize', () => { }; const result = normalize.data(data, options); - assert.deepEqual(result, expectedResult); + assert.deepStrictEqual(result, expectedResult); }); it('should add keys by tag (flat classify)', () => { @@ -605,7 +613,7 @@ describe('Normalize', () => { }; const result = normalize.data(data, options); - assert.deepEqual(result, expectedResult); + assert.deepStrictEqual(result, expectedResult); }); }); @@ -637,7 +645,7 @@ describe('Normalize', () => { }; const result = normalize.data(data, options); - assert.deepEqual(result, expectedResult); + assert.deepStrictEqual(result, expectedResult); }); it('should format timestamps when matching property key', () => { @@ -653,7 +661,7 @@ describe('Normalize', () => { }; const result = normalize.data(data, options); - assert.deepEqual(result, expectedResult); + assert.deepStrictEqual(result, expectedResult); }); }); }); @@ -671,7 +679,7 @@ describe('Normalize', () => { ltmConfigTime: '2019-06-19T20:15:28.000Z' }; const result = normalize._handleTimestamps(ret, timestamps, options); - assert.deepEqual(result, expected); + assert.deepStrictEqual(result, expected); }); it('should format timestamps without propertyKey', () => { @@ -683,7 +691,7 @@ describe('Normalize', () => { formatMe: '2019-09-16T01:00:00.000Z' }; const result = normalize._handleTimestamps(ret, timestamps, {}); - assert.deepEqual(result, expected); + assert.deepStrictEqual(result, expected); }); }); @@ -703,7 +711,7 @@ describe('Normalize', () => { key2: 'hello' }; const result = normalize._handleFilterByKeys(ret, filterByKeys); - assert.deepEqual(result, expected); + assert.deepStrictEqual(result, expected); }); }); @@ -723,7 +731,7 @@ describe('Normalize', () => { prop3: 'value3' }; const result = normalize._handleRenameKeys(ret, renameKeys); - assert.deepEqual(result, expected); + assert.deepStrictEqual(result, expected); }); }); }); diff --git a/test/unit/normalizeTestsData.js b/test/unit/normalizeTestsData.js index 129c7d0d..f208e828 100644 --- a/test/unit/normalizeTestsData.js +++ b/test/unit/normalizeTestsData.js @@ -8,7 +8,7 @@ 'use strict'; -const EVENT_TYPES = require('../../src/lib/constants.js').EVENT_TYPES; +const EVENT_TYPES = require('../../src/lib/constants').EVENT_TYPES; /* eslint-disable no-useless-escape */ @@ -54,6 +54,11 @@ module.exports = { data: 'acl_policy_name="AFM_PROFILE_NAME",acl_rule_name="AFM_RULE_NAME",key1="value",key2="value2"', category: EVENT_TYPES.AFM_EVENT }, + { + name: 'valid AFM DoS event', + data: 'action="Allow",hostname="bigip1.velasco",bigip_mgmt_ip="10.201.198.70",context_name="/Common/10.10.10.140",date_time="2019-12-18T03:37:19.000Z",dest_ip="10.10.10.140",dest_port="80",device_product="Advanced Firewall Module",device_vendor="F5",device_version="13.1.1.0.0.4",dos_attack_event="Attack Sampled",dos_attack_id="710253591",dos_attack_name="TCP RST flood",dos_packets_dropped="0",dos_packets_received="1",errdefs_msgno="23003138",errdefs_msg_name="Network DoS Event",flow_id="0000000000000000",severity="4",dos_mode="Enforced",dos_src="Volumetric, Aggregated across all SrcIP\'s, VS-Specific attack, metric:PPS",partition_name="Common",route_domain="0",source_ip="10.10.10.210",source_port="9607",vlan="/Common/internal",telemetryEventCategory="LTM",tenant="Common', + category: EVENT_TYPES.AFM_EVENT + }, { name: 'incomplete ASM event', data: 'policy_name="ASM_PROFILE_NAME",key1="value",key2="value2"', diff --git a/test/unit/normalizeUtilTests.js b/test/unit/normalizeUtilTests.js index 0bbe8f8e..4ca9130b 100644 --- a/test/unit/normalizeUtilTests.js +++ b/test/unit/normalizeUtilTests.js @@ -8,258 +8,345 @@ 'use strict'; -const assert = require('assert'); -const normalizeUtil = require('../../src/lib/normalizeUtil.js'); +/* eslint-disable import/order */ +require('./shared/restoreCache')(); + +const chai = require('chai'); +const chaiAsPromised = require('chai-as-promised'); + +const normalizeUtil = require('../../src/lib/normalizeUtil'); + +chai.use(chaiAsPromised); +const assert = chai.assert; describe('Normalize Util', () => { - after(() => { - Object.keys(require.cache).forEach((key) => { - delete require.cache[key]; + describe('._convertArrayToMap()', () => { + it('should convert array to map', () => { + const array = [ + { + name: 'foo' + } + ]; + const expectedMap = { + foo: { + name: 'foo' + } + }; + const actualMap = normalizeUtil._convertArrayToMap(array, 'name'); + assert.deepStrictEqual(actualMap, expectedMap); }); - }); - it('should convert array to map', () => { - const array = [ - { - name: 'foo' - } - ]; - const expectedMap = { - foo: { - name: 'foo' - } - }; - const actualMap = normalizeUtil._convertArrayToMap(array, 'name'); - assert.deepEqual(actualMap, expectedMap); + it('should fail to convert array to map', () => assert.throws( + () => normalizeUtil._convertArrayToMap({}, 'name'), + /array required/ + )); }); - it('should fail to convert array to map', () => { - assert.throws( - () => { - normalizeUtil._convertArrayToMap({}, 'name'); - }, - (err) => { - if ((err instanceof Error) && /array required/.test(err)) { - return true; + describe('._filterDataByKeys()', () => { + it('should filter data by keys', () => { + const obj = { + 'name/foo': { + name: 'foo', + removeMe: 'foo' } - return false; - }, - 'unexpected error' - ); - }); + }; + const expectedObj = { + 'name/foo': { + name: 'foo' + } + }; + // include + let actualObj = normalizeUtil._filterDataByKeys(obj, { include: ['name'] }); + assert.deepStrictEqual(actualObj, expectedObj); + // exclude + actualObj = normalizeUtil._filterDataByKeys(obj, { exclude: ['removeMe'] }); + assert.deepStrictEqual(actualObj, expectedObj); + }); - it('should filter data by keys', () => { - const obj = { - 'name/foo': { - name: 'foo', - removeMe: 'foo' - } - }; - const expectedObj = { - 'name/foo': { - name: 'foo' - } - }; - // include - let actualObj = normalizeUtil._filterDataByKeys(obj, { include: ['name'] }); - assert.deepEqual(actualObj, expectedObj); - // exclude - actualObj = normalizeUtil._filterDataByKeys(obj, { exclude: ['removeMe'] }); - assert.deepEqual(actualObj, expectedObj); + it('should not filter array', () => { + const obj = [1, 2, 3]; + const actualObj = normalizeUtil._filterDataByKeys(obj, { include: ['name'] }); + assert.deepStrictEqual(actualObj, obj); + }); }); - it('should not filter array', () => { - const obj = [1, 2, 3]; - const actualObj = normalizeUtil._filterDataByKeys(obj, { include: ['name'] }); - assert.deepEqual(actualObj, obj); + describe('.getAverage()', () => { + it('should get average', () => { + const data = { + tmm0: { + oneMinAverageSystem: 10 + }, + tmm1: { + oneMinAverageSystem: 20 + } + }; + const expectedResult = 15; + const result = normalizeUtil.getAverage({ data, keyWithValue: 'oneMinAverageSystem' }); + assert.deepStrictEqual(result, expectedResult); + }); }); + describe('restructureHostCpuInfo', () => { + const cpuInfoPattern = '^sys/host-info/\\d+::^sys/hostInfo/\\d+/cpuInfo'; + + it('should get cpuInfo properties via regex', () => { + const data = { + 'sys/host-info/0': { + 'sys/hostInfo/0/cpuInfo': { + 'sys/hostInfo/0/cpuInfo/0': { + oneMinAvgSystem: 2 + }, + 'sys/hostInfo/0/cpuInfo/1': { + oneMinAvgSystem: 4 + } + } + }, + 'sys/host-info/1': { + 'sys/hostInfo/1/cpuInfo': { + 'sys/hostInfo/1/cpuInfo/0': { + oneMinAvgSystem: 6 + }, + 'sys/hostInfo/1/cpuInfo/1': { + oneMinAvgSystem: 8 + } + } + }, + 'notsys/host-info/2': { + 'sys/hostInfo/2/cpuInfo': { + 'sys/hostInfo/2/cpuInfo/0': { + oneMinAvgSystem: 77 + }, + 'sys/hostInfo/2/cpuInfo/1': { + oneMinAvgSystem: 78 + } + } + }, + 'sys/host-info/3': { + 'sys/hostInfo/notNumber/cpuInfo': { + 'sys/hostInfo/notNumber/cpuInfo/0': { + oneMinAvgSystem: 87 + }, + 'sys/hostInfo/notNumber/cpuInfo/1': { + oneMinAvgSystem: 88 + } + } + } + }; + const expectedResult = { + 'sys/hostInfo/0/cpuInfo/0': { + oneMinAvgSystem: 2 + }, + 'sys/hostInfo/0/cpuInfo/1': { + oneMinAvgSystem: 4 + }, + 'sys/hostInfo/1/cpuInfo/0': { + oneMinAvgSystem: 6 + }, + 'sys/hostInfo/1/cpuInfo/1': { + oneMinAvgSystem: 8 + } + }; + const result = normalizeUtil.restructureHostCpuInfo({ data, keyPattern: cpuInfoPattern }); + assert.deepStrictEqual(result, expectedResult); + }); - it('should get average', () => { - const data = { - tmm0: { - oneMinAverageSystem: 10 - }, - tmm1: { - oneMinAverageSystem: 20 - } - }; - const expectedResult = 15; + it('should return \'missing data\' if empty data', () => { + const data = {}; - const result = normalizeUtil.getAverage({ data, keyWithValue: 'oneMinAverageSystem' }); - assert.strictEqual(result, expectedResult); - }); + const result = normalizeUtil.restructureHostCpuInfo({ data, keyPattern: cpuInfoPattern }); + assert.deepStrictEqual(result, 'missing data'); + }); - it('should get sum', () => { - const data = { - tmm0: { - clientSideTrafficBitsIn: 10, - clientSideTrafficBitsOut: 20 - }, - tmm1: { - clientSideTrafficBitsIn: 20, - clientSideTrafficBitsOut: 40 - } - }; - const expectedResult = { - clientSideTrafficBitsIn: 30, - clientSideTrafficBitsOut: 60 - }; - - let result = normalizeUtil.getSum({ data }); - assert.deepEqual(result, expectedResult); - - // check empty object is returned - result = normalizeUtil.getSum({ data: '' }); - assert.deepEqual(result, {}); - - // check that child non object does not cause issues - data.mangled = 'foo'; - result = normalizeUtil.getSum({ data }); - assert.deepEqual(result, expectedResult); - }); + it('should return \'missing data\' if no matches', () => { + const data = { + a: { + b: '1' + } + }; - it('should get first key', () => { - const data = { - '10.0.0.1/24': { - description: 'foo' - }, - '10.0.0.2/24': { - description: 'foo' - } - }; - // check multiple branches - let expectedResult = '10.0.0.1/24'; - let result = normalizeUtil.getFirstKey({ data }); - assert.strictEqual(result, expectedResult); - - expectedResult = 'https://10.0.0.1'; - result = normalizeUtil.getFirstKey({ data, splitOnValue: '/', keyPrefix: 'https://' }); - assert.strictEqual(result, expectedResult); - - expectedResult = 'null'; - result = normalizeUtil.getFirstKey({ data: '' }); - assert.strictEqual(result, expectedResult); + const result = normalizeUtil.restructureHostCpuInfo({ data, keyPattern: cpuInfoPattern }); + assert.deepStrictEqual(result, 'missing data'); + }); + }); + describe('.getSum()', () => { + it('should get sum', () => { + const data = { + tmm0: { + clientSideTrafficBitsIn: 10, + clientSideTrafficBitsOut: 20 + }, + tmm1: { + clientSideTrafficBitsIn: 20, + clientSideTrafficBitsOut: 40 + } + }; + const expectedResult = { + clientSideTrafficBitsIn: 30, + clientSideTrafficBitsOut: 60 + }; + + let result = normalizeUtil.getSum({ data }); + assert.deepStrictEqual(result, expectedResult); + + // check empty object is returned + result = normalizeUtil.getSum({ data: '' }); + assert.deepStrictEqual(result, {}); + + // check that child non object does not cause issues + data.mangled = 'foo'; + result = normalizeUtil.getSum({ data }); + assert.deepStrictEqual(result, expectedResult); + }); }); - it('should get percent from keys', () => { - const data = { - total: 10000, - partial: 2000 - }; - const expectedResult = 20; // percent - - const result = normalizeUtil.getPercentFromKeys({ data, totalKey: 'total', partialKey: 'partial' }); - assert.strictEqual(result, expectedResult); + describe('.getFirstKey()', () => { + it('should get first key', () => { + const data = { + '10.0.0.1/24': { + description: 'foo' + }, + '10.0.0.2/24': { + description: 'foo' + } + }; + // check multiple branches + let expectedResult = '10.0.0.1/24'; + let result = normalizeUtil.getFirstKey({ data }); + assert.strictEqual(result, expectedResult); + + expectedResult = 'https://10.0.0.1'; + result = normalizeUtil.getFirstKey({ data, splitOnValue: '/', keyPrefix: 'https://' }); + assert.strictEqual(result, expectedResult); + + expectedResult = 'null'; + result = normalizeUtil.getFirstKey({ data: '' }); + assert.strictEqual(result, expectedResult); + }); }); - it('should format as json', () => { - const data = 'named_key,key1\nname,value'; - const expectedResult = { - name: { - named_key: 'name', - key1: 'value' - } - }; + describe('.getPercentFromKeys()', () => { + it('should get percent from keys', () => { + const data = { + total: 10000, + partial: 2000 + }; + const expectedResult = 20; // percent - const result = normalizeUtil.formatAsJson({ data, type: 'csv', mapKey: 'named_key' }); - assert.deepEqual(result, expectedResult); - }); + const result = normalizeUtil.getPercentFromKeys({ data, totalKey: 'total', partialKey: 'partial' }); + assert.strictEqual(result, expectedResult); + }); - it('should format as json and filter/rename', () => { - const data = 'named_key,key1,key2,key3\nname,value,value,value'; - const expectedResult = { - name: { - named_key: 'name', - key1: 'value', - renamedKey: 'value' - } - }; - - const result = normalizeUtil.formatAsJson({ - data, - type: 'csv', - mapKey: 'named_key', - filterKeys: { exclude: ['key2'] }, - renameKeys: { patterns: { key3: { constant: 'renamedKey' } } } + it('should get percent from nested keys', () => { + const data = { + one: { + total: 10000, + partial: 2000 + }, + two: { + total: 30000, + partial: 4000 + } + }; + const expectedResult = 15; // percent + + const result = normalizeUtil.getPercentFromKeys({ + data, totalKey: 'total', partialKey: 'partial', nestedObjects: true + }); + assert.strictEqual(result, expectedResult); }); - assert.deepEqual(result, expectedResult); - }); - it('should format as json array and skip mapping keys', () => { - const data = 'named_key,key1\nnameOne,valueOne\nnameTwo,valueTwo'; - const expectedResult = [ - { - named_key: 'nameOne', - key1: 'valueOne' - }, - { - named_key: 'nameTwo', - key1: 'valueTwo' - } - ]; + it('should not fail when \'missing data\'', () => { + const data = 'missing data'; - const result = normalizeUtil.formatAsJson({ data, type: 'csv' }); - assert.deepEqual(result, expectedResult); + assert.doesNotThrow(() => normalizeUtil.getPercentFromKeys({ + data, totalKey: 'total', partialKey: 'partial', nestedObjects: true + })); + }); }); - it('should throw error about incorrect type', () => { - const data = 'named_key,key1\nname,value'; + describe('.formatAsJson()', () => { + it('should format as json', () => { + const data = 'named_key,key1\nname,value'; + const expectedResult = { + name: { + named_key: 'name', + key1: 'value' + } + }; - try { - normalizeUtil.formatAsJson({ data, type: 'foo', mapKey: 'named_key' }); - assert.fail('Error expected'); - } catch (err) { - const msg = err.message || err; - assert.notStrictEqual(msg.indexOf('Unsupported type'), -1); - } - }); + const result = normalizeUtil.formatAsJson({ data, type: 'csv', mapKey: 'named_key' }); + assert.deepStrictEqual(result, expectedResult); + }); - it('should restructure rules', () => { - const args = { - data: { - '/Common/_sys_APM_ExchangeSupport_OA_BasicAuth': { - aborts: 0, - avgCycles: 30660, - eventType: 'RULE_INIT', - failures: 0, - maxCycles: 30660, - minCycles: 23832, - priority: 500, - totalExecutions: 4 + it('should format as json and filter/rename', () => { + const data = 'named_key,key1,key2,key3\nname,value,value,value'; + const expectedResult = { + name: { + named_key: 'name', + key1: 'value', + renamedKey: 'value' + } + }; + + const result = normalizeUtil.formatAsJson({ + data, + type: 'csv', + mapKey: 'named_key', + filterKeys: { exclude: ['key2'] }, + renameKeys: { patterns: { key3: { constant: 'renamedKey' } } } + }); + assert.deepStrictEqual(result, expectedResult); + }); + + it('should format as json array and skip mapping keys', () => { + const data = 'named_key,key1\nnameOne,valueOne\nnameTwo,valueTwo'; + const expectedResult = [ + { + named_key: 'nameOne', + key1: 'valueOne' }, - '/Common/_sys_APM_ExchangeSupport_OA_NtlmAuth': { - aborts: 0, - avgCycles: 26028, - eventType: 'RULE_INIT', - failures: 0, - maxCycles: 26028, - minCycles: 23876, - priority: 500, - totalExecutions: 4 + { + named_key: 'nameTwo', + key1: 'valueTwo' } + ]; + + const result = normalizeUtil.formatAsJson({ data, type: 'csv' }); + assert.deepStrictEqual(result, expectedResult); + }); + + it('should throw error about incorrect type', () => { + const data = 'named_key,key1\nname,value'; + + try { + normalizeUtil.formatAsJson({ data, type: 'foo', mapKey: 'named_key' }); + assert.fail('Error expected'); + } catch (err) { + const msg = err.message || err; + assert.notStrictEqual(msg.indexOf('Unsupported type'), -1); } - }; - const expected = { - '/Common/_sys_APM_ExchangeSupport_OA_BasicAuth': { - events: { - RULE_INIT: { + }); + }); + + describe('.restructureRules()', () => { + it('should restructure rules', () => { + const args = { + data: { + '/Common/_sys_APM_ExchangeSupport_OA_BasicAuth': { aborts: 0, avgCycles: 30660, + eventType: 'RULE_INIT', failures: 0, maxCycles: 30660, minCycles: 23832, priority: 500, totalExecutions: 4 - } - } - }, - '/Common/_sys_APM_ExchangeSupport_OA_NtlmAuth': { - events: { - RULE_INIT: { + }, + '/Common/_sys_APM_ExchangeSupport_OA_NtlmAuth': { aborts: 0, avgCycles: 26028, + eventType: 'RULE_INIT', failures: 0, maxCycles: 26028, minCycles: 23876, @@ -267,198 +354,347 @@ describe('Normalize Util', () => { totalExecutions: 4 } } - } - }; - const result = normalizeUtil.restructureRules(args); - assert.deepStrictEqual(result, expected); + }; + const expected = { + '/Common/_sys_APM_ExchangeSupport_OA_BasicAuth': { + events: { + RULE_INIT: { + aborts: 0, + avgCycles: 30660, + failures: 0, + maxCycles: 30660, + minCycles: 23832, + priority: 500, + totalExecutions: 4 + } + } + }, + '/Common/_sys_APM_ExchangeSupport_OA_NtlmAuth': { + events: { + RULE_INIT: { + aborts: 0, + avgCycles: 26028, + failures: 0, + maxCycles: 26028, + minCycles: 23876, + priority: 500, + totalExecutions: 4 + } + } + } + }; + const result = normalizeUtil.restructureRules(args); + assert.deepStrictEqual(result, expected); + }); }); - it('should restructure gslb wideip', () => { - const pools = [ - [ - { - name: 'poolInCommon', - nameReference: { - link: 'https://localhost/mgmt/tm/gtm/pool/a/~Common~poolInCommon' + describe('.restructureGslbWideIp()', () => { + it('should restructure gslb wideip', () => { + const pools = [ + [ + { + name: 'poolInCommon', + nameReference: { + link: 'https://localhost/mgmt/tm/gtm/pool/a/~Common~poolInCommon' + }, + order: 0, + partition: 'Common', + ratio: 1 + } + ], + [ + { + name: 'poolInPartition', + nameReference: { + link: 'https://localhost/mgmt/tm/gtm/pool/a/~Partition~poolInPartition' + }, + order: 0, + partition: 'Partition', + ratio: 10 }, - order: 0, - partition: 'Common', - ratio: 1 + { + name: 'poolInPartitionSubpath', + nameReference: { + link: 'https://localhost/mgmt/tm/gtm/pool/a/~Partition~Subpath~poolInPartitionSubpath' + }, + order: 1, + partition: 'Partition', + subPath: 'Subpath', + ratio: 20 + } + ], + [ + { + name: 'anotherPoolInCommon', + nameReference: { + link: 'https://localhost/mgmt/tm/gtm/pool/a/~Common~anotherPoolInCommon' + }, + order: 0, + partition: 'Common', + ratio: 100 + } + ] + ]; + const poolsCname = [ + undefined, + undefined, + [ + { + name: 'poolCname', + nameReference: { + link: 'https://localhost/mgmt/tm/gtm/pool/cname/~Common~poolCname' + }, + order: 0, + partition: 'Common', + ratio: 1 + } + ] + ]; + const args = { + data: { + 'www.wide.com': { + persisted: 0, + preferred: 2, + rcode: 0, + requests: 8 + } } - ], - [ + }; + const expected = [ { - name: 'poolInPartition', - nameReference: { - link: 'https://localhost/mgmt/tm/gtm/pool/a/~Partition~poolInPartition' - }, - order: 0, - partition: 'Partition', - ratio: 10 + 'www.wide.com': { + persisted: 0, + preferred: 2, + rcode: 0, + requests: 8, + pools: ['/Common/poolInCommon'] + } }, { - name: 'poolInPartitionSubpath', - nameReference: { - link: 'https://localhost/mgmt/tm/gtm/pool/a/~Partition~Subpath~poolInPartitionSubpath' - }, - order: 1, - partition: 'Partition', - subPath: 'Subpath', - ratio: 20 - } - ], - [ - { - name: 'anotherPoolInCommon', - nameReference: { - link: 'https://localhost/mgmt/tm/gtm/pool/a/~Common~anotherPoolInCommon' - }, - order: 0, - partition: 'Common', - ratio: 100 - } - ] - ]; - const poolsCname = [ - undefined, - undefined, - [ + 'www.wide.com': { + persisted: 0, + preferred: 2, + rcode: 0, + requests: 8, + pools: [ + '/Partition/poolInPartition', + '/Partition/Subpath/poolInPartitionSubpath' + ] + } + }, { - name: 'poolCname', - nameReference: { - link: 'https://localhost/mgmt/tm/gtm/pool/cname/~Common~poolCname' - }, - order: 0, - partition: 'Common', - ratio: 1 - } - ] - ]; - const args = { - data: { - 'www.wide.com': { - persisted: 0, - preferred: 2, - rcode: 0, - requests: 8 - } - } - }; - const expected = [ - { - 'www.wide.com': { - persisted: 0, - preferred: 2, - rcode: 0, - requests: 8, - pools: ['/Common/poolInCommon'] - } - }, - { - 'www.wide.com': { - persisted: 0, - preferred: 2, - rcode: 0, - requests: 8, - pools: [ - '/Partition/poolInPartition', - '/Partition/Subpath/poolInPartitionSubpath' - ] - } - }, - { - 'www.wide.com': { - persisted: 0, - preferred: 2, - rcode: 0, - requests: 8, - pools: [ - '/Common/anotherPoolInCommon', - '/Common/poolCname' - ] + 'www.wide.com': { + persisted: 0, + preferred: 2, + rcode: 0, + requests: 8, + pools: [ + '/Common/anotherPoolInCommon', + '/Common/poolCname' + ] + } } - } - ]; - pools.forEach((p, index) => { - args.data['www.wide.com'].pools = p; - args.data['www.wide.com'].poolsCname = poolsCname[index]; - const actual = normalizeUtil.restructureGslbWideIp(args); - assert.deepEqual(actual, expected[index]); + ]; + pools.forEach((p, index) => { + args.data['www.wide.com'].pools = p; + args.data['www.wide.com'].poolsCname = poolsCname[index]; + const actual = normalizeUtil.restructureGslbWideIp(args); + assert.deepStrictEqual(actual, expected[index]); + }); }); - }); - it('should restructure gslb pool with members', () => { - const args = { - data: { - alternateMode: 'round-robin', - limitMaxBps: 100, - membersReference: { - entries: { - 'https://localhost/mgmt/tm/gtm/pool/a/~Common~ts_a_pool/members/vs1:~Common~server1/stats': { - nestedStats: { - selfLink: 'https://localhost/mgmt/tm/gtm/pool/a/~Common~ts_a_pool/members/vs1:~Common~server1/stats?ver=13.1.1.4', - entries: { - alternate: { value: 20 }, - 'status.availabilityState': { description: 'offline' } + it('should restructure gslb pool with members', () => { + const args = { + data: { + alternateMode: 'round-robin', + limitMaxBps: 100, + membersReference: { + entries: { + 'https://localhost/mgmt/tm/gtm/pool/a/~Common~ts_a_pool/members/vs1:~Common~server1/stats': { + nestedStats: { + selfLink: 'https://localhost/mgmt/tm/gtm/pool/a/~Common~ts_a_pool/members/vs1:~Common~server1/stats?ver=13.1.1.4', + entries: { + alternate: { value: 20 }, + 'status.availabilityState': { description: 'offline' } + } } } - } - }, - items: [ - { + }, + items: [ + { + fullPath: '/Common/server:vs1', + name: 'server1:vs1', + selfLink: 'https://localhost/mgmt/tm/gtm/pool/a/~Common~ts_a_pool/members/~Common~server1:vs1?ver=13.1.1.4', + memberOrder: 100, + monitor: 'default' + } + ] + } + } + }; + const expected = { + 'https://localhost/mgmt/tm/gtm/pool/a/~Common~ts_a_pool/members/vs1:~Common~server1/stats': { + nestedStats: { + selfLink: 'https://localhost/mgmt/tm/gtm/pool/a/~Common~ts_a_pool/members/vs1:~Common~server1/stats?ver=13.1.1.4', + entries: { + alternate: { value: 20 }, + 'status.availabilityState': { description: 'offline' }, fullPath: '/Common/server:vs1', name: 'server1:vs1', selfLink: 'https://localhost/mgmt/tm/gtm/pool/a/~Common~ts_a_pool/members/~Common~server1:vs1?ver=13.1.1.4', memberOrder: 100, monitor: 'default' } - ] - } - } - }; - const expected = { - 'https://localhost/mgmt/tm/gtm/pool/a/~Common~ts_a_pool/members/vs1:~Common~server1/stats': { - nestedStats: { - selfLink: 'https://localhost/mgmt/tm/gtm/pool/a/~Common~ts_a_pool/members/vs1:~Common~server1/stats?ver=13.1.1.4', - entries: { - alternate: { value: 20 }, - 'status.availabilityState': { description: 'offline' }, - fullPath: '/Common/server:vs1', - name: 'server1:vs1', - selfLink: 'https://localhost/mgmt/tm/gtm/pool/a/~Common~ts_a_pool/members/~Common~server1:vs1?ver=13.1.1.4', - memberOrder: 100, - monitor: 'default' } } - } - }; - const actual = normalizeUtil.restructureGslbPool(args); - // merge config with stats - assert.deepEqual(actual.membersReference.entries, expected); - }); + }; + const actual = normalizeUtil.restructureGslbPool(args); + // merge config with stats + assert.deepStrictEqual(actual.membersReference.entries, expected); + }); - it('should restructure gslb pool without members', () => { - const args = { - data: { + it('should restructure gslb pool without members', () => { + const args = { + data: { + alternateMode: 'packet-rate', + dynamicRatio: 'disabled', + enabled: true, + fallbackMode: 'quality-of-service', + loadBalancingMode: 'virtual-server-capacity', + membersReference: { + items: [] + } + } + }; + const expected = { alternateMode: 'packet-rate', dynamicRatio: 'disabled', enabled: true, fallbackMode: 'quality-of-service', loadBalancingMode: 'virtual-server-capacity', - membersReference: { - items: [] + membersReference: {} + }; + const actual = normalizeUtil.restructureGslbPool(args); + assert.deepStrictEqual(actual, expected); + }); + }); + + describe('.normalizeMACAddress()', () => { + it('should normalize mac address (string)', () => { + const inputOutputMap = { + '00:11:22:33:44:55': '00:11:22:33:44:55', + '0:a:B:c:D:e': '00:0A:0B:0C:0D:0E' + }; + Object.keys(inputOutputMap).forEach((input) => { + assert.strictEqual( + normalizeUtil.normalizeMACAddress({ data: input }), + inputOutputMap[input] + ); + }); + }); + + it('should normalize mac address (object)', () => { + const data = { + obj1: [ + { + mac: '0:a:B:c:D:e' + } + ], + mac: '0:a:B:c:D:e' + }; + const properties = ['mac']; + const expected = { + obj1: [ + { + mac: '00:0A:0B:0C:0D:0E' + } + ], + mac: '00:0A:0B:0C:0D:0E' + }; + assert.deepStrictEqual( + normalizeUtil.normalizeMACAddress({ data, properties }), + expected + ); + }); + + it('should return data as is when no colon in it', () => { + assert.strictEqual( + normalizeUtil.normalizeMACAddress({ data: 'missing data' }), + 'missing data' + ); + }); + }); + + describe('restructureVirtualServerProfiles', () => { + it('should restructure virtual server profiles (without items property)', () => { + const data = { + vs1: { + profiles: { + name: 'profiles' + } } - } - }; - const expected = { - alternateMode: 'packet-rate', - dynamicRatio: 'disabled', - enabled: true, - fallbackMode: 'quality-of-service', - loadBalancingMode: 'virtual-server-capacity', - membersReference: {} - }; - const actual = normalizeUtil.restructureGslbPool(args); - assert.deepEqual(actual, expected); + }; + const expected = { + vs1: { + profiles: {} + } + }; + assert.deepStrictEqual(normalizeUtil.restructureVirtualServerProfiles({ data }), expected); + }); + + it('should restructure virtual server profiles (with items property but without profiles data)', () => { + const data = { + vs1: { + profiles: { + name: 'profiles', + items: { + name: 'items' + } + } + } + }; + const expected = { + vs1: { + profiles: {} + } + }; + assert.deepStrictEqual(normalizeUtil.restructureVirtualServerProfiles({ data }), expected); + }); + + it('should restructure virtual server profiles', () => { + const data = { + vs1: { + profiles: { + name: 'profiles', + items: { + name: 'items', + profile1: { + name: 'profile1', + tenant: 'Common' + }, + profile2: { + name: 'profile2', + tenant: 'Common' + } + } + } + } + }; + const expected = { + vs1: { + profiles: { + profile1: { + name: 'profile1', + tenant: 'Common' + }, + profile2: { + name: 'profile2', + tenant: 'Common' + } + } + } + }; + assert.deepStrictEqual(normalizeUtil.restructureVirtualServerProfiles({ data }), expected); + }); }); }); diff --git a/test/unit/outputTests.js b/test/unit/outputTests.js index 9d50beec..27926943 100644 --- a/test/unit/outputTests.js +++ b/test/unit/outputTests.js @@ -8,45 +8,43 @@ 'use strict'; -const assert = require('assert'); -const fs = require('fs'); -const Ajv = require('ajv'); - -/* eslint-disable global-require */ - -const DISABLED_FOLDERS = ['request_logs', 'avr', 'consumers']; +/* eslint-disable import/order */ +require('./shared/restoreCache')(); -function validateAgainstSchema(data, schema) { - const ajv = new Ajv({ useDefaults: true }); - const validator = ajv.compile(schema); - const valid = validator(data); - if (!valid) { - return { errors: validator.errors }; - } - return true; -} +const Ajv = require('ajv'); +const chai = require('chai'); +const chaiAsPromised = require('chai-as-promised'); +const fs = require('fs'); +chai.use(chaiAsPromised); +const assert = chai.assert; describe('Example Output', () => { - after(() => { - Object.keys(require.cache).forEach((key) => { - delete require.cache[key]; - }); - }); + function validateAgainstSchema(data, schema) { + const ajv = new Ajv({ useDefaults: true }); + const validator = ajv.compile(schema); + const valid = validator(data); + if (!valid) { + return { errors: validator.errors }; + } + return true; + } // baseDir contains 1+ folders, each of which contain a schema.json and output.json file const baseDir = `${__dirname}/../../examples/output`; const schemaDir = `${__dirname}/../../shared/output_schemas`; - const dirs = fs.readdirSync(baseDir); - dirs.forEach((dir) => { - if (DISABLED_FOLDERS.indexOf(dir) !== -1) { + const disabledFolders = ['request_logs', 'avr', 'consumers']; + + fs.readdirSync(baseDir).forEach((dir) => { + if (disabledFolders.indexOf(dir) !== -1) { return; } const schemaFile = `${schemaDir}/${dir}_schema.json`; // example directory name + _schema.json const outputFile = `${baseDir}/${dir}/output.json`; - it(`should validate output in ${dir}`, () => { + + it(`should validate output in '${baseDir}/${dir}'`, () => { const schema = JSON.parse(fs.readFileSync(schemaFile)); const data = JSON.parse(fs.readFileSync(outputFile)); diff --git a/test/unit/persistentStorageTests.js b/test/unit/persistentStorageTests.js index 156a553b..efdf09d3 100644 --- a/test/unit/persistentStorageTests.js +++ b/test/unit/persistentStorageTests.js @@ -8,466 +8,493 @@ 'use strict'; -const assert = require('assert'); +/* eslint-disable import/order */ -/* eslint-disable global-require */ +require('./shared/restoreCache')(); -describe('PersistentStorage', () => { - let psModule; - let persistentStorage; - let restStorage; - let restWorker; +const chai = require('chai'); +const chaiAsPromised = require('chai-as-promised'); +const sinon = require('sinon'); - beforeEach(() => { - psModule = require('../../src/lib/persistentStorage.js'); - persistentStorage = psModule.persistentStorage; - restWorker = { +const persistentStorage = require('../../src/lib/persistentStorage'); +const testUtil = require('./shared/util'); + +chai.use(chaiAsPromised); +const assert = chai.assert; + +describe('Persistent Storage', () => { + describe('persistentStorage', () => { + const restWorker = { loadState: (first, cb) => { cb(null, {}); }, saveState: (first, state, cb) => { cb(null); } }; - restStorage = new psModule.RestStorage(restWorker); - restStorage._cache = null; - persistentStorage.storage = restStorage; - }); - afterEach(() => { - Object.keys(require.cache).forEach((key) => { - delete require.cache[key]; + let persistentStorageInst; + let restStorage; + + beforeEach(() => { + persistentStorageInst = persistentStorage.persistentStorage; + restStorage = new persistentStorage.RestStorage(restWorker); + restStorage._cache = null; + persistentStorageInst.storage = restStorage; + }); + afterEach(() => { + sinon.restore(); }); - }); - it('should fail to load when restWorker returns error (restStorage)', () => { - const errMsg = 'loadStateError'; - restWorker.loadState = (first, cb) => { cb(new Error(errMsg), {}); }; - return persistentStorage.load() - .then(() => { - assert.fail('Should throw an error'); - }) - .catch((err) => { - if (err.code === 'ERR_ASSERTION') return Promise.reject(err); - if (RegExp(errMsg).test(err)) return Promise.resolve(); - assert.fail(err); - return Promise.reject(err); + it('should return data copy on attempt to \'get\' it', () => { + const baseState = { + _data_: { + somekey: [1, 2, 3, 4, 5] + } + }; + sinon.stub(restWorker, 'loadState').callsFake((first, cb) => { + cb(null, baseState); }); - }); + return assert.isFulfilled( + persistentStorageInst.load() + .then(() => persistentStorageInst.get('somekey')) + .then((value) => { + value.push(6); + assert.deepStrictEqual(restStorage._cache._data_.somekey, [1, 2, 3, 4, 5]); + }) + ); + }); - it('should fail to save when restWorker returns error (restStorage)', () => { - const errMsg = 'saveStateError'; - restWorker.saveState = (first, state, cb) => cb(new Error(errMsg)); - return persistentStorage.load().then(() => persistentStorage.save()) - .then(() => { - assert.fail('Should throw an error'); - }) - .catch((err) => { - if (err.code === 'ERR_ASSERTION') return Promise.reject(err); - if (RegExp(errMsg).test(err)) return Promise.resolve(); - assert.fail(err); - return Promise.reject(err); + it('should return make data copy on attempt to \'save\' it', () => { + sinon.stub(restWorker, 'loadState').callsFake((first, cb) => { + cb(null, { _data_: {} }); }); - }); - - it('should fail to set when restWorker returns error (restStorage)', () => { - const errMsg = 'setDataError'; - restWorker.saveState = (first, second, cb) => { cb(new Error(errMsg), {}); }; - return persistentStorage.load() - .then(() => persistentStorage.set('somekey', 'somedata')) - .then(() => { - assert.fail('Should throw an error'); - }) - .catch((err) => { - if (err.code === 'ERR_ASSERTION') return Promise.reject(err); - if (RegExp(errMsg).test(err)) return Promise.resolve(); - assert.fail(err); - return Promise.reject(err); + sinon.stub(restWorker, 'saveState').callsFake((first, currentState, cb) => { + cb(null); }); + return assert.isFulfilled( + persistentStorageInst.load() + .then(() => { + const data = [1]; + const promise = persistentStorageInst.set('somekey', data); + data.push(2); + return promise; + }) + .then(() => { + assert.deepStrictEqual(restStorage._cache._data_.somekey, [1]); + }) + ); + }); }); - it('should fail to remove when restWorker returns error (restStorage)', () => { - const errMsg = 'removeDataError'; - restWorker.saveState = (first, second, cb) => { cb(new Error(errMsg), {}); }; - return persistentStorage.load() - .then(() => persistentStorage.remove('somekey')) - .then(() => { - assert.fail('Should throw an error'); - }) - .catch((err) => { - if (err.code === 'ERR_ASSERTION') return Promise.reject(err); - if (RegExp(errMsg).test(err)) return Promise.resolve(); - assert.fail(err); - return Promise.reject(err); - }); - }); + describe('restStorage', () => { + const restWorker = { + loadState: (first, cb) => { cb(null, {}); }, + saveState: (first, state, cb) => { cb(null); } + }; - it('should fail to load when no restWorker provided (restStorage)', () => { - restStorage.restWorker = null; - try { - persistentStorage.load(); - assert.fail('Should throw an error'); - } catch (err) { - if (!/restWorker is not specified/.test(err)) assert.fail(err); - } - }); + let persistentStorageInst; + let restStorage; - it('should fail to save when no restWorker provided (restStorage)', () => { - restStorage.restWorker = null; - try { - persistentStorage.save(); - assert.fail('Should throw an error'); - } catch (err) { - if (!/restWorker is not specified/.test(err)) assert.fail(err); - } - }); + beforeEach(() => { + persistentStorageInst = persistentStorage.persistentStorage; + restStorage = new persistentStorage.RestStorage(restWorker); + restStorage._cache = null; + persistentStorageInst.storage = restStorage; + }); + afterEach(() => { + sinon.restore(); + }); - it('should load empty state (restStorage)', () => { - restWorker.loadState = (first, cb) => { cb(null, null); }; - return persistentStorage.load() - .then((state) => { - assert.deepEqual(state, {}); - assert.deepEqual(restStorage._cache, { _data_: {} }); - return Promise.resolve(); + it('should fail to load when restWorker returns error', () => { + const errMsg = 'loadStateError'; + sinon.stub(restWorker, 'loadState').callsFake((first, cb) => { + cb(new Error(errMsg), {}); }); - }); - - it('should fail to save null state (restStorage)', () => { - restStorage._cache = null; - try { - persistentStorage.save(); - assert.fail('Should throw an error'); - } catch (err) { - if (!/no loaded state/.test(err)) assert.fail(err); - } - }); + const promise = persistentStorageInst.load(); + return assert.isRejected(promise, RegExp(errMsg)); + }); - it('should save loaded state (restStorage)', () => persistentStorage.load() - .then(() => persistentStorage.save())); - - it('should return undefined when key doesn\'t exists', () => persistentStorage.load() - .then(() => persistentStorage.get('undefinedkey')) - .then((data) => { - assert.strictEqual(data, undefined); - return Promise.resolve(); - })); - - it('should return data when key exists (restStorage)', () => { - const someKey = 'somekey'; - const someData = 'somedata'; - - return persistentStorage.load() - .then(() => persistentStorage.set(someKey, someData)) - .then(() => persistentStorage.get(someKey)) - .then((data) => { - assert.strictEqual(data, someData); - return Promise.resolve(); + it('should fail to save when restWorker returns error', () => { + const errMsg = 'saveStateError'; + sinon.stub(restWorker, 'saveState').callsFake((first, state, cb) => { + cb(new Error(errMsg), {}); }); - }); - - it('should removed data when key doesn\'t exists (restStorage)', () => { - const someKey = 'somekey'; + const promise = persistentStorageInst.load() + .then(() => persistentStorageInst.save()); + return assert.isRejected(promise, RegExp(errMsg)); + }); - return persistentStorage.load() - .then(() => persistentStorage.remove(someKey)) - .then(() => persistentStorage.get(someKey)) - .then((data) => { - assert.strictEqual(data, undefined); - return Promise.resolve(); + it('should fail to set when restWorker returns error', () => { + const errMsg = 'setDataError'; + sinon.stub(restWorker, 'saveState').callsFake((first, state, cb) => { + cb(new Error(errMsg), {}); }); - }); - - it('should removed data when key exists (restStorage)', () => { - const someKey = 'somekey'; - const someData = 'somedata'; + const promise = persistentStorageInst.load() + .then(() => persistentStorageInst.set('somekey', 'somedata')); + return assert.isRejected(promise, RegExp(errMsg)); + }); - return persistentStorage.load() - .then(() => persistentStorage.set(someKey, someData)) - .then(() => persistentStorage.get(someKey)) - .then((data) => { - assert.strictEqual(data, someData); - return Promise.resolve(); + it('should fail to remove when restWorker returns error', () => { + const errMsg = 'removeDataError'; + sinon.stub(restWorker, 'saveState').callsFake((first, state, cb) => { + cb(new Error(errMsg), {}); }); - }); - - it('should load pre-existing old-version state (restStorage)', () => { - const baseState = { - config: { - key: 'somedata' - } - }; + const promise = persistentStorageInst.load() + .then(() => persistentStorageInst.remove('somekey')); + return assert.isRejected(promise, RegExp(errMsg)); + }); - restWorker.loadState = (first, cb) => { cb(null, JSON.parse(JSON.stringify(baseState))); }; - return persistentStorage.load() - .then((state) => { - assert.deepEqual(state, baseState); - return Promise.resolve(); - }); - }); + it('should fail to load when no restWorker provided', () => { + restStorage.restWorker = null; + assert.throws( + () => persistentStorageInst.load(), + /restWorker is not specified/ + ); + }); - it('should load pre-existing current-version state (restStorage)', () => { - const baseState = { - _data_: { - somekey: 'somedata' - } - }; + it('should fail to save when no restWorker provided', () => { + restStorage.restWorker = null; + assert.throws( + () => persistentStorageInst.save(), + /restWorker is not specified/ + ); + }); - restWorker.loadState = (first, cb) => { cb(null, baseState); }; - return persistentStorage.load() - .then((state) => { - assert.deepEqual(restStorage._cache, baseState); - assert.deepEqual(state, baseState._data_); - return Promise.resolve(); - }); - }); + it('should load empty state', () => { + sinon.stub(restWorker, 'loadState').callsFake((first, cb) => { cb(null, null); }); + return assert.isFulfilled( + persistentStorageInst.load() + .then((state) => { + assert.deepStrictEqual(state, {}); + assert.deepStrictEqual(restStorage._cache, { _data_: {} }); + }) + ); + }); - it('should preserve service properties on load and save (restStorage)', () => { - const loadState = { - _data_: { - somekey: 'somedata' - }, - sp1: 100, - sp2: 200 - }; - const saveState = { - _data_: { - somekey: 'somedata' - }, - sp1: 200, - sp2: 300 - }; + it('should fail to save null state', () => { + restStorage._cache = null; + assert.throws( + () => persistentStorageInst.save(), + /no loaded state/ + ); + }); - restWorker.loadState = (first, cb) => { cb(null, loadState); }; - restWorker.saveState = (first, currentState, cb) => { - currentState.sp1 = saveState.sp1; - currentState.sp2 = saveState.sp2; - cb(null); - }; + it('should save loaded state', () => assert.isFulfilled( + persistentStorageInst.load() + .then(() => persistentStorageInst.save()) + )); + + it('should return undefined when key doesn\'t exists', () => assert.becomes( + persistentStorageInst.load().then(() => persistentStorageInst.get('undefinedkey')), + undefined + )); + + it('should return data when key exists', () => assert.becomes( + persistentStorageInst.load() + .then(() => persistentStorageInst.set('someKey', 'someData')) + .then(() => persistentStorageInst.get('someKey')), + 'someData' + )); + + it('should remove data when key doesn\'t exists', () => assert.becomes( + persistentStorageInst.load() + .then(() => persistentStorageInst.remove('someKey')) + .then(() => persistentStorageInst.get('someKey')), + undefined + )); + + it('should remove data when key exists', () => assert.becomes( + persistentStorageInst.load() + .then(() => persistentStorageInst.set('someKey', 'someData')) + .then(() => persistentStorageInst.get('someKey')), + 'someData' + )); + + it('should load pre-existing state (old-version)', () => { + const baseState = { + config: { + key: 'somedata' + } + }; + sinon.stub(restWorker, 'loadState').callsFake((first, cb) => { + cb(null, testUtil.deepCopy(baseState)); + }); + return assert.becomes(persistentStorageInst.load(), baseState); + }); - return persistentStorage.load() - .then((state) => { - assert.deepEqual(restStorage._cache, loadState); - assert.deepEqual(state, loadState._data_); - - return persistentStorage.save(); - }) - .then(() => { - const cache = restStorage._cache; - assert.deepEqual(cache, saveState); - assert.deepEqual(cache._data_, saveState._data_); - return Promise.resolve(); + it('should load pre-existing state (new-version)', () => { + const baseState = { + _data_: { + somekey: 'somedata' + } + }; + sinon.stub(restWorker, 'loadState').callsFake((first, cb) => { + cb(null, testUtil.deepCopy(baseState)); }); - }); + return assert.isFulfilled( + persistentStorageInst.load() + .then((state) => { + assert.deepStrictEqual(restStorage._cache, baseState); + assert.deepStrictEqual(state, baseState._data_); + }) + ); + }); - it('should save only once in current event cycle (restStorage)', () => { - const restState = { - _data_: {} - }; - let saveCounter = 0; - const key = 'somekey'; - const expectedValue = 'expectedValue'; - - restWorker.saveState = (first, currentState, cb) => { - restState._data_ = currentState._data_; - saveCounter += 1; - cb(null, restState); - }; - return persistentStorage.load() - .then(() => Promise.all([ - persistentStorage.set(key, 1), - persistentStorage.set(key, 2), - persistentStorage.set(key, 3), - persistentStorage.set(key, expectedValue) - ])) - .then(() => persistentStorage.get(key)) - .then((value) => { - assert.strictEqual(saveCounter, 1); - assert.strictEqual(value, expectedValue); - return Promise.resolve(); + it('should preserve service properties on load and save', () => { + const loadState = { + _data_: { + somekey: 'somedata' + }, + sp1: 100, + sp2: 200 + }; + const saveState = { + _data_: { + somekey: 'somedata' + }, + sp1: 200, + sp2: 300 + }; + sinon.stub(restWorker, 'loadState').callsFake((first, cb) => { + cb(null, loadState); }); - }); + sinon.stub(restWorker, 'saveState').callsFake((first, currentState, cb) => { + currentState.sp1 = saveState.sp1; + currentState.sp2 = saveState.sp2; + cb(null); + }); + return assert.isFulfilled( + persistentStorageInst.load() + .then((state) => { + assert.deepStrictEqual(restStorage._cache, loadState); + assert.deepStrictEqual(state, loadState._data_); + return persistentStorageInst.save(); + }) + .then(() => { + const cache = restStorage._cache; + assert.deepStrictEqual(cache, saveState); + assert.deepStrictEqual(cache._data_, saveState._data_); + }) + ); + }); - it('should load only once in current event cycle (restStorage)', () => { - const restState = { - _data_: {} - }; - let saveCounter = 0; + it('should save only once in current event cycle', () => { + const restState = { + _data_: {} + }; + let saveCounter = 0; - restWorker.loadState = (first, cb) => { - saveCounter += 1; - cb(null, restState); - }; - return persistentStorage.load() // load #1 - .then(() => Promise.all([ // load #2 - persistentStorage.load(), - persistentStorage.load(), - persistentStorage.load(), - persistentStorage.load(), - persistentStorage.load() - ])) - .then(() => Promise.all([ // load #3 - persistentStorage.load(), - persistentStorage.load(), - persistentStorage.load(), - persistentStorage.load(), - persistentStorage.load() - ])) - .then(() => { - assert.strictEqual(saveCounter, 3); - return Promise.resolve(); + sinon.stub(restWorker, 'saveState').callsFake((first, currentState, cb) => { + restState._data_ = currentState._data_; + saveCounter += 1; + cb(null, restState); }); - }); + return assert.isFulfilled( + persistentStorageInst.load() + .then(() => Promise.all([ + persistentStorageInst.set('somekey', 1), + persistentStorageInst.set('somekey', 2), + persistentStorageInst.set('somekey', 3), + persistentStorageInst.set('somekey', 'expectedValue') + ])) + .then(() => persistentStorageInst.get('somekey')) + .then((value) => { + assert.strictEqual(saveCounter, 1); + assert.strictEqual(value, 'expectedValue'); + }) + ); + }); - it('should fail to save when unable to copy data (restStorage)', () => { - restStorage._cache = { - _data_: {} - }; - restStorage._cache._data_.cache = restStorage._cache; - - return persistentStorage.save() - .then(() => { - assert.fail('Should throw an error'); - }) - .catch((err) => { - if (err.code === 'ERR_ASSERTION') return Promise.reject(err); - if (/Converting circular structure to JSON/.test(err)) return Promise.resolve(); - assert.fail(err); - return Promise.reject(err); - }); - }); + it('should load only once in current event cycle', () => { + const restState = { + _data_: {} + }; + let saveCounter = 0; - it('should be able to save data after previous attempt (restStorage)', () => { - const restState = { - _data_: {} - }; - let saveCounter = 0; - const key = 'somekey'; - const expectedValue = 'expectedValue'; - - restWorker.saveState = (first, currentState, cb) => { - restState._data_ = currentState._data_; - saveCounter += 1; - cb(null, restState); - }; - return persistentStorage.load() // load #1 - .then(() => Promise.all([ // save #1 - persistentStorage.set(key, 1), - persistentStorage.set(key, 2), - persistentStorage.set(key, 3), - persistentStorage.set(key, 4) - ])) - .then(() => Promise.all([ // save #2 - persistentStorage.set(key, 1), - persistentStorage.set(key, 2), - persistentStorage.set(key, 10), - persistentStorage.set(key, expectedValue) - ])) - .then(() => persistentStorage.get(key)) - .then((value) => { - assert.strictEqual(value, expectedValue); - assert.strictEqual(saveCounter, 2); - return Promise.resolve(); + sinon.stub(restWorker, 'loadState').callsFake((first, cb) => { + saveCounter += 1; + cb(null, restState); }); - }); + return assert.isFulfilled( + persistentStorageInst.load() // load #1 + .then(() => Promise.all([ // load #2 + persistentStorageInst.load(), + persistentStorageInst.load(), + persistentStorageInst.load(), + persistentStorageInst.load(), + persistentStorageInst.load() + ])) + .then(() => Promise.all([ // load #3 + persistentStorageInst.load(), + persistentStorageInst.load(), + persistentStorageInst.load(), + persistentStorageInst.load(), + persistentStorageInst.load() + ])) + .then(() => { + assert.strictEqual(saveCounter, 3); + }) + ); + }); - it('should be able to save / load in current cycle (restStorage)', () => { - const restState = { - _data_: {} - }; - let slCounter = 0; - const loadIdx = []; - const saveIdx = []; + it('should fail to save when unable to copy data', () => { + restStorage._cache = { + _data_: {} + }; + restStorage._cache._data_.cache = restStorage._cache; + return assert.isRejected(persistentStorageInst.save(), /Converting circular structure to JSON/); + }); + it('should be able to save data after previous attempt', () => { + const restState = { + _data_: {} + }; + let saveCounter = 0; - restWorker.saveState = (first, currentState, cb) => { - restState._data_ = currentState._data_; - saveIdx.push(slCounter); - slCounter += 1; - cb(null, restState); - }; + sinon.stub(restWorker, 'saveState').callsFake((first, currentState, cb) => { + restState._data_ = currentState._data_; + saveCounter += 1; + cb(null, restState); + }); + return assert.isFulfilled( + persistentStorageInst.load() // load #1 + .then(() => Promise.all([ // save #1 + persistentStorageInst.set('somekey', 1), + persistentStorageInst.set('somekey', 2), + persistentStorageInst.set('somekey', 3), + persistentStorageInst.set('somekey', 4) + ])) + .then(() => Promise.all([ // save #2 + persistentStorageInst.set('somekey', 1), + persistentStorageInst.set('somekey', 2), + persistentStorageInst.set('somekey', 10), + persistentStorageInst.set('somekey', 'expectedValue') + ])) + .then(() => persistentStorageInst.get('somekey')) + .then((value) => { + assert.strictEqual(value, 'expectedValue'); + assert.strictEqual(saveCounter, 2); + }) + ); + }); - restWorker.loadState = (first, cb) => { - loadIdx.push(slCounter); - slCounter += 1; - cb(null, restState); - }; + it('should be able to save / load in current cycle', () => { + const restState = { + _data_: {} + }; + let saveCounter = 0; + let loadCounter = 0; + + sinon.stub(restWorker, 'saveState').callsFake((first, currentState, cb) => { + restState._data_ = currentState._data_; + saveCounter += 1; + cb(null, restState); + }); + sinon.stub(restWorker, 'loadState').callsFake((first, cb) => { + loadCounter += 1; + cb(null, restState); + }); + return assert.isFulfilled( + persistentStorageInst.load() // load #1 + .then(() => Promise.all([ // load #2, save #1 + persistentStorageInst.set('somekey1', 1), + persistentStorageInst.set('somekey2', 2), + persistentStorageInst.set('somekey3', 3), + persistentStorageInst.load(), + persistentStorageInst.set('somekey4', 4), + persistentStorageInst.load(), + persistentStorageInst.set('somekey5', 6), + persistentStorageInst.load(), + persistentStorageInst.set('somekey6', 1) + ])) + .then(() => { + assert.strictEqual(saveCounter, 1); + assert.strictEqual(loadCounter, 2); + }) + ); + }); - return persistentStorage.load() // load #1 - .then(() => Promise.all([ // load #2, save #1 - persistentStorage.set('somekey1', 1), - persistentStorage.set('somekey2', 2), - persistentStorage.set('somekey3', 3), - persistentStorage.load(), - persistentStorage.set('somekey4', 4), - persistentStorage.load(), - persistentStorage.set('somekey5', 6), - persistentStorage.load(), - persistentStorage.set('somekey6', 1) - ])) - .then(() => { - assert.strictEqual(saveIdx.length, 1); - assert.strictEqual(loadIdx.length, 2); - return Promise.resolve(); + it('should preserve load-save order', () => { + const restState = { + _data_: {} + }; + let counter = 0; + let saveIdx = null; + let loadIdx = null; + + sinon.stub(restWorker, 'saveState').callsFake((first, currentState, cb) => { + restState._data_ = currentState._data_; + saveIdx = counter; + counter += 1; + cb(null, restState); + }); + sinon.stub(restWorker, 'loadState').callsFake((first, cb) => { + loadIdx = counter; + counter += 1; + cb(null, restState); }); - }); - it('should preserve load-save order (restStorage)', () => { - const restState = { - _data_: {} - }; - let slCounter = 0; - let loadIdx = null; - let saveIdx = null; - - restWorker.saveState = (first, currentState, cb) => { - restState._data_ = currentState._data_; - saveIdx = slCounter; - slCounter += 1; - cb(null, restState); - }; + persistentStorageInst.storage._cache = restState; + persistentStorageInst.load(); // load #1 + persistentStorageInst.save(); // save #1 + persistentStorageInst.load(); // load #1 + return assert.isFulfilled( + persistentStorageInst.save() // save #1 + .then(() => { + assert.strictEqual(loadIdx, 0); + assert.strictEqual(saveIdx, 1); + }) + ); + }); - restWorker.loadState = (first, cb) => { - loadIdx = slCounter; - slCounter += 1; - cb(null, restState); - }; - persistentStorage.storage._cache = restState; - persistentStorage.load(); // load #1 - persistentStorage.save(); // save #1 - persistentStorage.load(); // load #1 ! - return persistentStorage.save() // save #1 - .then(() => { - assert.strictEqual(loadIdx, 0); - assert.strictEqual(saveIdx, 1); - return Promise.resolve(); + it('should preserve save-load order', () => { + const restState = { + _data_: {} + }; + let counter = 0; + let saveIdx = null; + let loadIdx = null; + + sinon.stub(restWorker, 'saveState').callsFake((first, currentState, cb) => { + restState._data_ = currentState._data_; + saveIdx = counter; + counter += 1; + cb(null, restState); + }); + sinon.stub(restWorker, 'loadState').callsFake((first, cb) => { + loadIdx = counter; + counter += 1; + cb(null, restState); }); - }); - it('should preserve save-load order (restStorage)', () => { - const restState = { - _data_: {} - }; - let slCounter = 0; - let loadIdx = null; - let saveIdx = null; - - restWorker.saveState = (first, currentState, cb) => { - restState._data_ = currentState._data_; - saveIdx = slCounter; - slCounter += 1; - cb(null, restState); - }; + persistentStorageInst.storage._cache = restState; + persistentStorageInst.save(); // load #1 + persistentStorageInst.load(); // save #1 + persistentStorageInst.save(); // load #1 ! + return assert.isFulfilled( + persistentStorageInst.load() // save #1 + .then(() => { + assert.strictEqual(loadIdx, 1); + assert.strictEqual(saveIdx, 0); + }) + ); + }); - restWorker.loadState = (first, cb) => { - loadIdx = slCounter; - slCounter += 1; - cb(null, restState); - }; - persistentStorage.storage._cache = restState; - persistentStorage.save(); // load #1 - persistentStorage.load(); // save #1 - persistentStorage.save(); // load #1 ! - return persistentStorage.load() // save #1 - .then(() => { - assert.strictEqual(loadIdx, 1); - assert.strictEqual(saveIdx, 0); - return Promise.resolve(); + it('should queue \'save\' operation if current \'save\' in progress', () => { + let counter = 0; + + sinon.stub(restWorker, 'saveState').callsFake((first, currentState, cb) => { + if (counter === 0) { + persistentStorageInst.save(); + } + counter += 1; + cb(null); }); + + return assert.isFulfilled( + persistentStorageInst.load() // save #1 + .then(() => persistentStorageInst.save()) + .then(() => { + assert.strictEqual(counter, 2); + }) + ); + }); }); }); diff --git a/test/unit/propertiesJsonTests.js b/test/unit/propertiesJsonTests.js new file mode 100644 index 00000000..3a0e1cf2 --- /dev/null +++ b/test/unit/propertiesJsonTests.js @@ -0,0 +1,131 @@ +/* + * Copyright 2018. F5 Networks, Inc. See End User License Agreement ('EULA') for + * license terms. Notwithstanding anything to the contrary in the EULA, Licensee + * may copy and modify this software product for its internal business purposes. + * Further, Licensee may upload, publish and distribute the modified version of + * the software product on devcentral.f5.com. + */ + +'use strict'; + +/* eslint-disable import/order */ + +require('./shared/restoreCache')(); + +const chai = require('chai'); +const chaiAsPromised = require('chai-as-promised'); +const nock = require('nock'); + +const defaultPaths = require('../../src/lib/paths.json'); +const defaultProperties = require('../../src/lib/properties.json'); +const propertiesTestsData = require('./propertiesJsonTestsData'); +const SystemStats = require('../../src/lib/systemStats'); +const testUtil = require('./shared/util'); + +chai.use(chaiAsPromised); +const assert = chai.assert; + +describe('properties.json', () => { + // global vars - to avoid problems with 'before(Each)' + let paths = testUtil.deepCopy(defaultPaths); + let allProperties = testUtil.deepCopy(defaultProperties); + + const checkResponse = (endpointMock, response) => { + if (!response.kind) { + throw new Error(`Endpoint '${endpointMock.endpoint}' has no property 'kind' in response`); + } + }; + + const copyProperties = (properties, keys) => { + const ret = {}; + keys.forEach((key) => { + if (typeof properties[key] === 'undefined') { + throw new Error(`copyProperties: unknown key "${key}"`); + } + ret[key] = properties[key]; + }); + return ret; + }; + + const generateProperties = (source, data) => { + let ret; + if (Array.isArray(data)) { + ret = copyProperties(source, data); + } else if (typeof data === 'function') { + ret = data(source); + } + return ret; + }; + + Object.keys(propertiesTestsData).forEach((testSetKey) => { + const testSet = propertiesTestsData[testSetKey]; + + testUtil.getCallableDescribe(testSet)(testSet.name, () => { + beforeEach(() => { + // copy before each test to avoid modifications + paths = testUtil.deepCopy(defaultPaths); + allProperties = testUtil.deepCopy(defaultProperties); + }); + + afterEach(() => { + nock.cleanAll(); + }); + + testSet.tests.forEach((testConf) => { + testUtil.getCallableIt(testConf)(testConf.name, () => { + const contextToCollect = generateProperties(allProperties.context, testConf.contextToCollect) + || allProperties.context; + const statsToCollect = generateProperties(allProperties.stats, testConf.statsToCollect) + || allProperties.stats; + + const options = { + paths, + properties: { + stats: statsToCollect, + context: contextToCollect, + global: allProperties.global, + definitions: allProperties.definitions + }, + dataOpts: { + tags: { + tenant: '`T`', + application: '`A`' + } + } + }; + + const getCollectedData = testConf.getCollectedData + ? testConf.getCollectedData : promise => promise; + + const stats = new SystemStats(options); + + return Promise.resolve() + .then(() => { + testUtil.mockEndpoints(testConf.endpoints || [], { responseChecker: checkResponse }); + return assert.becomes( + getCollectedData(stats.collect(), stats), + testConf.expectedData, + 'should match expected output on first attempt to collect data' + ); + }) + .then(() => { + assert.deepStrictEqual(stats.loader.cachedResponse, {}, 'cache should be erased'); + }) + .then(() => { + // if after second attempt output will be the same + // then properties, paths and etc. works correctly + testUtil.mockEndpoints(testConf.endpoints || [], { responseChecker: checkResponse }); + return assert.becomes( + getCollectedData(stats.collect(), stats), + testConf.expectedData, + 'should match expected output on second attempt to collect data' + ); + }) + .then(() => { + assert.deepStrictEqual(stats.loader.cachedResponse, {}, 'cache should be erased'); + }); + }); + }); + }); + }); +}); diff --git a/test/unit/propertiesJsonTestsData.js b/test/unit/propertiesJsonTestsData.js new file mode 100644 index 00000000..a465fc36 --- /dev/null +++ b/test/unit/propertiesJsonTestsData.js @@ -0,0 +1,7193 @@ +/* + * Copyright 2018. F5 Networks, Inc. See End User License Agreement ('EULA') for + * license terms. Notwithstanding anything to the contrary in the EULA, Licensee + * may copy and modify this software product for its internal business purposes. + * Further, Licensee may upload, publish and distribute the modified version of + * the software product on devcentral.f5.com. + */ + +'use strict'; + +const defaultProperties = require('../../src/lib/properties.json'); + +const TMCTL_CMD_REGEXP = /'tmctl\s+-c\s+(.*)'/; + +/* eslint-disable no-useless-escape */ + +/** + * NOTE: DO NOT REMOVE 'kind' AND 'selfLink' PROPERTIES FROM RESPONSE's TOP LEVEL + */ +/** + * TODO: update/remove 'options: { times: XXXX }' when EndpointLoader's cache will be fixed + */ + +module.exports = { + /** + * Set of data to check actual and expected results only. + * If you need some additional check feel free to add additional + * property or write separate test. + * + * Note: you can specify 'testOpts' property on the same level as 'name'. + * Following options available: + * - only (bool) - run this test only (it.only) + * */ + /** + * TEST SET DATA STARTS HERE + * */ + collectContextData: { + name: 'context data', + tests: [ + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should collect context data', + statsToCollect: () => ({}), + contextToCollect: context => context, + getCollectedData: (promise, stats) => promise.then(() => stats.contextData), + endpoints: [ + { + endpoint: '/mgmt/tm/sys/provision', + response: { + kind: 'tm:sys:provision:provisioncollectionstate', + selfLink: 'https://localhost/mgmt/tm/sys/provision?ver=14.1.0', + items: [ + { + name: 'afm', + level: 'none', + // just to be sure that filterKeys works + cpuRatio: 0 + }, + { + name: 'ltm', + level: 'none', + // just to be sure that filterKeys works + cpuRatio: 0 + }, + { + name: 'asm', + level: 'nominal', + // just to be sure that filterKeys works + cpuRatio: 0 + } + ] + } + }, + { + endpoint: '/mgmt/shared/identified-devices/config/device-info', + options: { + times: 999 + }, + response: { + kind: 'shared:resolver:device-groups:deviceinfostate', + selfLink: 'https://localhost/mgmt/shared/identified-devices/config/device-info', + baseMac: '00:01:2:a:B:d0', + hostname: 'bigip1', + version: '12.1.5.1', + machineId: '00000000-0000-0000-0000-000000000000' + } + } + ], + expectedData: { + HOSTNAME: 'bigip1', + BASE_MAC_ADDR: '00:01:02:0A:0B:D0', + deviceVersion: '12.1.5.1', + provisioning: { + afm: { + name: 'afm', + level: 'none' + }, + ltm: { + name: 'ltm', + level: 'none' + }, + asm: { + name: 'asm', + level: 'nominal' + } + } + } + }, + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should not fail when no data (with items property)', + statsToCollect: () => ({}), + contextToCollect: context => context, + getCollectedData: (promise, stats) => promise.then(() => stats.contextData), + endpoints: [ + { + endpoint: '/mgmt/tm/sys/provision', + response: { + kind: 'tm:sys:provision:provisioncollectionstate', + selfLink: 'https://localhost/mgmt/tm/sys/provision?ver=14.1.0', + items: [] + } + }, + { + endpoint: '/mgmt/shared/identified-devices/config/device-info', + options: { + times: 999 + }, + response: { + kind: 'shared:resolver:device-groups:deviceinfostate', + selfLink: 'https://localhost/mgmt/shared/identified-devices/config/device-info' + } + } + ], + expectedData: { + HOSTNAME: 'missing data', + BASE_MAC_ADDR: 'missing data', + deviceVersion: 'missing data', + provisioning: {} + } + }, + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should not fail when no data (without items property)', + statsToCollect: () => ({}), + contextToCollect: context => context, + getCollectedData: (promise, stats) => promise.then(() => stats.contextData), + endpoints: [ + { + endpoint: '/mgmt/tm/sys/provision', + response: { + kind: 'tm:sys:provision:provisioncollectionstate', + selfLink: 'https://localhost/mgmt/tm/sys/provision?ver=14.1.0' + } + }, + { + endpoint: '/mgmt/shared/identified-devices/config/device-info', + options: { + times: 999 + }, + response: { + kind: 'shared:resolver:device-groups:deviceinfostate', + selfLink: 'https://localhost/mgmt/shared/identified-devices/config/device-info' + } + } + ], + expectedData: { + HOSTNAME: 'missing data', + BASE_MAC_ADDR: 'missing data', + deviceVersion: 'missing data', + provisioning: {} + } + } + ] + }, + /** + * TEST SET DATA STARTS HERE + * */ + collectSystemStats: { + name: 'system stats', + tests: [ + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should create empty system folder', + statsToCollect: ['system'], + contextToCollect: [], + expectedData: { + system: {} + } + }, + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should set properties to undefined when conditional block results to false', + statsToCollect: [ + 'system', + 'configReady', + 'licenseReady', + 'provisionReady', + 'asmState', + 'lastAsmChange', + 'apmState', + 'afmState', + 'lastAfmDeploy', + 'ltmConfigTime', + 'gtmConfigTime' + ], + contextToCollect: ['deviceVersion', 'provisioning'], + expectedData: { + system: { + configReady: undefined, + licenseReady: undefined, + provisionReady: undefined, + asmState: undefined, + lastAsmChange: undefined, + apmState: undefined, + afmState: undefined, + lastAfmDeploy: undefined, + ltmConfigTime: undefined, + gtmConfigTime: undefined + } + }, + endpoints: [ + { + endpoint: '/mgmt/shared/identified-devices/config/device-info', + options: { + times: 999 + }, + response: { + kind: 'shared:resolver:device-groups:deviceinfostate', + selfLink: 'https://localhost/mgmt/shared/identified-devices/config/device-info', + version: '12.0.0', + machineId: '00000000-0000-0000-0000-000000000000', + hostname: 'test.local' + } + }, + { + endpoint: '/mgmt/tm/sys/provision', + options: { + times: 999 + }, + response: { + kind: 'tm:sys:provision:provisioncollectionstate', + selfLink: 'https://localhost/mgmt/tm/sys/provision?ver=14.1.0', + items: [ + { + name: 'afm', + level: 'none', + // just to be sure that filterKeys works + cpuRatio: 0 + }, + { + name: 'apm', + level: 'none', + // just to be sure that filterKeys works + cpuRatio: 0 + }, + { + name: 'asm', + level: 'none', + // just to be sure that filterKeys works + cpuRatio: 0 + }, + { + name: 'gtm', + level: 'none', + // just to be sure that filterKeys works + cpuRatio: 0 + }, + { + name: 'ltm', + level: 'none', + // just to be sure that filterKeys works + cpuRatio: 0 + } + ] + } + } + ] + }, + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should collect system stats', + statsToCollect: (stats) => { + const ret = { + system: stats.system + }; + Object.keys(stats).forEach((statKey) => { + const stat = stats[statKey]; + if (stat.structure && stat.structure.parentKey === 'system') { + ret[statKey] = stat; + } + }); + return ret; + }, + contextToCollect: context => context, + expectedData: { + system: { + hostname: 'test.local', + machineId: '00000000-0000-0000-0000-000000000000', + version: '14.1.2.3', + versionBuild: '0.0.5', + location: 'Location', + description: 'Description', + marketingName: 'Marketing Name', + platformId: 'Platform ID', + chassisId: '00000000-0000-0000-0000-000000000001', + baseMac: '00:01:0A:0B:0C:0D', + callBackUrl: 'https://10.0.0.107', + configReady: 'yes', + licenseReady: 'yes', + provisionReady: 'yes', + syncColor: 'green', + syncMode: 'standalone', + syncStatus: 'Standalone', + syncSummary: 'Description', + failoverStatus: 'ACTIVE', + failoverColor: 'green', + systemTimestamp: '2020-01-26T08:42:48.000Z', + afmState: 'quiescent', + apmState: 'Pending Policy Changes', + asmState: 'Policies Consistent', + gtmConfigTime: '2020-01-24T03:47:29.000Z', + lastAfmDeploy: '2020-01-23T08:00:56.000Z', + lastAsmChange: '2020-01-24T03:47:29.000Z', + ltmConfigTime: '2020-01-24T03:47:29.000Z', + cpu: 7, + memory: 16, + tmmCpu: 0, + tmmMemory: 3, + tmmTraffic: { + 'clientSideTraffic.bitsIn': 197416, + 'clientSideTraffic.bitsOut': 470632, + 'serverSideTraffic.bitsIn': 179496, + 'serverSideTraffic.bitsOut': 469992 + }, + diskLatency: { + nvme0n1: { + name: 'nvme0n1', + '%util': '0.19', + 'r/s': '2.77', + 'w/s': '4.80' + } + }, + diskStorage: { + '/': { + '1024-blocks': '428150', + Capacity: '35%', + name: '/' + } + }, + networkInterfaces: { + '1.0': { + 'counters.bitsIn': 2, + 'counters.bitsOut': 0, + name: '1.0', + status: 'up' + }, + 1.1: { + 'counters.bitsIn': 1, + 'counters.bitsOut': 0, + name: '1.1', + status: 'up' + } + }, + provisioning: { + afm: { + level: 'nominal', + name: 'afm' + }, + apm: { + level: 'nominal', + name: 'apm' + }, + asm: { + level: 'nominal', + name: 'asm' + }, + gtm: { + level: 'nominal', + name: 'gtm' + }, + ltm: { + level: 'nominal', + name: 'ltm' + } + } + } + }, + endpoints: [ + { + endpoint: '/mgmt/tm/sys/provision', + options: { + times: 999 + }, + response: { + kind: 'tm:sys:provision:provisioncollectionstate', + selfLink: 'https://localhost/mgmt/tm/sys/provision?ver=14.1.0', + items: [ + { + name: 'afm', + level: 'nominal', + // just to be sure that filterKeys works + cpuRatio: 0 + }, + { + name: 'apm', + level: 'nominal', + // just to be sure that filterKeys works + cpuRatio: 0 + }, + { + name: 'asm', + level: 'nominal', + // just to be sure that filterKeys works + cpuRatio: 0 + }, + { + name: 'gtm', + level: 'nominal', + // just to be sure that filterKeys works + cpuRatio: 0 + }, + { + name: 'ltm', + level: 'nominal', + // just to be sure that filterKeys works + cpuRatio: 0 + } + ] + } + }, + { + endpoint: '/mgmt/shared/identified-devices/config/device-info', + options: { + times: 999 + }, + response: { + kind: 'shared:resolver:device-groups:deviceinfostate', + selfLink: 'https://localhost/mgmt/shared/identified-devices/config/device-info', + machineId: '00000000-0000-0000-0000-000000000000', + hostname: 'test.local', + version: '14.1.2.3', + build: '0.0.5', + chassisSerialNumber: '00000000-0000-0000-0000-000000000001', + platformMarketingName: 'Marketing Name', + platform: 'Platform ID', + baseMac: '00:1:a:0B:c:D', + cpu: 'cpu info' + } + }, + { + endpoint: '/mgmt/tm/cm/device', + options: { + times: 999 + }, + response: { + kind: 'tm:cm:device:devicecollectionstate', + selfLink: 'https://localhost/mgmt/tm/cm/device?ver=14.1.0', + items: [ + { + name: 'test.local', + baseMac: '00:1:a:0B:c:D', + description: 'Description', + location: 'Location', + chassisType: 'individual', + configsyncIp: '10.0.2.7', + edition: 'Final', + failoverState: 'active', + haCapacity: 0 + } + ] + } + }, + { + endpoint: '/mgmt/tm/sys/management-ip', + response: { + kind: 'tm:sys:management-ip:management-ipcollectionstate', + selfLink: 'https://localhost/mgmt/tm/sys/management-ip?ver=14.1.0', + items: [ + { + name: '10.0.0.107/24' + } + ] + } + }, + { + endpoint: '/mgmt/tm/sys/ready', + options: { + times: 999 + }, + response: { + kind: 'tm:sys:ready:readystats', + selfLink: 'https://localhost/mgmt/tm/sys/ready?ver=14.1.0', + entries: { + 'https://localhost/mgmt/tm/sys/ready/0': { + nestedStats: { + entries: { + configReady: { + description: 'yes' + }, + licenseReady: { + description: 'yes' + }, + provisionReady: { + description: 'yes' + } + } + } + } + } + } + }, + { + endpoint: '/mgmt/tm/net/interface/stats', + response: { + kind: 'tm:net:interface:interfacecollectionstats', + selfLink: 'https://localhost/mgmt/tm/net/interface/stats?ver=14.1.0', + entries: { + 'https://localhost/mgmt/tm/net/interface/1.0/stats': { + nestedStats: { + entries: { + 'counters.bitsIn': { + value: 2 + }, + 'counters.bitsOut': { + value: 0 + }, + // just to be sure that filterKeys works + 'counters.dropsAll': { + value: 5504272 + }, + status: { + description: 'up' + } + } + } + }, + 'https://localhost/mgmt/tm/net/interface/1.1/stats': { + nestedStats: { + entries: { + 'counters.bitsIn': { + value: 1 + }, + 'counters.bitsOut': { + value: 0 + }, + // just to be sure that filterKeys works + 'counters.dropsAll': { + value: 5504272 + }, + status: { + description: 'up' + } + } + } + } + } + } + }, + { + endpoint: '/mgmt/tm/cm/sync-status', + options: { + times: 999 + }, + response: { + kind: 'tm:cm:sync-status:sync-statusstats', + selfLink: 'https://localhost/mgmt/tm/cm/sync-status?ver=14.1.0', + entries: { + 'https://localhost/mgmt/tm/cm/sync-status/0': { + nestedStats: { + entries: { + color: { + description: 'green' + }, + mode: { + description: 'standalone' + }, + status: { + description: 'Standalone' + }, + summary: { + description: 'Description' + } + } + } + } + } + } + }, + { + endpoint: '/mgmt/tm/cm/failover-status', + options: { + times: 999 + }, + response: { + kind: 'tm:cm:failover-status:failover-statusstats', + selfLink: 'https://localhost/mgmt/tm/cm/failover-status?ver=14.1.0', + entries: { + 'https://localhost/mgmt/tm/cm/failover-status/0': { + nestedStats: { + entries: { + color: { + description: 'green' + }, + status: { + description: 'ACTIVE' + } + } + } + } + } + } + }, + { + endpoint: '/mgmt/tm/sys/clock', + response: { + kind: 'tm:sys:clock:clockstats', + selfLink: 'https://localhost/mgmt/tm/sys/clock?ver=14.1.0', + entries: { + 'https://localhost/mgmt/tm/sys/clock/0': { + nestedStats: { + entries: { + fullDate: { + description: '2020-01-26T08:42:48Z' + } + } + } + } + } + } + }, + { + endpoint: '/mgmt/tm/sys/host-info', + response: { + kind: 'tm:sys:host-info:host-infostats', + selfLink: 'https://localhost/mgmt/tm/sys/host-info?ver=14.1.0', + entries: { + 'https://localhost/mgmt/tm/sys/host-info/0': { + nestedStats: { + entries: { + 'https://localhost/mgmt/tm/sys/hostInfo/0/cpuInfo': { + nestedStats: { + entries: { + 'https://localhost/mgmt/tm/sys/hostInfo/0/cpuInfo/0': { + nestedStats: { + entries: { + oneMinAvgSystem: { + value: 6 + } + } + } + }, + 'https://localhost/mgmt/tm/sys/hostInfo/0/cpuInfo/1': { + nestedStats: { + entries: { + oneMinAvgSystem: { + value: 8 + } + } + } + }, + 'https://localhost/mgmt/tm/sys/hostInfo/0/cpuInfo/2': { + nestedStats: { + entries: { + oneMinAvgSystem: { + value: 12 + } + } + } + }, + 'https://localhost/mgmt/tm/sys/hostInfo/0/cpuInfo/3': { + nestedStats: { + entries: { + oneMinAvgSystem: { + value: 2 + } + } + } + } + } + } + } + } + } + } + } + } + }, + { + endpoint: '/mgmt/tm/sys/memory', + options: { + times: 2 + }, + response: { + kind: 'tm:sys:memory:memorystats', + selfLink: 'https://localhost/mgmt/tm/sys/memory?ver=14.1.0', + entries: { + 'https://localhost/mgmt/tm/sys/memory/memory-host': { + nestedStats: { + entries: { + 'https://localhost/mgmt/tm/sys/memory/memory-host/0': { + nestedStats: { + entries: { + memoryTotal: { + value: 8062742528 + }, + memoryUsed: { + value: 1314352272 + }, + tmmMemoryTotal: { + value: 6320816128 + }, + tmmMemoryUsed: { + value: 169258128 + } + } + } + } + } + } + } + } + } + }, + { + endpoint: '/mgmt/tm/sys/tmm-info', + response: { + kind: 'tm:sys:tmm-info:tmm-infostats', + selfLink: 'https://localhost/mgmt/tm/sys/tmm-info?ver=14.1.0', + entries: { + 'https://localhost/mgmt/tm/sys/tmm-info/0.0': { + nestedStats: { + entries: { + oneMinAvgUsageRatio: { + value: 0 + } + } + } + }, + 'https://localhost/mgmt/tm/sys/tmm-info/0.1': { + nestedStats: { + entries: { + oneMinAvgUsageRatio: { + value: 0 + } + } + } + } + } + } + }, + { + endpoint: '/mgmt/tm/sys/tmm-traffic', + response: { + kind: 'tm:sys:tmm-traffic:tmm-trafficstats', + selfLink: 'https://localhost/mgmt/tm/sys/tmm-traffic?ver=14.1.0', + entries: { + 'https://localhost/mgmt/tm/sys/tmm-traffic/0.0': { + nestedStats: { + entries: { + 'clientSideTraffic.bitsIn': { + value: 17768 + }, + 'clientSideTraffic.bitsOut': { + value: 9960 + }, + // just to be sure that filterKeys works + 'clientSideTraffic.curConns': { + value: 0 + }, + 'serverSideTraffic.bitsIn': { + value: 3632 + }, + 'serverSideTraffic.bitsOut': { + value: 9640 + } + } + } + }, + 'https://localhost/mgmt/tm/sys/tmm-traffic/0.1': { + nestedStats: { + entries: { + 'clientSideTraffic.bitsIn': { + value: 179648 + }, + 'clientSideTraffic.bitsOut': { + value: 460672 + }, + // just to be sure that filterKeys works + 'clientSideTraffic.curConns': { + value: 0 + }, + 'serverSideTraffic.bitsIn': { + value: 175864 + }, + 'serverSideTraffic.bitsOut': { + value: 460352 + } + } + } + } + } + } + }, + { + endpoint: '/mgmt/tm/util/bash', + method: 'post', + request: { + command: 'run', + utilCmdArgs: '-c "/bin/df -P | /usr/bin/tr -s \' \' \',\'"' + }, + response: { + kind: 'tm:util:bash:runstate', + commandResult: 'Filesystem,1024-blocks,Used,Available,Capacity,Mounted,on\n/dev/mapper/vg--db--vda-set.1.root,428150,140352,261174,35%,/\n' + } + }, + { + endpoint: '/mgmt/tm/util/bash', + method: 'post', + request: { + command: 'run', + utilCmdArgs: '-c "/usr/bin/iostat -x -d | /usr/bin/tail -n +3 | /usr/bin/tr -s \' \' \',\'"' + }, + response: { + kind: 'tm:util:bash:runstate', + commandResult: 'Device:,rrqm/s,wrqm/s,r/s,w/s,rkB/s,wkB/s,avgrq-sz,avgqu-sz,await,r_await,w_await,svctm,%util\nnvme0n1,0.16,3.60,2.77,4.80,95.04,54.34,39.44,0.01,1.77,2.39,1.41,0.25,0.19\n\n' + } + }, + { + endpoint: '/mgmt/tm/util/bash', + options: { + times: 2 + }, + method: 'post', + request: body => body.utilCmdArgs.indexOf('Policies Consistent') !== -1, + response: { + kind: 'tm:util:bash:runstate', + commandResult: 'asm_state,last_asm_change\nPolicies Consistent,1579837649\n' + } + }, + { + endpoint: '/mgmt/tm/util/bash', + options: { + times: 2 + }, + method: 'post', + request: body => body.utilCmdArgs.indexOf('profile_access_misc_stat') !== -1, + response: { + kind: 'tm:util:bash:runstate', + commandResult: 'apm_state\nPending Policy Changes' + } + }, + { + endpoint: '/mgmt/tm/security/firewall/current-state/stats', + options: { + times: 2 + }, + response: { + kind: 'tm:security:firewall:current-state:current-statestats', + selfLink: 'https://localhost/mgmt/tm/security/firewall/current-state/stats?ver=14.1.0', + entries: { + 'https://localhost/mgmt/tm/security/firewall/current-state/0/stats': { + nestedStats: { + entries: { + pccdStatus: { + description: 'quiescent' + }, + ruleDeployEndTimeFmt: { + description: 'Jan 23 2020 00:00:56-0800' + } + } + } + } + } + } + }, + { + endpoint: '/mgmt/tm/sys/db/ltm.configtime', + response: { + kind: 'tm:sys:db:dbstate', + selfLink: 'https://localhost/mgmt/tm/sys/db/ltm.configtime?ver=14.1.0', + value: '1579837649' + } + }, + { + endpoint: '/mgmt/tm/sys/db/gtm.configtime', + response: { + kind: 'tm:sys:db:dbstate', + selfLink: 'https://localhost/mgmt/tm/sys/db/gtm.configtime?ver=14.1.0', + value: '1579837649' + } + } + ] + }, + /** + * TEST DATA STARTS HERE + */ + { + name: 'should collect cpuInfo on a multi CPU device', + statsToCollect: ['system', 'cpu'], + contextToCollect: context => context, + expectedData: { + system: { + cpu: 20 + } + }, + endpoints: [ + { + endpoint: '/mgmt/tm/sys/host-info', + response: { + kind: 'tm:sys:host-info:host-infostats', + selfLink: 'https://localhost/mgmt/tm/sys/host-info?ver=14.1.0', + entries: { + 'https://localhost/mgmt/tm/sys/host-info/1': { + nestedStats: { + entries: { + 'https://localhost/mgmt/tm/sys/hostInfo/1/cpuInfo': { + nestedStats: { + entries: { + 'https://localhost/mgmt/tm/sys/hostInfo/1/cpuInfo/0': { + nestedStats: { + entries: { + oneMinAvgSystem: { + value: 2 + } + } + } + }, + 'https://localhost/mgmt/tm/sys/hostInfo/1/cpuInfo/1': { + nestedStats: { + entries: { + oneMinAvgSystem: { + value: 4 + } + } + } + }, + 'https://localhost/mgmt/tm/sys/hostInfo/1/cpuInfo/2': { + nestedStats: { + entries: { + oneMinAvgSystem: { + value: 6 + } + } + } + }, + 'https://localhost/mgmt/tm/sys/hostInfo/1/cpuInfo/3': { + nestedStats: { + entries: { + oneMinAvgSystem: { + value: 8 + } + } + } + } + } + } + } + } + } + }, + 'https://localhost/mgmt/tm/sys/host-info/2': { + nestedStats: { + entries: { + 'https://localhost/mgmt/tm/sys/hostInfo/2/cpuInfo': { + nestedStats: { + entries: { + 'https://localhost/mgmt/tm/sys/hostInfo/2/cpuInfo/0': { + nestedStats: { + entries: { + oneMinAvgSystem: { + value: 12 + } + } + } + }, + 'https://localhost/mgmt/tm/sys/hostInfo/2/cpuInfo/1': { + nestedStats: { + entries: { + oneMinAvgSystem: { + value: 14 + } + } + } + }, + 'https://localhost/mgmt/tm/sys/hostInfo/2/cpuInfo/2': { + nestedStats: { + entries: { + oneMinAvgSystem: { + value: 16 + } + } + } + }, + 'https://localhost/mgmt/tm/sys/hostInfo/2/cpuInfo/3': { + nestedStats: { + entries: { + oneMinAvgSystem: { + value: 18 + } + } + } + } + } + } + } + } + } + }, + 'https://localhost/mgmt/tm/sys/host-info/3': { + nestedStats: { + entries: { + 'https://localhost/mgmt/tm/sys/hostInfo/3/cpuInfo': { + nestedStats: { + entries: { + 'https://localhost/mgmt/tm/sys/hostInfo/3/cpuInfo/0': { + nestedStats: { + entries: { + oneMinAvgSystem: { + value: 22 + } + } + } + }, + 'https://localhost/mgmt/tm/sys/hostInfo/3/cpuInfo/1': { + nestedStats: { + entries: { + oneMinAvgSystem: { + value: 24 + } + } + } + }, + 'https://localhost/mgmt/tm/sys/hostInfo/3/cpuInfo/2': { + nestedStats: { + entries: { + oneMinAvgSystem: { + value: 26 + } + } + } + }, + 'https://localhost/mgmt/tm/sys/hostInfo/3/cpuInfo/3': { + nestedStats: { + entries: { + oneMinAvgSystem: { + value: 28 + } + } + } + } + } + } + } + } + } + }, + 'https://localhost/mgmt/tm/sys/host-info/4': { + nestedStats: { + entries: { + 'https://localhost/mgmt/tm/sys/hostInfo/4/cpuInfo': { + nestedStats: { + entries: { + 'https://localhost/mgmt/tm/sys/hostInfo/4/cpuInfo/0': { + nestedStats: { + entries: { + oneMinAvgSystem: { + value: 32 + } + } + } + }, + 'https://localhost/mgmt/tm/sys/hostInfo/4/cpuInfo/1': { + nestedStats: { + entries: { + oneMinAvgSystem: { + value: 34 + } + } + } + }, + 'https://localhost/mgmt/tm/sys/hostInfo/4/cpuInfo/2': { + nestedStats: { + entries: { + oneMinAvgSystem: { + value: 36 + } + } + } + }, + 'https://localhost/mgmt/tm/sys/hostInfo/4/cpuInfo/3': { + nestedStats: { + entries: { + oneMinAvgSystem: { + value: 38 + } + } + } + } + } + } + } + } + } + } + } + } + }, + { + endpoint: '/mgmt/shared/identified-devices/config/device-info', + options: { + times: 999 + }, + response: { + kind: 'shared:resolver:device-groups:deviceinfostate', + selfLink: 'https://localhost/mgmt/shared/identified-devices/config/device-info', + baseMac: '00:01:2:a:B:d0', + hostname: 'bigip1', + version: '12.1.5.1', + machineId: '00000000-0000-0000-0000-000000000000' + } + }, + { + endpoint: '/mgmt/tm/sys/provision', + options: { + times: 999 + }, + response: { + kind: 'tm:sys:provision:provisioncollectionstate', + selfLink: 'https://localhost/mgmt/tm/sys/provision?ver=14.1.0' + } + } + ] + }, + /** + * TEST DATA STARTS HERE + */ + { + name: 'should collect memory-host on a multi host device', + statsToCollect: ['system', 'memory', 'tmmMemory'], + contextToCollect: context => context, + expectedData: { + system: { + memory: 70, + tmmMemory: 7 + } + }, + endpoints: [ + { + endpoint: '/mgmt/tm/sys/memory', + options: { + times: 2 + }, + response: { + kind: 'tm:sys:memory:memorystats', + selfLink: 'https://localhost/mgmt/tm/sys/memory?ver=14.1.0', + entries: { + 'https://localhost/mgmt/tm/sys/memory/memory-host': { + nestedStats: { + entries: { + 'https://localhost/mgmt/tm/sys/memory/memory-host/0': { + nestedStats: { + entries: { + memoryTotal: { + value: 8062742528 + }, + memoryUsed: { + value: 1314352272 + }, + tmmMemoryTotal: { + value: 6320816128 + }, + tmmMemoryUsed: { + value: 169258128 + } + } + } + }, + 'https://localhost/mgmt/tm/sys/memory/memory-host/1': { + nestedStats: { + entries: { + memoryTotal: { + value: 16759459840 + }, + memoryUsed: { + value: 16091751448 + }, + tmmMemoryTotal: { + value: 423624704 + }, + tmmMemoryUsed: { + value: 283849752 + } + } + } + } + } + } + } + } + } + }, + { + endpoint: '/mgmt/shared/identified-devices/config/device-info', + options: { + times: 999 + }, + response: { + kind: 'shared:resolver:device-groups:deviceinfostate', + selfLink: 'https://localhost/mgmt/shared/identified-devices/config/device-info', + baseMac: '00:01:2:a:B:d0', + hostname: 'bigip1', + version: '12.1.5.1', + machineId: '00000000-0000-0000-0000-000000000000' + } + }, + { + endpoint: '/mgmt/tm/sys/provision', + response: { + kind: 'tm:sys:provision:provisioncollectionstate', + selfLink: 'https://localhost/mgmt/tm/sys/provision?ver=14.1.0' + } + } + ] + }, + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should not fail when no data (with items property)', + statsToCollect: (stats) => { + const ret = { + system: stats.system + }; + Object.keys(stats).forEach((statKey) => { + const stat = stats[statKey]; + if (stat.structure && stat.structure.parentKey === 'system') { + ret[statKey] = stat; + } + }); + return ret; + }, + contextToCollect: context => context, + expectedData: { + system: { + hostname: 'missing data', + machineId: 'missing data', + version: 'missing data', + versionBuild: 'missing data', + location: 'missing data', + description: 'missing data', + marketingName: 'missing data', + platformId: 'missing data', + chassisId: 'missing data', + baseMac: 'missing data', + callBackUrl: 'null', + configReady: undefined, // device version missed + licenseReady: undefined, // device version missed + provisionReady: undefined, // device version missed + syncColor: 'missing data', + syncMode: 'missing data', + syncStatus: 'missing data', + syncSummary: 'missing data', + failoverStatus: 'missing data', + failoverColor: 'missing data', + systemTimestamp: 'missing data', + afmState: undefined, // not provisioned + apmState: undefined, // not provisioned + asmState: undefined, // not provisioned + gtmConfigTime: undefined, // not provisioned + lastAfmDeploy: undefined, // not provisioned + lastAsmChange: undefined, // not provisioned + ltmConfigTime: undefined, // not provisioned + cpu: 'missing data', + memory: NaN, // should be fixed and set to missing data + tmmCpu: NaN, // should be fixed and set to missing data + tmmMemory: NaN, // should be fixed and set to missing data + tmmTraffic: {}, + diskLatency: {}, + diskStorage: {}, + networkInterfaces: {}, + provisioning: {} + } + }, + endpoints: [ + { + endpoint: '/mgmt/tm/sys/provision', + options: { + times: 999 + }, + response: { + kind: 'tm:sys:provision:provisioncollectionstate', + selfLink: 'https://localhost/mgmt/tm/sys/provision?ver=14.1.0', + items: [] + } + }, + { + endpoint: '/mgmt/shared/identified-devices/config/device-info', + options: { + times: 999 + }, + response: { + kind: 'shared:resolver:device-groups:deviceinfostate', + selfLink: 'https://localhost/mgmt/shared/identified-devices/config/device-info' + } + }, + { + endpoint: '/mgmt/tm/cm/device', + options: { + times: 999 + }, + response: { + kind: 'tm:cm:device:devicecollectionstate', + selfLink: 'https://localhost/mgmt/tm/cm/device?ver=14.1.0', + items: [] + } + }, + { + endpoint: '/mgmt/tm/sys/management-ip', + response: { + kind: 'tm:sys:management-ip:management-ipcollectionstate', + selfLink: 'https://localhost/mgmt/tm/sys/management-ip?ver=14.1.0', + items: [] + } + }, + { + endpoint: '/mgmt/tm/sys/ready', + options: { + times: 999 + }, + response: { + kind: 'tm:sys:ready:readystats', + selfLink: 'https://localhost/mgmt/tm/sys/ready?ver=14.1.0' + } + }, + { + endpoint: '/mgmt/tm/net/interface/stats', + response: { + kind: 'tm:net:interface:interfacecollectionstats', + selfLink: 'https://localhost/mgmt/tm/net/interface/stats?ver=14.1.0' + } + }, + { + endpoint: '/mgmt/tm/cm/sync-status', + options: { + times: 999 + }, + response: { + kind: 'tm:cm:sync-status:sync-statusstats', + selfLink: 'https://localhost/mgmt/tm/cm/sync-status?ver=14.1.0' + } + }, + { + endpoint: '/mgmt/tm/cm/failover-status', + options: { + times: 999 + }, + response: { + kind: 'tm:cm:failover-status:failover-statusstats', + selfLink: 'https://localhost/mgmt/tm/cm/failover-status?ver=14.1.0' + } + }, + { + endpoint: '/mgmt/tm/sys/clock', + response: { + kind: 'tm:sys:clock:clockstats', + selfLink: 'https://localhost/mgmt/tm/sys/clock?ver=14.1.0' + } + }, + { + endpoint: '/mgmt/tm/sys/host-info', + response: { + kind: 'tm:sys:host-info:host-infostats', + selfLink: 'https://localhost/mgmt/tm/sys/host-info?ver=14.1.0' + } + }, + { + endpoint: '/mgmt/tm/sys/memory', + options: { + times: 2 + }, + response: { + kind: 'tm:sys:memory:memorystats', + selfLink: 'https://localhost/mgmt/tm/sys/memory?ver=14.1.0' + } + }, + { + endpoint: '/mgmt/tm/sys/tmm-info', + response: { + kind: 'tm:sys:tmm-info:tmm-infostats', + selfLink: 'https://localhost/mgmt/tm/sys/tmm-info?ver=14.1.0' + } + }, + { + endpoint: '/mgmt/tm/sys/tmm-traffic', + response: { + kind: 'tm:sys:tmm-traffic:tmm-trafficstats', + selfLink: 'https://localhost/mgmt/tm/sys/tmm-traffic?ver=14.1.0' + } + }, + { + endpoint: '/mgmt/tm/util/bash', + method: 'post', + request: { + command: 'run', + utilCmdArgs: '-c "/bin/df -P | /usr/bin/tr -s \' \' \',\'"' + }, + response: { + kind: 'tm:util:bash:runstate' + } + }, + { + endpoint: '/mgmt/tm/util/bash', + method: 'post', + request: { + command: 'run', + utilCmdArgs: '-c "/usr/bin/iostat -x -d | /usr/bin/tail -n +3 | /usr/bin/tr -s \' \' \',\'"' + }, + response: { + kind: 'tm:util:bash:runstate' + } + }, + { + endpoint: '/mgmt/tm/util/bash', + options: { + times: 2 + }, + method: 'post', + request: body => body.utilCmdArgs.indexOf('Policies Consistent') !== -1, + response: { + kind: 'tm:util:bash:runstate' + } + }, + { + endpoint: '/mgmt/tm/util/bash', + options: { + times: 2 + }, + method: 'post', + request: body => body.utilCmdArgs.indexOf('profile_access_misc_stat') !== -1, + response: { + kind: 'tm:util:bash:runstate' + } + }, + { + endpoint: '/mgmt/tm/security/firewall/current-state/stats', + options: { + times: 2 + }, + response: { + kind: 'tm:security:firewall:current-state:current-statestats', + selfLink: 'https://localhost/mgmt/tm/security/firewall/current-state/stats?ver=14.1.0' + } + }, + { + endpoint: '/mgmt/tm/sys/db/ltm.configtime', + response: { + kind: 'tm:sys:db:dbstate', + selfLink: 'https://localhost/mgmt/tm/sys/db/ltm.configtime?ver=14.1.0' + } + }, + { + endpoint: '/mgmt/tm/sys/db/gtm.configtime', + response: { + kind: 'tm:sys:db:dbstate', + selfLink: 'https://localhost/mgmt/tm/sys/db/gtm.configtime?ver=14.1.0' + } + } + ] + }, + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should not fail when no data (without items property)', + statsToCollect: (stats) => { + const ret = { + system: stats.system + }; + Object.keys(stats).forEach((statKey) => { + const stat = stats[statKey]; + if (stat.structure && stat.structure.parentKey === 'system') { + ret[statKey] = stat; + } + }); + return ret; + }, + contextToCollect: context => context, + expectedData: { + system: { + hostname: 'missing data', + machineId: 'missing data', + version: 'missing data', + versionBuild: 'missing data', + location: 'missing data', + description: 'missing data', + marketingName: 'missing data', + platformId: 'missing data', + chassisId: 'missing data', + baseMac: 'missing data', + callBackUrl: 'null', + configReady: undefined, // device version missed + licenseReady: undefined, // device version missed + provisionReady: undefined, // device version missed + syncColor: 'missing data', + syncMode: 'missing data', + syncStatus: 'missing data', + syncSummary: 'missing data', + failoverStatus: 'missing data', + failoverColor: 'missing data', + systemTimestamp: 'missing data', + afmState: undefined, // not provisioned + apmState: undefined, // not provisioned + asmState: undefined, // not provisioned + gtmConfigTime: undefined, // not provisioned + lastAfmDeploy: undefined, // not provisioned + lastAsmChange: undefined, // not provisioned + ltmConfigTime: undefined, // not provisioned + cpu: 'missing data', + memory: NaN, // should be fixed and set to missing data + tmmCpu: NaN, // should be fixed and set to missing data + tmmMemory: NaN, // should be fixed and set to missing data + tmmTraffic: {}, + diskLatency: {}, + diskStorage: {}, + networkInterfaces: {}, + provisioning: {} + } + }, + endpoints: [ + { + endpoint: '/mgmt/tm/sys/provision', + options: { + times: 999 + }, + response: { + kind: 'tm:sys:provision:provisioncollectionstate', + selfLink: 'https://localhost/mgmt/tm/sys/provision?ver=14.1.0' + } + }, + { + endpoint: '/mgmt/shared/identified-devices/config/device-info', + options: { + times: 999 + }, + response: { + kind: 'shared:resolver:device-groups:deviceinfostate', + selfLink: 'https://localhost/mgmt/shared/identified-devices/config/device-info' + } + }, + { + endpoint: '/mgmt/tm/cm/device', + options: { + times: 999 + }, + response: { + kind: 'tm:cm:device:devicecollectionstate', + selfLink: 'https://localhost/mgmt/tm/cm/device?ver=14.1.0' + } + }, + { + endpoint: '/mgmt/tm/sys/management-ip', + response: { + kind: 'tm:sys:management-ip:management-ipcollectionstate', + selfLink: 'https://localhost/mgmt/tm/sys/management-ip?ver=14.1.0' + } + }, + { + endpoint: '/mgmt/tm/sys/ready', + options: { + times: 999 + }, + response: { + kind: 'tm:sys:ready:readystats', + selfLink: 'https://localhost/mgmt/tm/sys/ready?ver=14.1.0' + } + }, + { + endpoint: '/mgmt/tm/net/interface/stats', + response: { + kind: 'tm:net:interface:interfacecollectionstats', + selfLink: 'https://localhost/mgmt/tm/net/interface/stats?ver=14.1.0' + } + }, + { + endpoint: '/mgmt/tm/cm/sync-status', + options: { + times: 999 + }, + response: { + kind: 'tm:cm:sync-status:sync-statusstats', + selfLink: 'https://localhost/mgmt/tm/cm/sync-status?ver=14.1.0' + } + }, + { + endpoint: '/mgmt/tm/cm/failover-status', + options: { + times: 999 + }, + response: { + kind: 'tm:cm:failover-status:failover-statusstats', + selfLink: 'https://localhost/mgmt/tm/cm/failover-status?ver=14.1.0' + } + }, + { + endpoint: '/mgmt/tm/sys/clock', + response: { + kind: 'tm:sys:clock:clockstats', + selfLink: 'https://localhost/mgmt/tm/sys/clock?ver=14.1.0' + } + }, + { + endpoint: '/mgmt/tm/sys/host-info', + response: { + kind: 'tm:sys:host-info:host-infostats', + selfLink: 'https://localhost/mgmt/tm/sys/host-info?ver=14.1.0' + } + }, + { + endpoint: '/mgmt/tm/sys/memory', + options: { + times: 2 + }, + response: { + kind: 'tm:sys:memory:memorystats', + selfLink: 'https://localhost/mgmt/tm/sys/memory?ver=14.1.0' + } + }, + { + endpoint: '/mgmt/tm/sys/tmm-info', + response: { + kind: 'tm:sys:tmm-info:tmm-infostats', + selfLink: 'https://localhost/mgmt/tm/sys/tmm-info?ver=14.1.0' + } + }, + { + endpoint: '/mgmt/tm/sys/tmm-traffic', + response: { + kind: 'tm:sys:tmm-traffic:tmm-trafficstats', + selfLink: 'https://localhost/mgmt/tm/sys/tmm-traffic?ver=14.1.0' + } + }, + { + endpoint: '/mgmt/tm/util/bash', + method: 'post', + request: { + command: 'run', + utilCmdArgs: '-c "/bin/df -P | /usr/bin/tr -s \' \' \',\'"' + }, + response: { + kind: 'tm:util:bash:runstate' + } + }, + { + endpoint: '/mgmt/tm/util/bash', + method: 'post', + request: { + command: 'run', + utilCmdArgs: '-c "/usr/bin/iostat -x -d | /usr/bin/tail -n +3 | /usr/bin/tr -s \' \' \',\'"' + }, + response: { + kind: 'tm:util:bash:runstate' + } + }, + { + endpoint: '/mgmt/tm/util/bash', + options: { + times: 2 + }, + method: 'post', + request: body => body.utilCmdArgs.indexOf('Policies Consistent') !== -1, + response: { + kind: 'tm:util:bash:runstate' + } + }, + { + endpoint: '/mgmt/tm/util/bash', + options: { + times: 2 + }, + method: 'post', + request: body => body.utilCmdArgs.indexOf('profile_access_misc_stat') !== -1, + response: { + kind: 'tm:util:bash:runstate' + } + }, + { + endpoint: '/mgmt/tm/security/firewall/current-state/stats', + options: { + times: 2 + }, + response: { + kind: 'tm:security:firewall:current-state:current-statestats', + selfLink: 'https://localhost/mgmt/tm/security/firewall/current-state/stats?ver=14.1.0' + } + }, + { + endpoint: '/mgmt/tm/sys/db/ltm.configtime', + response: { + kind: 'tm:sys:db:dbstate', + selfLink: 'https://localhost/mgmt/tm/sys/db/ltm.configtime?ver=14.1.0' + } + }, + { + endpoint: '/mgmt/tm/sys/db/gtm.configtime', + response: { + kind: 'tm:sys:db:dbstate', + selfLink: 'https://localhost/mgmt/tm/sys/db/gtm.configtime?ver=14.1.0' + } + } + ] + } + ] + }, + /** + * TEST SET DATA STARTS HERE + * */ + collectGtmStats: { + name: 'gtm stats', + tests: [ + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should set gtm properties to undefined when gtm module is not provisioned', + statsToCollect: [ + 'aWideIps', + 'aaaaWideIps', + 'cnameWideIps', + 'mxWideIps', + 'naptrWideIps', + 'srvWideIps', + 'aPools', + 'aaaaPools', + 'cnamePools', + 'mxPools', + 'naptrPools', + 'srvPools' + ], + contextToCollect: ['provisioning'], + expectedData: { + aWideIps: undefined, + aaaaWideIps: undefined, + cnameWideIps: undefined, + mxWideIps: undefined, + naptrWideIps: undefined, + srvWideIps: undefined, + aPools: undefined, + aaaaPools: undefined, + cnamePools: undefined, + mxPools: undefined, + naptrPools: undefined, + srvPools: undefined + }, + endpoints: [ + { + endpoint: '/mgmt/tm/sys/provision', + options: { + times: 999 + }, + response: { + kind: 'tm:sys:provision:provisioncollectionstate', + selfLink: 'https://localhost/mgmt/tm/sys/provision?ver=14.1.0', + items: [ + { + name: 'gtm', + level: 'none', + // just to be sure that filterKeys works + cpuRatio: 0 + }, + { + name: 'ltm', + level: 'nominal', + // just to be sure that filterKeys works + cpuRatio: 0 + } + ] + } + } + ] + }, + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should set gtm properties to empty objects if not configured (with items property)', + statsToCollect: [ + 'aWideIps', + 'aaaaWideIps', + 'cnameWideIps', + 'mxWideIps', + 'naptrWideIps', + 'srvWideIps', + 'aPools', + 'aaaaPools', + 'cnamePools', + 'mxPools', + 'naptrPools', + 'srvPools' + ], + contextToCollect: ['provisioning'], + expectedData: { + aWideIps: undefined, + aaaaWideIps: undefined, + cnameWideIps: undefined, + mxWideIps: undefined, + naptrWideIps: undefined, + srvWideIps: undefined, + aPools: {}, + aaaaPools: {}, + cnamePools: {}, + mxPools: {}, + naptrPools: {}, + srvPools: {} + }, + endpoints: [ + { + endpoint: '/mgmt/tm/sys/provision', + options: { + times: 999 + }, + response: { + kind: 'tm:sys:provision:provisioncollectionstate', + selfLink: 'https://localhost/mgmt/tm/sys/provision?ver=14.1.0', + items: [ + { + name: 'gtm', + level: 'nominal', + // just to be sure that filterKeys works + cpuRatio: 0 + }, + { + name: 'ltm', + level: 'nominal', + // just to be sure that filterKeys works + cpuRatio: 0 + } + ] + } + }, + { + endpoint: /\/mgmt\/tm\/gtm\/\w+\/\w+$/, + options: { + times: 12 + }, + response: (uri) => { + const match = uri.match(/\/mgmt\/tm\/gtm\/(\w+)\/(\w+)$/); + return { + kind: `tm:gtm:${match[1]}:${match[2]}:${match[2]}collectionstate`, + selfLink: `https://localhost/mgmt/tm/gtm/${match[1]}/${match[2]}?ver=14.1.0`, + items: [] + }; + } + } + ] + }, + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should set gtm properties to empty objects if not configured (without items property)', + statsToCollect: [ + 'aWideIps', + 'aaaaWideIps', + 'cnameWideIps', + 'mxWideIps', + 'naptrWideIps', + 'srvWideIps', + 'aPools', + 'aaaaPools', + 'cnamePools', + 'mxPools', + 'naptrPools', + 'srvPools' + ], + contextToCollect: ['provisioning'], + expectedData: { + aWideIps: undefined, + aaaaWideIps: undefined, + cnameWideIps: undefined, + mxWideIps: undefined, + naptrWideIps: undefined, + srvWideIps: undefined, + aPools: {}, + aaaaPools: {}, + cnamePools: {}, + mxPools: {}, + naptrPools: {}, + srvPools: {} + }, + endpoints: [ + { + endpoint: '/mgmt/tm/sys/provision', + options: { + times: 999 + }, + response: { + kind: 'tm:sys:provision:provisioncollectionstate', + selfLink: 'https://localhost/mgmt/tm/sys/provision?ver=14.1.0', + items: [ + { + name: 'gtm', + level: 'nominal', + // just to be sure that filterKeys works + cpuRatio: 0 + }, + { + name: 'ltm', + level: 'nominal', + // just to be sure that filterKeys works + cpuRatio: 0 + } + ] + } + }, + { + endpoint: /\/mgmt\/tm\/gtm\/\w+\/\w+$/, + options: { + times: 12 + }, + response: (uri) => { + const match = uri.match(/\/mgmt\/tm\/gtm\/(\w+)\/(\w+)$/); + return { + kind: `tm:gtm:${match[1]}:${match[2]}:${match[2]}collectionstate`, + selfLink: `https://localhost/mgmt/tm/gtm/${match[1]}/${match[2]}?ver=14.1.0` + }; + } + } + ] + }, + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should collect gtm stats', + statsToCollect: [ + 'aWideIps', + 'aaaaWideIps', + 'cnameWideIps', + 'mxWideIps', + 'naptrWideIps', + 'srvWideIps', + 'aPools', + 'aaaaPools', + 'cnamePools', + 'mxPools', + 'naptrPools', + 'srvPools' + ], + contextToCollect: ['provisioning'], + expectedData: { + aWideIps: { + '/Common/testA.com': { + tenant: 'Common', + alternate: 0, + cnameResolutions: 0, + dropped: 0, + fallback: 0, + persisted: 0, + preferred: 0, + rcode: 0, + requests: 0, + resolutions: 0, + returnFromDns: 0, + returnToDns: 0, + 'status.availabilityState': 'unknown', + 'status.enabledState': 'enabled', + 'status.statusReason': 'Checking', + wipType: 'A', + name: '/Common/testA.com', + partition: 'Common', + enabled: true, + failureRcode: 'noerror', + failureRcodeResponse: 'disabled', + failureRcodeTtl: 0, + lastResortPool: '/Common/ts_a_pool', + loadBalancingDecisionLogVerbosity: [ + 'pool-selection', + 'pool-traversal', + 'pool-member-selection', + 'pool-member-traversal' + ], + minimalResponse: 'enabled', + persistCidrIpv4: 32, + persistCidrIpv6: 128, + persistence: 'disabled', + poolLbMode: 'round-robin', + topologyPreferEdns0ClientSubnet: 'disabled', + ttlPersistence: 3600, + aliases: [ + 'www.aone.com' + ], + pools: [ + '/Common/ts_a_pool' + ] + } + }, + aaaaWideIps: { + '/Common/testAAAA.com': { + tenant: 'Common', + alternate: 0, + cnameResolutions: 0, + dropped: 0, + fallback: 0, + persisted: 0, + preferred: 0, + rcode: 0, + requests: 0, + resolutions: 0, + returnFromDns: 0, + returnToDns: 0, + 'status.availabilityState': 'unknown', + 'status.enabledState': 'enabled', + 'status.statusReason': 'Checking', + wipType: 'AAAA', + name: '/Common/testAAAA.com', + partition: 'Common', + enabled: true, + failureRcode: 'noerror', + failureRcodeResponse: 'disabled', + failureRcodeTtl: 0, + lastResortPool: '/Common/ts_aaaa_pool', + loadBalancingDecisionLogVerbosity: [ + 'pool-selection', + 'pool-traversal', + 'pool-member-selection', + 'pool-member-traversal' + ], + minimalResponse: 'enabled', + persistCidrIpv4: 32, + persistCidrIpv6: 128, + persistence: 'disabled', + poolLbMode: 'round-robin', + topologyPreferEdns0ClientSubnet: 'disabled', + ttlPersistence: 3600, + aliases: [ + 'www.aone.com' + ], + pools: [ + '/Common/ts_aaaa_pool' + ] + } + }, + cnameWideIps: { + '/Common/testCNAME.com': { + tenant: 'Common', + alternate: 0, + cnameResolutions: 0, + dropped: 0, + fallback: 0, + persisted: 0, + preferred: 0, + rcode: 0, + requests: 0, + resolutions: 0, + returnFromDns: 0, + returnToDns: 0, + 'status.availabilityState': 'unknown', + 'status.enabledState': 'enabled', + 'status.statusReason': 'Checking', + wipType: 'CNAME', + name: '/Common/testCNAME.com', + partition: 'Common', + enabled: true, + failureRcode: 'noerror', + failureRcodeResponse: 'disabled', + failureRcodeTtl: 0, + lastResortPool: '/Common/ts_cname_pool', + loadBalancingDecisionLogVerbosity: [ + 'pool-selection', + 'pool-traversal', + 'pool-member-selection', + 'pool-member-traversal' + ], + minimalResponse: 'enabled', + persistCidrIpv4: 32, + persistCidrIpv6: 128, + persistence: 'disabled', + poolLbMode: 'round-robin', + topologyPreferEdns0ClientSubnet: 'disabled', + ttlPersistence: 3600, + aliases: [ + 'www.aone.com' + ], + pools: [ + '/Common/ts_cname_pool' + ] + } + }, + mxWideIps: { + '/Common/testMX.com': { + tenant: 'Common', + alternate: 0, + cnameResolutions: 0, + dropped: 0, + fallback: 0, + persisted: 0, + preferred: 0, + rcode: 0, + requests: 0, + resolutions: 0, + returnFromDns: 0, + returnToDns: 0, + 'status.availabilityState': 'unknown', + 'status.enabledState': 'enabled', + 'status.statusReason': 'Checking', + wipType: 'MX', + name: '/Common/testMX.com', + partition: 'Common', + enabled: true, + failureRcode: 'noerror', + failureRcodeResponse: 'disabled', + failureRcodeTtl: 0, + lastResortPool: '/Common/ts_mx_pool', + loadBalancingDecisionLogVerbosity: [ + 'pool-selection', + 'pool-traversal', + 'pool-member-selection', + 'pool-member-traversal' + ], + minimalResponse: 'enabled', + persistCidrIpv4: 32, + persistCidrIpv6: 128, + persistence: 'disabled', + poolLbMode: 'round-robin', + topologyPreferEdns0ClientSubnet: 'disabled', + ttlPersistence: 3600, + aliases: [ + 'www.aone.com' + ], + pools: [ + '/Common/ts_mx_pool' + ] + } + }, + naptrWideIps: { + '/Common/testNAPTR.com': { + tenant: 'Common', + alternate: 0, + cnameResolutions: 0, + dropped: 0, + fallback: 0, + persisted: 0, + preferred: 0, + rcode: 0, + requests: 0, + resolutions: 0, + returnFromDns: 0, + returnToDns: 0, + 'status.availabilityState': 'unknown', + 'status.enabledState': 'enabled', + 'status.statusReason': 'Checking', + wipType: 'NAPTR', + name: '/Common/testNAPTR.com', + partition: 'Common', + enabled: true, + failureRcode: 'noerror', + failureRcodeResponse: 'disabled', + failureRcodeTtl: 0, + lastResortPool: '/Common/ts_naptr_pool', + loadBalancingDecisionLogVerbosity: [ + 'pool-selection', + 'pool-traversal', + 'pool-member-selection', + 'pool-member-traversal' + ], + minimalResponse: 'enabled', + persistCidrIpv4: 32, + persistCidrIpv6: 128, + persistence: 'disabled', + poolLbMode: 'round-robin', + topologyPreferEdns0ClientSubnet: 'disabled', + ttlPersistence: 3600, + aliases: [ + 'www.aone.com' + ], + pools: [ + '/Common/ts_naptr_pool' + ] + } + }, + srvWideIps: { + '/Common/testSRV.com': { + tenant: 'Common', + alternate: 0, + cnameResolutions: 0, + dropped: 0, + fallback: 0, + persisted: 0, + preferred: 0, + rcode: 0, + requests: 0, + resolutions: 0, + returnFromDns: 0, + returnToDns: 0, + 'status.availabilityState': 'unknown', + 'status.enabledState': 'enabled', + 'status.statusReason': 'Checking', + wipType: 'SRV', + name: '/Common/testSRV.com', + partition: 'Common', + enabled: true, + failureRcode: 'noerror', + failureRcodeResponse: 'disabled', + failureRcodeTtl: 0, + lastResortPool: '/Common/ts_srv_pool', + loadBalancingDecisionLogVerbosity: [ + 'pool-selection', + 'pool-traversal', + 'pool-member-selection', + 'pool-member-traversal' + ], + minimalResponse: 'enabled', + persistCidrIpv4: 32, + persistCidrIpv6: 128, + persistence: 'disabled', + poolLbMode: 'round-robin', + topologyPreferEdns0ClientSubnet: 'disabled', + ttlPersistence: 3600, + aliases: [ + 'www.aone.com' + ], + pools: [ + '/Common/ts_srv_pool' + ] + } + }, + aPools: { + '/Common/ts_a_pool': { + tenant: 'Common', + alternate: 0, + dropped: 0, + fallback: 0, + poolType: 'A', + preferred: 0, + returnFromDns: 0, + returnToDns: 0, + availabilityState: 'offline', + enabledState: 'enabled', + 'status.statusReason': 'No enabled pool members available', + name: '/Common/ts_a_pool', + partition: 'Common', + alternateMode: 'round-robin', + dynamicRatio: 'disabled', + enabled: true, + fallbackMode: 'return-to-dns', + loadBalancingMode: 'round-robin', + manualResume: 'disabled', + maxAnswersReturned: 1, + qosHitRatio: 5, + qosHops: 0, + qosKilobytesSecond: 3, + qosLcs: 30, + qosPacketRate: 1, + qosRtt: 50, + qosTopology: 0, + qosVsCapacity: 0, + qosVsScore: 0, + ttl: 30, + verifyMemberAvailability: 'enabled', + members: { + 'vs1:testA.com': { + alternate: 0, + fallback: 0, + poolName: '/Common/ts_a_pool', + poolType: 'A', + preferred: 0, + serverName: 'testA.com', + availabilityState: 'offline', + enabledState: 'enabled', + 'status.statusReason': ' ', + vsName: 'vs1', + name: 'testA.com:vs1', + enabled: true, + memberOrder: 0, + ratio: 1, + limitMaxBps: 100, + limitMaxBpsStatus: 'disabled', + limitMaxConnections: 100, + limitMaxConnectionsStatus: 'disabled', + limitMaxPps: 100, + limitMaxPpsStatus: 'disabled', + monitor: 'default' + } + }, + fallbackIp: '8.8.8.8', + limitMaxBps: 0, + limitMaxBpsStatus: 'disabled', + limitMaxConnections: 0, + limitMaxConnectionsStatus: 'disabled', + limitMaxPps: 0, + limitMaxPpsStatus: 'disabled', + monitor: '/Common/gateway_icmp' + } + }, + aaaaPools: { + '/Common/ts_aaaa_pool': { + tenant: 'Common', + alternate: 0, + dropped: 0, + fallback: 0, + poolType: 'AAAA', + preferred: 0, + returnFromDns: 0, + returnToDns: 0, + availabilityState: 'offline', + enabledState: 'enabled', + 'status.statusReason': 'No enabled pool members available', + name: '/Common/ts_aaaa_pool', + partition: 'Common', + alternateMode: 'round-robin', + dynamicRatio: 'disabled', + enabled: true, + fallbackMode: 'return-to-dns', + loadBalancingMode: 'round-robin', + manualResume: 'disabled', + maxAnswersReturned: 1, + qosHitRatio: 5, + qosHops: 0, + qosKilobytesSecond: 3, + qosLcs: 30, + qosPacketRate: 1, + qosRtt: 50, + qosTopology: 0, + qosVsCapacity: 0, + qosVsScore: 0, + ttl: 30, + verifyMemberAvailability: 'enabled', + members: { + 'vs1:testAAAA.com': { + alternate: 0, + fallback: 0, + poolName: '/Common/ts_aaaa_pool', + poolType: 'AAAA', + preferred: 0, + serverName: 'testAAAA.com', + availabilityState: 'offline', + enabledState: 'enabled', + 'status.statusReason': ' ', + vsName: 'vs1', + name: 'testAAAA.com:vs1', + enabled: true, + memberOrder: 0, + ratio: 1, + limitMaxBps: 100, + limitMaxBpsStatus: 'disabled', + limitMaxConnections: 100, + limitMaxConnectionsStatus: 'disabled', + limitMaxPps: 100, + limitMaxPpsStatus: 'disabled', + monitor: 'default' + } + }, + fallbackIp: '8.8.8.8', + limitMaxBps: 0, + limitMaxBpsStatus: 'disabled', + limitMaxConnections: 0, + limitMaxConnectionsStatus: 'disabled', + limitMaxPps: 0, + limitMaxPpsStatus: 'disabled', + monitor: '/Common/gateway_icmp' + } + }, + cnamePools: { + '/Common/ts_cname_pool': { + tenant: 'Common', + alternate: 0, + dropped: 0, + fallback: 0, + poolType: 'CNAME', + preferred: 0, + returnFromDns: 0, + returnToDns: 0, + availabilityState: 'offline', + enabledState: 'enabled', + 'status.statusReason': 'No enabled pool members available', + name: '/Common/ts_cname_pool', + partition: 'Common', + alternateMode: 'round-robin', + dynamicRatio: 'disabled', + enabled: true, + fallbackMode: 'return-to-dns', + loadBalancingMode: 'round-robin', + manualResume: 'disabled', + maxAnswersReturned: 1, + qosHitRatio: 5, + qosHops: 0, + qosKilobytesSecond: 3, + qosLcs: 30, + qosPacketRate: 1, + qosRtt: 50, + qosTopology: 0, + qosVsCapacity: 0, + qosVsScore: 0, + ttl: 30, + verifyMemberAvailability: 'enabled', + members: { + 'testCNAME.com': { + alternate: 0, + fallback: 0, + poolName: '/Common/ts_cname_pool', + poolType: 'CNAME', + preferred: 0, + serverName: 'testCNAME.com', + availabilityState: 'offline', + enabledState: 'enabled', + 'status.statusReason': ' ', + vsName: ' ' + } + } + } + }, + mxPools: { + '/Common/ts_mx_pool': { + tenant: 'Common', + alternate: 0, + dropped: 0, + fallback: 0, + poolType: 'MX', + preferred: 0, + returnFromDns: 0, + returnToDns: 0, + availabilityState: 'offline', + enabledState: 'enabled', + 'status.statusReason': 'No enabled pool members available', + name: '/Common/ts_mx_pool', + partition: 'Common', + alternateMode: 'round-robin', + dynamicRatio: 'disabled', + enabled: true, + fallbackMode: 'return-to-dns', + loadBalancingMode: 'round-robin', + manualResume: 'disabled', + maxAnswersReturned: 1, + qosHitRatio: 5, + qosHops: 0, + qosKilobytesSecond: 3, + qosLcs: 30, + qosPacketRate: 1, + qosRtt: 50, + qosTopology: 0, + qosVsCapacity: 0, + qosVsScore: 0, + ttl: 30, + verifyMemberAvailability: 'enabled', + members: { + 'testMX.com': { + alternate: 0, + fallback: 0, + poolName: '/Common/ts_mx_pool', + poolType: 'MX', + preferred: 0, + serverName: 'testMX.com', + availabilityState: 'offline', + enabledState: 'enabled', + 'status.statusReason': ' ', + vsName: ' ' + } + } + } + }, + naptrPools: { + '/Common/ts_naptr_pool': { + tenant: 'Common', + alternate: 0, + dropped: 0, + fallback: 0, + poolType: 'NAPTR', + preferred: 0, + returnFromDns: 0, + returnToDns: 0, + availabilityState: 'offline', + enabledState: 'enabled', + 'status.statusReason': 'No enabled pool members available', + name: '/Common/ts_naptr_pool', + partition: 'Common', + alternateMode: 'round-robin', + dynamicRatio: 'disabled', + enabled: true, + fallbackMode: 'return-to-dns', + loadBalancingMode: 'round-robin', + manualResume: 'disabled', + maxAnswersReturned: 1, + qosHitRatio: 5, + qosHops: 0, + qosKilobytesSecond: 3, + qosLcs: 30, + qosPacketRate: 1, + qosRtt: 50, + qosTopology: 0, + qosVsCapacity: 0, + qosVsScore: 0, + ttl: 30, + verifyMemberAvailability: 'enabled', + members: { + 'testNAPTR.com': { + alternate: 0, + fallback: 0, + poolName: '/Common/ts_naptr_pool', + poolType: 'NAPTR', + preferred: 0, + serverName: 'testNAPTR.com', + availabilityState: 'offline', + enabledState: 'enabled', + 'status.statusReason': ' ', + vsName: ' ' + } + } + } + }, + srvPools: { + '/Common/ts_srv_pool': { + tenant: 'Common', + alternate: 0, + dropped: 0, + fallback: 0, + poolType: 'SRV', + preferred: 0, + returnFromDns: 0, + returnToDns: 0, + availabilityState: 'offline', + enabledState: 'enabled', + 'status.statusReason': 'No enabled pool members available', + name: '/Common/ts_srv_pool', + partition: 'Common', + alternateMode: 'round-robin', + dynamicRatio: 'disabled', + enabled: true, + fallbackMode: 'return-to-dns', + loadBalancingMode: 'round-robin', + manualResume: 'disabled', + maxAnswersReturned: 1, + qosHitRatio: 5, + qosHops: 0, + qosKilobytesSecond: 3, + qosLcs: 30, + qosPacketRate: 1, + qosRtt: 50, + qosTopology: 0, + qosVsCapacity: 0, + qosVsScore: 0, + ttl: 30, + verifyMemberAvailability: 'enabled', + members: { + 'testSRV.com': { + alternate: 0, + fallback: 0, + poolName: '/Common/ts_srv_pool', + poolType: 'SRV', + preferred: 0, + serverName: 'testSRV.com', + availabilityState: 'offline', + enabledState: 'enabled', + 'status.statusReason': ' ', + vsName: ' ' + } + } + } + } + }, + endpoints: [ + { + endpoint: '/mgmt/tm/sys/provision', + options: { + times: 999 + }, + response: { + kind: 'tm:sys:provision:provisioncollectionstate', + selfLink: 'https://localhost/mgmt/tm/sys/provision?ver=14.1.0', + items: [ + { + name: 'gtm', + level: 'nominal', + // just to be sure that filterKeys works + cpuRatio: 0 + }, + { + name: 'ltm', + level: 'nominal', + // just to be sure that filterKeys works + cpuRatio: 0 + } + ] + } + }, + { + endpoint: /\/mgmt\/tm\/gtm\/wideip\/\w+$/, + options: { + times: 6 + }, + response: (uri) => { + const recType = uri.match(/\/mgmt\/tm\/gtm\/wideip\/(\w+)$/)[1].toLowerCase(); + const recTypeUC = recType.toUpperCase(); + return { + kind: `tm:gtm:wideip:${recType}:srvcollectionstate`, + selfLink: `https://localhost/mgmt/tm/gtm/wideip/${recType}?ver=14.1.0`, + items: [ + { + kind: `tm:gtm:wideip:${recType}:${recType}state`, + name: `test${recTypeUC}.com`, + partition: 'Common', + fullPath: `/Common/test${recTypeUC}.com`, + generation: 9060, + selfLink: `https://localhost/mgmt/tm/gtm/wideip/${recType}/~Common~test${recTypeUC}.com?ver=14.1.0`, + enabled: true, + failureRcode: 'noerror', + failureRcodeResponse: 'disabled', + failureRcodeTtl: 0, + lastResortPool: `${recType} /Common/ts_${recType}_pool`, + loadBalancingDecisionLogVerbosity: [ + 'pool-selection', + 'pool-traversal', + 'pool-member-selection', + 'pool-member-traversal' + ], + minimalResponse: 'enabled', + persistCidrIpv4: 32, + persistCidrIpv6: 128, + persistence: 'disabled', + poolLbMode: 'round-robin', + topologyPreferEdns0ClientSubnet: 'disabled', + ttlPersistence: 3600, + aliases: [ + 'www.aone.com' + ], + pools: [ + { + name: `ts_${recType}_pool`, + partition: 'Common', + order: 0, + ratio: 1, + nameReference: { + link: `https://localhost/mgmt/tm/gtm/pool/${recType}/~Common~ts_${recType}_pool?ver=14.1.0` + } + } + ] + } + ] + }; + } + }, + { + endpoint: /\/mgmt\/tm\/gtm\/wideip\/\w+\/~Common~test\w+.com\/stats$/, + options: { + times: 6 + }, + response: (uri) => { + const recType = uri.match(/\/mgmt\/tm\/gtm\/wideip\/(\w+)\/~Common~test\w+.com\/stats$/)[1].toLowerCase(); + const recTypeUC = recType.toUpperCase(); + return { + kind: `tm:gtm:wideip:${recType}:${recType}stats`, + selfLink: `https://localhost/mgmt/tm/gtm/wideip/${recType}/~Common~test${recTypeUC}.com/stats?ver=14.1.0`, + entries: { + [`https://localhost/mgmt/tm/gtm/wideip/${recType}/~Common~test${recTypeUC}.com/~Common~test${recTypeUC}.com:${recTypeUC}/stats`]: { + nestedStats: { + kind: `tm:gtm:wideip:${recType}:${recType}stats`, + selfLink: `https://localhost/mgmt/tm/gtm/wideip/${recType}/~Common~test${recTypeUC}.com/~Common~test${recTypeUC}.com:${recTypeUC}/stats?ver=14.1.0`, + entries: { + alternate: { + value: 0 + }, + cnameResolutions: { + value: 0 + }, + dropped: { + value: 0 + }, + fallback: { + value: 0 + }, + persisted: { + value: 0 + }, + preferred: { + value: 0 + }, + rcode: { + value: 0 + }, + requests: { + value: 0 + }, + resolutions: { + value: 0 + }, + returnFromDns: { + value: 0 + }, + returnToDns: { + value: 0 + }, + 'status.availabilityState': { + description: 'unknown' + }, + 'status.enabledState': { + description: 'enabled' + }, + 'status.statusReason': { + description: 'Checking' + }, + wipName: { + description: `/Common/test${recTypeUC}.com` + }, + wipType: { + description: recTypeUC + } + } + } + } + } + }; + } + }, + { + endpoint: /\/mgmt\/tm\/gtm\/pool\/\w+$/, + options: { + times: 6 + }, + response: (uri) => { + const recType = uri.match(/\/mgmt\/tm\/gtm\/pool\/(\w+)$/)[1].toLowerCase(); + const ret = { + kind: `tm:gtm:pool:${recType}:${recType}collectionstate`, + selfLink: `https://localhost/mgmt/tm/gtm/pool/${recType}?ver=14.1.0`, + items: [ + { + kind: `tm:gtm:pool:${recType}:${recType}state`, + name: `ts_${recType}_pool`, + partition: 'Common', + fullPath: `/Common/ts_${recType}_pool`, + generation: 9053, + selfLink: `https://localhost/mgmt/tm/gtm/pool/${recType}/~Common~ts_${recType}_pool?ver=14.1.0`, + alternateMode: 'round-robin', + dynamicRatio: 'disabled', + enabled: true, + fallbackMode: 'return-to-dns', + loadBalancingMode: 'round-robin', + manualResume: 'disabled', + maxAnswersReturned: 1, + qosHitRatio: 5, + qosHops: 0, + qosKilobytesSecond: 3, + qosLcs: 30, + qosPacketRate: 1, + qosRtt: 50, + qosTopology: 0, + qosVsCapacity: 0, + qosVsScore: 0, + ttl: 30, + verifyMemberAvailability: 'enabled', + membersReference: { + link: `https://localhost/mgmt/tm/gtm/pool/${recType}/~Common~ts_${recType}_pool/members?ver=14.1.0`, + isSubcollection: true + } + } + ] + }; + if (recType === 'a' || recType === 'aaaa') { + Object.assign(ret.items[0], { + fallbackIp: '8.8.8.8', + fallbackMode: 'return-to-dns', + limitMaxBps: 0, + limitMaxBpsStatus: 'disabled', + limitMaxConnections: 0, + limitMaxConnectionsStatus: 'disabled', + limitMaxPps: 0, + limitMaxPpsStatus: 'disabled', + monitor: '/Common/gateway_icmp' + }); + } + return ret; + } + }, + { + endpoint: /\/mgmt\/tm\/gtm\/pool\/\w+\/~Common~ts_\w+_pool\/members\/stats$/, + options: { + times: 6 + }, + response: (uri) => { + const recType = uri.match(/\/mgmt\/tm\/gtm\/pool\/(\w+)\/~Common~ts_\w+_pool\/members\/stats$/)[1].toLowerCase(); + const recTypeUC = recType.toUpperCase(); + let vsName = '%20'; + if (recType === 'a' || recType === 'aaaa') { + vsName = 'vs1'; + } + return { + kind: `tm:gtm:pool:${recType}:members:memberscollectionstats`, + selfLink: `https://localhost/mgmt/tm/gtm/pool/${recType}/~Common~ts_${recType}_pool/members/stats?ver=14.1.0`, + entries: { + [`https://localhost/mgmt/tm/gtm/pool/${recType}/~Common~ts_${recType}_pool/members/${vsName}:test${recTypeUC}.com/stats`]: { + nestedStats: { + kind: 'tm:gtm:pool:mx:members:membersstats', + selfLink: `https://localhost/mgmt/tm/gtm/pool/${recType}/~Common~ts_${recType}_pool/members/${vsName}:test${recTypeUC}.com/stats?ver=14.1.0`, + entries: { + alternate: { + value: 0 + }, + fallback: { + value: 0 + }, + poolName: { + description: `/Common/ts_${recType}_pool` + }, + poolType: { + description: recTypeUC + }, + preferred: { + value: 0 + }, + serverName: { + description: `test${recTypeUC}.com` + }, + 'status.availabilityState': { + description: 'offline' + }, + 'status.enabledState': { + description: 'enabled' + }, + 'status.statusReason': { + description: ' ' + }, + vsName: { + description: vsName === '%20' ? ' ' : vsName + } + } + } + } + } + }; + } + }, + { + endpoint: /\/mgmt\/tm\/gtm\/pool\/\w+\/~Common~ts_\w+_pool\/members$/, + options: { + times: 6 + }, + response: (uri) => { + const recType = uri.match(/\/mgmt\/tm\/gtm\/pool\/(\w+)\/~Common~ts_\w+_pool\/members$/)[1].toLowerCase(); + const recTypeUC = recType.toUpperCase(); + let vsName = ''; + if (recType === 'a' || recType === 'aaaa') { + vsName = ':vs1'; + } + + const ret = { + kind: `tm:gtm:pool:${recType}:members:memberscollectionstate`, + selfLink: `https://localhost/mgmt/tm/gtm/pool/${recType}/~Common~ts_${recType}_pool/members?ver=14.1.0`, + items: [ + { + kind: `tm:gtm:pool:${recType}:members:membersstate`, + name: `test${recTypeUC}.com${vsName}`, + fullPath: `test${recTypeUC}.com${vsName}`, + generation: 237, + selfLink: `https://localhost/mgmt/tm/gtm/pool/${recType}/~Common~ts_${recType}_pool/members/test${recTypeUC}.com${vsName}?ver=14.1.0`, + enabled: true, + memberOrder: 0, + ratio: 1 + } + ] + }; + if (recType === 'mx') { + ret.items[0].priority = 100; + } + if (recType === 'cname') { + ret.items[0].staticTarget = 'no'; + } + if (recType === 'naptr') { + Object.assign(ret.items[0], { + flags: 'a', + preference: 100, + service: '80' + }); + } + if (recType === 'srv') { + Object.assign(ret.items[0], { + port: 80, + priority: 10, + weight: 10 + }); + } + if (recType === 'a' || recType === 'aaaa') { + Object.assign(ret.items[0], { + limitMaxBps: 100, + limitMaxBpsStatus: 'disabled', + limitMaxConnections: 100, + limitMaxConnectionsStatus: 'disabled', + limitMaxPps: 100, + limitMaxPpsStatus: 'disabled', + monitor: 'default' + }); + } + return ret; + } + }, + { + endpoint: /\/mgmt\/tm\/gtm\/pool\/\w+\/~Common~ts_\w+_pool\/stats$/, + options: { + times: 6 + }, + response: (uri) => { + const recType = uri.match(/\/mgmt\/tm\/gtm\/pool\/(\w+)\/~Common~ts_\w+_pool\/stats$/)[1].toLowerCase(); + const recTypeUC = recType.toUpperCase(); + return { + kind: `tm:gtm:pool:${recType}:${recType}stats`, + selfLink: `https://localhost/mgmt/tm/gtm/pool/${recType}/~Common~ts_${recType}_pool/stats?ver=14.1.0`, + entries: { + [`https://localhost/mgmt/tm/gtm/pool/${recType}/~Common~ts_${recType}_pool/~Common~ts_${recType}_pool:${recTypeUC}/stats`]: { + nestedStats: { + kind: `tm:gtm:pool:${recType}:${recType}stats`, + selfLink: `https://localhost/mgmt/tm/gtm/pool/${recType}/~Common~ts_${recType}_pool/~Common~ts_${recType}_pool:${recTypeUC}/stats?ver=14.1.0`, + entries: { + alternate: { + value: 0 + }, + dropped: { + value: 0 + }, + fallback: { + value: 0 + }, + tmName: { + description: `/Common/ts_${recType}_pool` + }, + poolType: { + description: recTypeUC + }, + preferred: { + value: 0 + }, + returnFromDns: { + value: 0 + }, + returnToDns: { + value: 0 + }, + 'status.availabilityState': { + description: 'offline' + }, + 'status.enabledState': { + description: 'enabled' + }, + 'status.statusReason': { + description: 'No enabled pool members available' + } + } + } + } + } + }; + } + } + ] + }, + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should set members to empty object when pool has no members', + statsToCollect: [ + 'aPools', + 'aaaaPools', + 'cnamePools', + 'mxPools', + 'naptrPools', + 'srvPools' + ], + contextToCollect: ['provisioning'], + expectedData: { + aPools: { + '/Common/ts_a_pool': { + tenant: 'Common', + alternate: 0, + dropped: 0, + fallback: 0, + poolType: 'A', + preferred: 0, + returnFromDns: 0, + returnToDns: 0, + availabilityState: 'offline', + enabledState: 'enabled', + 'status.statusReason': 'No enabled pool members available', + name: '/Common/ts_a_pool', + partition: 'Common', + alternateMode: 'round-robin', + dynamicRatio: 'disabled', + enabled: true, + fallbackMode: 'return-to-dns', + loadBalancingMode: 'round-robin', + manualResume: 'disabled', + maxAnswersReturned: 1, + qosHitRatio: 5, + qosHops: 0, + qosKilobytesSecond: 3, + qosLcs: 30, + qosPacketRate: 1, + qosRtt: 50, + qosTopology: 0, + qosVsCapacity: 0, + qosVsScore: 0, + ttl: 30, + verifyMemberAvailability: 'enabled', + members: {}, + fallbackIp: '8.8.8.8', + limitMaxBps: 0, + limitMaxBpsStatus: 'disabled', + limitMaxConnections: 0, + limitMaxConnectionsStatus: 'disabled', + limitMaxPps: 0, + limitMaxPpsStatus: 'disabled', + monitor: '/Common/gateway_icmp' + } + }, + aaaaPools: { + '/Common/ts_aaaa_pool': { + tenant: 'Common', + alternate: 0, + dropped: 0, + fallback: 0, + poolType: 'AAAA', + preferred: 0, + returnFromDns: 0, + returnToDns: 0, + availabilityState: 'offline', + enabledState: 'enabled', + 'status.statusReason': 'No enabled pool members available', + name: '/Common/ts_aaaa_pool', + partition: 'Common', + alternateMode: 'round-robin', + dynamicRatio: 'disabled', + enabled: true, + fallbackMode: 'return-to-dns', + loadBalancingMode: 'round-robin', + manualResume: 'disabled', + maxAnswersReturned: 1, + qosHitRatio: 5, + qosHops: 0, + qosKilobytesSecond: 3, + qosLcs: 30, + qosPacketRate: 1, + qosRtt: 50, + qosTopology: 0, + qosVsCapacity: 0, + qosVsScore: 0, + ttl: 30, + verifyMemberAvailability: 'enabled', + members: {}, + fallbackIp: '8.8.8.8', + limitMaxBps: 0, + limitMaxBpsStatus: 'disabled', + limitMaxConnections: 0, + limitMaxConnectionsStatus: 'disabled', + limitMaxPps: 0, + limitMaxPpsStatus: 'disabled', + monitor: '/Common/gateway_icmp' + } + }, + cnamePools: { + '/Common/ts_cname_pool': { + tenant: 'Common', + alternate: 0, + dropped: 0, + fallback: 0, + poolType: 'CNAME', + preferred: 0, + returnFromDns: 0, + returnToDns: 0, + availabilityState: 'offline', + enabledState: 'enabled', + 'status.statusReason': 'No enabled pool members available', + name: '/Common/ts_cname_pool', + partition: 'Common', + alternateMode: 'round-robin', + dynamicRatio: 'disabled', + enabled: true, + fallbackMode: 'return-to-dns', + loadBalancingMode: 'round-robin', + manualResume: 'disabled', + maxAnswersReturned: 1, + qosHitRatio: 5, + qosHops: 0, + qosKilobytesSecond: 3, + qosLcs: 30, + qosPacketRate: 1, + qosRtt: 50, + qosTopology: 0, + qosVsCapacity: 0, + qosVsScore: 0, + ttl: 30, + verifyMemberAvailability: 'enabled', + members: {} + } + }, + mxPools: { + '/Common/ts_mx_pool': { + tenant: 'Common', + alternate: 0, + dropped: 0, + fallback: 0, + poolType: 'MX', + preferred: 0, + returnFromDns: 0, + returnToDns: 0, + availabilityState: 'offline', + enabledState: 'enabled', + 'status.statusReason': 'No enabled pool members available', + name: '/Common/ts_mx_pool', + partition: 'Common', + alternateMode: 'round-robin', + dynamicRatio: 'disabled', + enabled: true, + fallbackMode: 'return-to-dns', + loadBalancingMode: 'round-robin', + manualResume: 'disabled', + maxAnswersReturned: 1, + qosHitRatio: 5, + qosHops: 0, + qosKilobytesSecond: 3, + qosLcs: 30, + qosPacketRate: 1, + qosRtt: 50, + qosTopology: 0, + qosVsCapacity: 0, + qosVsScore: 0, + ttl: 30, + verifyMemberAvailability: 'enabled', + members: {} + } + }, + naptrPools: { + '/Common/ts_naptr_pool': { + tenant: 'Common', + alternate: 0, + dropped: 0, + fallback: 0, + poolType: 'NAPTR', + preferred: 0, + returnFromDns: 0, + returnToDns: 0, + availabilityState: 'offline', + enabledState: 'enabled', + 'status.statusReason': 'No enabled pool members available', + name: '/Common/ts_naptr_pool', + partition: 'Common', + alternateMode: 'round-robin', + dynamicRatio: 'disabled', + enabled: true, + fallbackMode: 'return-to-dns', + loadBalancingMode: 'round-robin', + manualResume: 'disabled', + maxAnswersReturned: 1, + qosHitRatio: 5, + qosHops: 0, + qosKilobytesSecond: 3, + qosLcs: 30, + qosPacketRate: 1, + qosRtt: 50, + qosTopology: 0, + qosVsCapacity: 0, + qosVsScore: 0, + ttl: 30, + verifyMemberAvailability: 'enabled', + members: {} + } + }, + srvPools: { + '/Common/ts_srv_pool': { + tenant: 'Common', + alternate: 0, + dropped: 0, + fallback: 0, + poolType: 'SRV', + preferred: 0, + returnFromDns: 0, + returnToDns: 0, + availabilityState: 'offline', + enabledState: 'enabled', + 'status.statusReason': 'No enabled pool members available', + name: '/Common/ts_srv_pool', + partition: 'Common', + alternateMode: 'round-robin', + dynamicRatio: 'disabled', + enabled: true, + fallbackMode: 'return-to-dns', + loadBalancingMode: 'round-robin', + manualResume: 'disabled', + maxAnswersReturned: 1, + qosHitRatio: 5, + qosHops: 0, + qosKilobytesSecond: 3, + qosLcs: 30, + qosPacketRate: 1, + qosRtt: 50, + qosTopology: 0, + qosVsCapacity: 0, + qosVsScore: 0, + ttl: 30, + verifyMemberAvailability: 'enabled', + members: {} + } + } + }, + endpoints: [ + { + endpoint: '/mgmt/tm/sys/provision', + options: { + times: 999 + }, + response: { + kind: 'tm:sys:provision:provisioncollectionstate', + selfLink: 'https://localhost/mgmt/tm/sys/provision?ver=14.1.0', + items: [ + { + name: 'gtm', + level: 'nominal', + // just to be sure that filterKeys works + cpuRatio: 0 + }, + { + name: 'ltm', + level: 'nominal', + // just to be sure that filterKeys works + cpuRatio: 0 + } + ] + } + }, + { + endpoint: /\/mgmt\/tm\/gtm\/pool\/\w+$/, + options: { + times: 6 + }, + response: (uri) => { + const recType = uri.match(/\/mgmt\/tm\/gtm\/pool\/(\w+)$/)[1].toLowerCase(); + const ret = { + kind: `tm:gtm:pool:${recType}:${recType}collectionstate`, + selfLink: `https://localhost/mgmt/tm/gtm/pool/${recType}?ver=14.1.0`, + items: [ + { + kind: `tm:gtm:pool:${recType}:${recType}state`, + name: `ts_${recType}_pool`, + partition: 'Common', + fullPath: `/Common/ts_${recType}_pool`, + generation: 9053, + selfLink: `https://localhost/mgmt/tm/gtm/pool/${recType}/~Common~ts_${recType}_pool?ver=14.1.0`, + alternateMode: 'round-robin', + dynamicRatio: 'disabled', + enabled: true, + fallbackMode: 'return-to-dns', + loadBalancingMode: 'round-robin', + manualResume: 'disabled', + maxAnswersReturned: 1, + qosHitRatio: 5, + qosHops: 0, + qosKilobytesSecond: 3, + qosLcs: 30, + qosPacketRate: 1, + qosRtt: 50, + qosTopology: 0, + qosVsCapacity: 0, + qosVsScore: 0, + ttl: 30, + verifyMemberAvailability: 'enabled', + membersReference: { + link: `https://localhost/mgmt/tm/gtm/pool/${recType}/~Common~ts_${recType}_pool/members?ver=14.1.0`, + isSubcollection: true + } + } + ] + }; + if (recType === 'a' || recType === 'aaaa') { + Object.assign(ret.items[0], { + fallbackIp: '8.8.8.8', + fallbackMode: 'return-to-dns', + limitMaxBps: 0, + limitMaxBpsStatus: 'disabled', + limitMaxConnections: 0, + limitMaxConnectionsStatus: 'disabled', + limitMaxPps: 0, + limitMaxPpsStatus: 'disabled', + monitor: '/Common/gateway_icmp' + }); + } + return ret; + } + }, + { + endpoint: /\/mgmt\/tm\/gtm\/pool\/\w+\/~Common~ts_\w+_pool\/members\/stats$/, + options: { + times: 6 + }, + response: (uri) => { + const recType = uri.match(/\/mgmt\/tm\/gtm\/pool\/(\w+)\/~Common~ts_\w+_pool\/members\/stats$/)[1].toLowerCase(); + return { + kind: `tm:gtm:pool:${recType}:members:memberscollectionstats`, + selfLink: `https://localhost/mgmt/tm/gtm/pool/${recType}/~Common~ts_${recType}_pool/members/stats?ver=14.1.0` + }; + } + }, + { + endpoint: /\/mgmt\/tm\/gtm\/pool\/\w+\/~Common~ts_\w+_pool\/members$/, + options: { + times: 6 + }, + response: (uri) => { + const recType = uri.match(/\/mgmt\/tm\/gtm\/pool\/(\w+)\/~Common~ts_\w+_pool\/members$/)[1].toLowerCase(); + return { + kind: `tm:gtm:pool:${recType}:members:memberscollectionstate`, + selfLink: `https://localhost/mgmt/tm/gtm/pool/${recType}/~Common~ts_${recType}_pool/members?ver=14.1.0` + }; + } + }, + { + endpoint: /\/mgmt\/tm\/gtm\/pool\/\w+\/~Common~ts_\w+_pool\/stats$/, + options: { + times: 6 + }, + response: (uri) => { + const recType = uri.match(/\/mgmt\/tm\/gtm\/pool\/(\w+)\/~Common~ts_\w+_pool\/stats$/)[1].toLowerCase(); + const recTypeUC = recType.toUpperCase(); + return { + kind: `tm:gtm:pool:${recType}:${recType}stats`, + selfLink: `https://localhost/mgmt/tm/gtm/pool/${recType}/~Common~ts_${recType}_pool/stats?ver=14.1.0`, + entries: { + [`https://localhost/mgmt/tm/gtm/pool/${recType}/~Common~ts_${recType}_pool/~Common~ts_${recType}_pool:${recTypeUC}/stats`]: { + nestedStats: { + kind: `tm:gtm:pool:${recType}:${recType}stats`, + selfLink: `https://localhost/mgmt/tm/gtm/pool/${recType}/~Common~ts_${recType}_pool/~Common~ts_${recType}_pool:${recTypeUC}/stats?ver=14.1.0`, + entries: { + alternate: { + value: 0 + }, + dropped: { + value: 0 + }, + fallback: { + value: 0 + }, + tmName: { + description: `/Common/ts_${recType}_pool` + }, + poolType: { + description: recTypeUC + }, + preferred: { + value: 0 + }, + returnFromDns: { + value: 0 + }, + returnToDns: { + value: 0 + }, + 'status.availabilityState': { + description: 'offline' + }, + 'status.enabledState': { + description: 'enabled' + }, + 'status.statusReason': { + description: 'No enabled pool members available' + } + } + } + } + } + }; + } + } + ] + } + ] + }, + /** + * TEST SET DATA STARTS HERE + * */ + collectVirtualServers: { + name: 'virtual servers stats', + tests: [ + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should set virtualServers to empty object if not configured (with items property)', + statsToCollect: ['virtualServers'], + contextToCollect: [], + expectedData: { + virtualServers: {} + }, + endpoints: [ + { + endpoint: /\/mgmt\/tm\/ltm\/virtual/, + response: { + kind: 'tm:ltm:virtual:virtualcollectionstate', + selfLink: 'https://localhost/mgmt/tm/ltm/virtual?ver=14.1.0', + items: [] + } + } + ] + }, + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should set virtualServers to empty object if not configured (without items property)', + statsToCollect: ['virtualServers'], + contextToCollect: [], + expectedData: { + virtualServers: {} + }, + endpoints: [ + { + endpoint: /\/mgmt\/tm\/ltm\/virtual/, + response: { + kind: 'tm:ltm:virtual:virtualcollectionstate', + selfLink: 'https://localhost/mgmt/tm/ltm/virtual?ver=14.1.0' + } + } + ] + }, + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should collect virtual servers stats', + statsToCollect: ['virtualServers'], + contextToCollect: [], + expectedData: { + virtualServers: { + '/Common/app/test_vs_0': { + availabilityState: 'unknown', + 'clientside.bitsIn': 0, + 'clientside.bitsOut': 0, + 'clientside.curConns': 0, + destination: '10.11.0.2:80', + enabledState: 'enabled', + ipProtocol: 'tcp', + mask: '255.255.255.255', + name: '/Common/app/test_vs_0', + pool: '/Common/test_pool_0', + tenant: 'Common', + application: 'app' + } + } + }, + endpoints: [ + { + endpoint: /\/mgmt\/tm\/ltm\/virtual/, + response: { + kind: 'tm:ltm:virtual:virtualcollectionstate', + selfLink: 'https://localhost/mgmt/tm/ltm/virtual?ver=14.1.0', + items: [ + { + name: 'test_vs_0', + fullPath: '/Common/app/test_vs_0', + selfLink: 'https://localhost/mgmt/tm/ltm/virtual/~Common~app~test_vs_0?ver=14.1.0', + ipProtocol: 'tcp', + mask: '255.255.255.255', + pool: '/Common/test_pool_0', + poolReference: { + link: 'https://localhost/mgmt/tm/ltm/pool/~Common~test_pool_0?ver=14.1.0' + } + } + ] + } + }, + { + endpoint: '/mgmt/tm/ltm/virtual/~Common~app~test_vs_0/stats', + response: { + kind: 'tm:ltm:virtual:virtualstats', + selfLink: 'https://localhost/mgmt/tm/ltm/virtual/~Common~app~test_vs_0/stats?ver=14.1.0', + entries: { + 'https://localhost/mgmt/tm/ltm/virtual/~Common~app~test_vs_0/stats': { + nestedStats: { + kind: 'tm:ltm:virtual:virtualstats', + selfLink: 'https://localhost/mgmt/tm/ltm/virtual/~Common~app~test_vs_0/stats?ver=14.1.0', + entries: { + 'clientside.bitsIn': { + value: 0 + }, + 'clientside.bitsOut': { + value: 0 + }, + 'clientside.curConns': { + value: 0 + }, + 'clientside.evictedConns': { + value: 0 + }, + 'clientside.maxConns': { + value: 0 + }, + 'clientside.pktsIn': { + value: 0 + }, + 'clientside.pktsOut': { + value: 0 + }, + 'clientside.slowKilled': { + value: 0 + }, + 'clientside.totConns': { + value: 0 + }, + cmpEnableMode: { + description: 'all-cpus' + }, + cmpEnabled: { + description: 'enabled' + }, + csMaxConnDur: { + value: 0 + }, + csMeanConnDur: { + value: 0 + }, + csMinConnDur: { + value: 0 + }, + destination: { + description: '10.11.0.2:80' + }, + 'ephemeral.bitsIn': { + value: 0 + }, + 'ephemeral.bitsOut': { + value: 0 + }, + 'ephemeral.curConns': { + value: 0 + }, + 'ephemeral.evictedConns': { + value: 0 + }, + 'ephemeral.maxConns': { + value: 0 + }, + 'ephemeral.pktsIn': { + value: 0 + }, + 'ephemeral.pktsOut': { + value: 0 + }, + 'ephemeral.slowKilled': { + value: 0 + }, + 'ephemeral.totConns': { + value: 0 + }, + fiveMinAvgUsageRatio: { + value: 0 + }, + fiveSecAvgUsageRatio: { + value: 0 + }, + tmName: { + description: '/Common/test_vs_0' + }, + oneMinAvgUsageRatio: { + value: 0 + }, + 'status.availabilityState': { + description: 'unknown' + }, + 'status.enabledState': { + description: 'enabled' + }, + 'status.statusReason': { + description: 'The children pool member(s) either don\'t have service checking enabled, or service check results are not available yet' + }, + syncookieStatus: { + description: 'not-activated' + }, + 'syncookie.accepts': { + value: 0 + }, + 'syncookie.hwAccepts': { + value: 0 + }, + 'syncookie.hwSyncookies': { + value: 0 + }, + 'syncookie.hwsyncookieInstance': { + value: 0 + }, + 'syncookie.rejects': { + value: 0 + }, + 'syncookie.swsyncookieInstance': { + value: 0 + }, + 'syncookie.syncacheCurr': { + value: 0 + }, + 'syncookie.syncacheOver': { + value: 0 + }, + 'syncookie.syncookies': { + value: 0 + }, + totRequests: { + value: 0 + } + } + } + } + } + } + } + ] + }, + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should collect virtual servers stats and expand profilesReference', + statsToCollect: ['virtualServers'], + contextToCollect: [], + expectedData: { + virtualServers: { + '/Common/test_vs_0': { + tenant: 'Common', + availabilityState: 'unknown', + 'clientside.bitsIn': 0, + 'clientside.bitsOut': 0, + 'clientside.curConns': 0, + destination: '10.11.0.2:80', + enabledState: 'enabled', + ipProtocol: 'tcp', + mask: '255.255.255.255', + name: '/Common/test_vs_0', + pool: '/Common/test_pool_0', + profiles: { + '/Common/f5-tcp-lan': { + name: '/Common/f5-tcp-lan', + tenant: 'Common' + }, + '/Common/http': { + name: '/Common/http', + tenant: 'Common' + }, + '/Common/http-proxy-connect': { + name: '/Common/http-proxy-connect', + tenant: 'Common' + }, + '/Common/tcp': { + name: '/Common/tcp', + tenant: 'Common' + } + } + } + } + }, + endpoints: [ + { + endpoint: /\/mgmt\/tm\/ltm\/virtual/, + response: { + kind: 'tm:ltm:virtual:virtualcollectionstate', + selfLink: 'https://localhost/mgmt/tm/ltm/virtual?ver=14.1.0', + items: [ + { + name: 'test_vs_0', + fullPath: '/Common/test_vs_0', + selfLink: 'https://localhost/mgmt/tm/ltm/virtual/~Common~test_vs_0?ver=14.1.0', + ipProtocol: 'tcp', + mask: '255.255.255.255', + pool: '/Common/test_pool_0', + poolReference: { + link: 'https://localhost/mgmt/tm/ltm/pool/~Common~test_pool_0?ver=14.1.0' + }, + profilesReference: { + link: 'https://localhost/mgmt/tm/ltm/virtual/~Common~test_vs_0/profiles?ver=14.1.0', + isSubcollection: true + } + } + ] + } + }, + { + endpoint: '/mgmt/tm/ltm/virtual/~Common~test_vs_0/stats', + response: { + kind: 'tm:ltm:virtual:virtualstats', + selfLink: 'https://localhost/mgmt/tm/ltm/virtual/~Common~test_vs_0/stats?ver=14.1.0', + entries: { + 'https://localhost/mgmt/tm/ltm/virtual/~Common~test_vs_0/stats': { + nestedStats: { + kind: 'tm:ltm:virtual:virtualstats', + selfLink: 'https://localhost/mgmt/tm/ltm/virtual/~Common~test_vs_0/stats?ver=14.1.0', + entries: { + 'clientside.bitsIn': { + value: 0 + }, + 'clientside.bitsOut': { + value: 0 + }, + 'clientside.curConns': { + value: 0 + }, + 'clientside.evictedConns': { + value: 0 + }, + 'clientside.maxConns': { + value: 0 + }, + 'clientside.pktsIn': { + value: 0 + }, + 'clientside.pktsOut': { + value: 0 + }, + 'clientside.slowKilled': { + value: 0 + }, + 'clientside.totConns': { + value: 0 + }, + cmpEnableMode: { + description: 'all-cpus' + }, + cmpEnabled: { + description: 'enabled' + }, + csMaxConnDur: { + value: 0 + }, + csMeanConnDur: { + value: 0 + }, + csMinConnDur: { + value: 0 + }, + destination: { + description: '10.11.0.2:80' + }, + 'ephemeral.bitsIn': { + value: 0 + }, + 'ephemeral.bitsOut': { + value: 0 + }, + 'ephemeral.curConns': { + value: 0 + }, + 'ephemeral.evictedConns': { + value: 0 + }, + 'ephemeral.maxConns': { + value: 0 + }, + 'ephemeral.pktsIn': { + value: 0 + }, + 'ephemeral.pktsOut': { + value: 0 + }, + 'ephemeral.slowKilled': { + value: 0 + }, + 'ephemeral.totConns': { + value: 0 + }, + fiveMinAvgUsageRatio: { + value: 0 + }, + fiveSecAvgUsageRatio: { + value: 0 + }, + tmName: { + description: '/Common/test_vs_0' + }, + oneMinAvgUsageRatio: { + value: 0 + }, + 'status.availabilityState': { + description: 'unknown' + }, + 'status.enabledState': { + description: 'enabled' + }, + 'status.statusReason': { + description: 'The children pool member(s) either don\'t have service checking enabled, or service check results are not available yet' + }, + syncookieStatus: { + description: 'not-activated' + }, + 'syncookie.accepts': { + value: 0 + }, + 'syncookie.hwAccepts': { + value: 0 + }, + 'syncookie.hwSyncookies': { + value: 0 + }, + 'syncookie.hwsyncookieInstance': { + value: 0 + }, + 'syncookie.rejects': { + value: 0 + }, + 'syncookie.swsyncookieInstance': { + value: 0 + }, + 'syncookie.syncacheCurr': { + value: 0 + }, + 'syncookie.syncacheOver': { + value: 0 + }, + 'syncookie.syncookies': { + value: 0 + }, + totRequests: { + value: 0 + } + } + } + } + } + } + }, + { + endpoint: '/mgmt/tm/ltm/virtual/~Common~test_vs_0/profiles?$select=name,fullPath', + response: { + kind: 'tm:ltm:virtual:profiles:profilescollectionstate', + selfLink: 'https://localhost/mgmt/tm/ltm/virtual/~Common~test_vs/profiles?$select=name%2CfullPath&ver=14.1.0', + items: [ + { + name: 'f5-tcp-lan', + fullPath: '/Common/f5-tcp-lan' + }, + { + name: 'http', + fullPath: '/Common/http' + }, + { + name: 'http-proxy-connect', + fullPath: '/Common/http-proxy-connect' + }, + { + name: 'tcp', + fullPath: '/Common/tcp' + } + ] + } + } + ] + }, + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should expand profilesReference even if no profiles attached (with items property)', + statsToCollect: ['virtualServers'], + contextToCollect: [], + expectedData: { + virtualServers: { + '/Common/test_vs_0': { + tenant: 'Common', + availabilityState: 'unknown', + 'clientside.bitsIn': 0, + 'clientside.bitsOut': 0, + 'clientside.curConns': 0, + destination: '10.11.0.2:80', + enabledState: 'enabled', + ipProtocol: 'tcp', + mask: '255.255.255.255', + name: '/Common/test_vs_0', + pool: '/Common/test_pool_0', + profiles: {} + } + } + }, + endpoints: [ + { + endpoint: /\/mgmt\/tm\/ltm\/virtual/, + response: { + kind: 'tm:ltm:virtual:virtualcollectionstate', + selfLink: 'https://localhost/mgmt/tm/ltm/virtual?ver=14.1.0', + items: [ + { + name: 'test_vs_0', + fullPath: '/Common/test_vs_0', + selfLink: 'https://localhost/mgmt/tm/ltm/virtual/~Common~test_vs_0?ver=14.1.0', + ipProtocol: 'tcp', + mask: '255.255.255.255', + pool: '/Common/test_pool_0', + poolReference: { + link: 'https://localhost/mgmt/tm/ltm/pool/~Common~test_pool_0?ver=14.1.0' + }, + profilesReference: { + link: 'https://localhost/mgmt/tm/ltm/virtual/~Common~test_vs_0/profiles?ver=14.1.0', + isSubcollection: true + } + } + ] + } + }, + { + endpoint: '/mgmt/tm/ltm/virtual/~Common~test_vs_0/stats', + response: { + kind: 'tm:ltm:virtual:virtualstats', + selfLink: 'https://localhost/mgmt/tm/ltm/virtual/~Common~test_vs_0/stats?ver=14.1.0', + entries: { + 'https://localhost/mgmt/tm/ltm/virtual/~Common~test_vs_0/stats': { + nestedStats: { + kind: 'tm:ltm:virtual:virtualstats', + selfLink: 'https://localhost/mgmt/tm/ltm/virtual/~Common~test_vs_0/stats?ver=14.1.0', + entries: { + 'clientside.bitsIn': { + value: 0 + }, + 'clientside.bitsOut': { + value: 0 + }, + 'clientside.curConns': { + value: 0 + }, + 'clientside.evictedConns': { + value: 0 + }, + 'clientside.maxConns': { + value: 0 + }, + 'clientside.pktsIn': { + value: 0 + }, + 'clientside.pktsOut': { + value: 0 + }, + 'clientside.slowKilled': { + value: 0 + }, + 'clientside.totConns': { + value: 0 + }, + cmpEnableMode: { + description: 'all-cpus' + }, + cmpEnabled: { + description: 'enabled' + }, + csMaxConnDur: { + value: 0 + }, + csMeanConnDur: { + value: 0 + }, + csMinConnDur: { + value: 0 + }, + destination: { + description: '10.11.0.2:80' + }, + 'ephemeral.bitsIn': { + value: 0 + }, + 'ephemeral.bitsOut': { + value: 0 + }, + 'ephemeral.curConns': { + value: 0 + }, + 'ephemeral.evictedConns': { + value: 0 + }, + 'ephemeral.maxConns': { + value: 0 + }, + 'ephemeral.pktsIn': { + value: 0 + }, + 'ephemeral.pktsOut': { + value: 0 + }, + 'ephemeral.slowKilled': { + value: 0 + }, + 'ephemeral.totConns': { + value: 0 + }, + fiveMinAvgUsageRatio: { + value: 0 + }, + fiveSecAvgUsageRatio: { + value: 0 + }, + tmName: { + description: '/Common/test_vs_0' + }, + oneMinAvgUsageRatio: { + value: 0 + }, + 'status.availabilityState': { + description: 'unknown' + }, + 'status.enabledState': { + description: 'enabled' + }, + 'status.statusReason': { + description: 'The children pool member(s) either don\'t have service checking enabled, or service check results are not available yet' + }, + syncookieStatus: { + description: 'not-activated' + }, + 'syncookie.accepts': { + value: 0 + }, + 'syncookie.hwAccepts': { + value: 0 + }, + 'syncookie.hwSyncookies': { + value: 0 + }, + 'syncookie.hwsyncookieInstance': { + value: 0 + }, + 'syncookie.rejects': { + value: 0 + }, + 'syncookie.swsyncookieInstance': { + value: 0 + }, + 'syncookie.syncacheCurr': { + value: 0 + }, + 'syncookie.syncacheOver': { + value: 0 + }, + 'syncookie.syncookies': { + value: 0 + }, + totRequests: { + value: 0 + } + } + } + } + } + } + }, + { + endpoint: '/mgmt/tm/ltm/virtual/~Common~test_vs_0/profiles?$select=name,fullPath', + response: { + kind: 'tm:ltm:virtual:profiles:profilescollectionstate', + selfLink: 'https://localhost/mgmt/tm/ltm/virtual/~Common~test_vs/profiles?$select=name%2CfullPath&ver=14.1.0', + items: [] + } + } + ] + }, + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should expand profilesReference even if no profiles attached (without items property)', + statsToCollect: ['virtualServers'], + contextToCollect: [], + expectedData: { + virtualServers: { + '/Common/test_vs_0': { + tenant: 'Common', + availabilityState: 'unknown', + 'clientside.bitsIn': 0, + 'clientside.bitsOut': 0, + 'clientside.curConns': 0, + destination: '10.11.0.2:80', + enabledState: 'enabled', + ipProtocol: 'tcp', + mask: '255.255.255.255', + name: '/Common/test_vs_0', + pool: '/Common/test_pool_0', + profiles: {} + } + } + }, + endpoints: [ + { + endpoint: /\/mgmt\/tm\/ltm\/virtual/, + response: { + kind: 'tm:ltm:virtual:virtualcollectionstate', + selfLink: 'https://localhost/mgmt/tm/ltm/virtual?ver=14.1.0', + items: [ + { + name: 'test_vs_0', + fullPath: '/Common/test_vs_0', + selfLink: 'https://localhost/mgmt/tm/ltm/virtual/~Common~test_vs_0?ver=14.1.0', + ipProtocol: 'tcp', + mask: '255.255.255.255', + pool: '/Common/test_pool_0', + poolReference: { + link: 'https://localhost/mgmt/tm/ltm/pool/~Common~test_pool_0?ver=14.1.0' + }, + profilesReference: { + link: 'https://localhost/mgmt/tm/ltm/virtual/~Common~test_vs_0/profiles?ver=14.1.0', + isSubcollection: true + } + } + ] + } + }, + { + endpoint: '/mgmt/tm/ltm/virtual/~Common~test_vs_0/stats', + response: { + kind: 'tm:ltm:virtual:virtualstats', + selfLink: 'https://localhost/mgmt/tm/ltm/virtual/~Common~test_vs_0/stats?ver=14.1.0', + entries: { + 'https://localhost/mgmt/tm/ltm/virtual/~Common~test_vs_0/stats': { + nestedStats: { + kind: 'tm:ltm:virtual:virtualstats', + selfLink: 'https://localhost/mgmt/tm/ltm/virtual/~Common~test_vs_0/stats?ver=14.1.0', + entries: { + 'clientside.bitsIn': { + value: 0 + }, + 'clientside.bitsOut': { + value: 0 + }, + 'clientside.curConns': { + value: 0 + }, + 'clientside.evictedConns': { + value: 0 + }, + 'clientside.maxConns': { + value: 0 + }, + 'clientside.pktsIn': { + value: 0 + }, + 'clientside.pktsOut': { + value: 0 + }, + 'clientside.slowKilled': { + value: 0 + }, + 'clientside.totConns': { + value: 0 + }, + cmpEnableMode: { + description: 'all-cpus' + }, + cmpEnabled: { + description: 'enabled' + }, + csMaxConnDur: { + value: 0 + }, + csMeanConnDur: { + value: 0 + }, + csMinConnDur: { + value: 0 + }, + destination: { + description: '10.11.0.2:80' + }, + 'ephemeral.bitsIn': { + value: 0 + }, + 'ephemeral.bitsOut': { + value: 0 + }, + 'ephemeral.curConns': { + value: 0 + }, + 'ephemeral.evictedConns': { + value: 0 + }, + 'ephemeral.maxConns': { + value: 0 + }, + 'ephemeral.pktsIn': { + value: 0 + }, + 'ephemeral.pktsOut': { + value: 0 + }, + 'ephemeral.slowKilled': { + value: 0 + }, + 'ephemeral.totConns': { + value: 0 + }, + fiveMinAvgUsageRatio: { + value: 0 + }, + fiveSecAvgUsageRatio: { + value: 0 + }, + tmName: { + description: '/Common/test_vs_0' + }, + oneMinAvgUsageRatio: { + value: 0 + }, + 'status.availabilityState': { + description: 'unknown' + }, + 'status.enabledState': { + description: 'enabled' + }, + 'status.statusReason': { + description: 'The children pool member(s) either don\'t have service checking enabled, or service check results are not available yet' + }, + syncookieStatus: { + description: 'not-activated' + }, + 'syncookie.accepts': { + value: 0 + }, + 'syncookie.hwAccepts': { + value: 0 + }, + 'syncookie.hwSyncookies': { + value: 0 + }, + 'syncookie.hwsyncookieInstance': { + value: 0 + }, + 'syncookie.rejects': { + value: 0 + }, + 'syncookie.swsyncookieInstance': { + value: 0 + }, + 'syncookie.syncacheCurr': { + value: 0 + }, + 'syncookie.syncacheOver': { + value: 0 + }, + 'syncookie.syncookies': { + value: 0 + }, + totRequests: { + value: 0 + } + } + } + } + } + } + }, + { + endpoint: '/mgmt/tm/ltm/virtual/~Common~test_vs_0/profiles?$select=name,fullPath', + response: { + kind: 'tm:ltm:virtual:profiles:profilescollectionstate', + selfLink: 'https://localhost/mgmt/tm/ltm/virtual/~Common~test_vs/profiles?$select=name%2CfullPath&ver=14.1.0' + } + } + ] + } + ] + }, + /** + * TEST SET DATA STARTS HERE + * */ + collectPools: { + name: 'pools stats', + tests: [ + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should set pools to empty object if not configured (with items property)', + statsToCollect: ['pools'], + contextToCollect: [], + expectedData: { + pools: {} + }, + endpoints: [ + { + endpoint: /\/mgmt\/tm\/ltm\/pool/, + response: { + kind: 'tm:ltm:pool:poolcollectionstate', + selfLink: 'https://localhost/mgmt/tm/ltm/pool?ver=14.1.0', + items: [] + } + } + ] + }, + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should set pools to empty object if not configured (without items property)', + statsToCollect: ['pools'], + contextToCollect: [], + expectedData: { + pools: {} + }, + endpoints: [ + { + endpoint: /\/mgmt\/tm\/ltm\/pool/, + response: { + kind: 'tm:ltm:pool:poolcollectionstate', + selfLink: 'https://localhost/mgmt/tm/ltm/pool?ver=14.1.0' + } + } + ] + }, + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should collect pools stats', + statsToCollect: ['pools'], + contextToCollect: [], + expectedData: { + pools: { + '/Common/test_pool_0': { + tenant: 'Common', + activeMemberCnt: 0, + availabilityState: 'unknown', + curPriogrp: 0, + enabledState: 'enabled', + highestPriogrp: 0, + lowestPriogrp: 0, + members: { + '/Common/10.10.0.2:80': { + addr: '10.10.0.2', + availabilityState: 'unknown', + enabledState: 'enabled', + port: 80, + 'serverside.bitsIn': 0, + 'serverside.bitsOut': 0, + 'serverside.curConns': 0 + } + }, + name: '/Common/test_pool_0', + 'serverside.bitsIn': 0, + 'serverside.bitsOut': 0, + 'serverside.curConns': 0 + } + } + }, + endpoints: [ + { + endpoint: /\/mgmt\/tm\/ltm\/pool/, + response: { + kind: 'tm:ltm:pool:poolcollectionstate', + items: [ + { + kind: 'tm:ltm:pool:poolstate', + name: 'test_pool_0', + partition: 'Common', + fullPath: '/Common/test_pool_0', + generation: 1876, + selfLink: 'https://localhost/mgmt/tm/ltm/pool/~Common~test_pool_0?ver=14.1.0', + allowNat: 'yes', + allowSnat: 'yes', + ignorePersistedWeight: 'disabled', + ipTosToClient: 'pass-through', + ipTosToServer: 'pass-through', + linkQosToClient: 'pass-through', + linkQosToServer: 'pass-through', + loadBalancingMode: 'round-robin', + minActiveMembers: 0, + minUpMembers: 0, + minUpMembersAction: 'failover', + minUpMembersChecking: 'disabled', + queueDepthLimit: 0, + queueOnConnectionLimit: 'disabled', + queueTimeLimit: 0, + reselectTries: 0, + serviceDownAction: 'none', + slowRampTime: 10, + membersReference: { + link: 'https://localhost/mgmt/tm/ltm/pool/~Common~test_pool_0/members?ver=14.1.0', + isSubcollection: true + } + } + ] + } + }, + { + endpoint: '/mgmt/tm/ltm/pool/~Common~test_pool_0/members/stats', + response: { + kind: 'tm:ltm:pool:members:memberscollectionstats', + selfLink: 'https://localhost/mgmt/tm/ltm/pool/~Common~test_pool_0/members/stats?ver=14.1.0', + entries: { + 'https://localhost/mgmt/tm/ltm/pool/~Common~test_pool_0/members/~Common~10.10.0.2:80/stats': { + nestedStats: { + kind: 'tm:ltm:pool:members:membersstats', + selfLink: 'https://localhost/mgmt/tm/ltm/pool/~Common~test_pool_0/members/~Common~10.10.0.2:80/stats?ver=14.1.0', + entries: { + addr: { + description: '10.10.0.2' + }, + 'connq.ageEdm': { + value: 0 + }, + 'connq.ageEma': { + value: 0 + }, + 'connq.ageHead': { + value: 0 + }, + 'connq.ageMax': { + value: 0 + }, + 'connq.depth': { + value: 0 + }, + 'connq.serviced': { + value: 0 + }, + curSessions: { + value: 0 + }, + monitorRule: { + description: 'none' + }, + monitorStatus: { + description: 'unchecked' + }, + nodeName: { + description: '/Common/10.10.0.2' + }, + poolName: { + description: '/Common/test_pool_0' + }, + port: { + value: 80 + }, + 'serverside.bitsIn': { + value: 0 + }, + 'serverside.bitsOut': { + value: 0 + }, + 'serverside.curConns': { + value: 0 + }, + 'serverside.maxConns': { + value: 0 + }, + 'serverside.pktsIn': { + value: 0 + }, + 'serverside.pktsOut': { + value: 0 + }, + 'serverside.totConns': { + value: 0 + }, + sessionStatus: { + description: 'enabled' + }, + 'status.availabilityState': { + description: 'unknown' + }, + 'status.enabledState': { + description: 'enabled' + }, + 'status.statusReason': { + description: 'Pool member does not have service checking enabled' + }, + totRequests: { + value: 0 + } + } + } + } + } + } + }, + { + endpoint: '/mgmt/tm/ltm/pool/~Common~test_pool_0/stats', + response: { + kind: 'tm:ltm:pool:poolstats', + selfLink: 'https://localhost/mgmt/tm/ltm/pool/~Common~test_pool_0/stats?ver=14.1.0', + entries: { + 'https://localhost/mgmt/tm/ltm/pool/~Common~test_pool_0/stats': { + nestedStats: { + kind: 'tm:ltm:pool:poolstats', + selfLink: 'https://localhost/mgmt/tm/ltm/pool/~Common~test_pool_0/stats?ver=14.1.0', + entries: { + activeMemberCnt: { + value: 0 + }, + availableMemberCnt: { + value: 3 + }, + 'connqAll.ageEdm': { + value: 0 + }, + 'connqAll.ageEma': { + value: 0 + }, + 'connqAll.ageHead': { + value: 0 + }, + 'connqAll.ageMax': { + value: 0 + }, + 'connqAll.depth': { + value: 0 + }, + 'connqAll.serviced': { + value: 0 + }, + 'connq.ageEdm': { + value: 0 + }, + 'connq.ageEma': { + value: 0 + }, + 'connq.ageHead': { + value: 0 + }, + 'connq.ageMax': { + value: 0 + }, + 'connq.depth': { + value: 0 + }, + 'connq.serviced': { + value: 0 + }, + curPriogrp: { + value: 0 + }, + curSessions: { + value: 0 + }, + highestPriogrp: { + value: 0 + }, + lowestPriogrp: { + value: 0 + }, + memberCnt: { + value: 3 + }, + minActiveMembers: { + value: 0 + }, + monitorRule: { + description: 'none' + }, + tmName: { + description: '/Common/test_pool_0' + }, + 'serverside.bitsIn': { + value: 0 + }, + 'serverside.bitsOut': { + value: 0 + }, + 'serverside.curConns': { + value: 0 + }, + 'serverside.maxConns': { + value: 0 + }, + 'serverside.pktsIn': { + value: 0 + }, + 'serverside.pktsOut': { + value: 0 + }, + 'serverside.totConns': { + value: 0 + }, + 'status.availabilityState': { + description: 'unknown' + }, + 'status.enabledState': { + description: 'enabled' + }, + 'status.statusReason': { + description: 'The children pool member(s) either don\'t have service checking enabled, or service check results are not available yet' + }, + totRequests: { + value: 0 + } + } + } + } + } + } + } + ] + } + ] + }, + /** + * TEST SET DATA STARTS HERE + * */ + collectLtmPolicies: { + name: 'ltm policies stats', + tests: [ + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should set ltm policies to empty object if not configured', + statsToCollect: ['ltmPolicies'], + contextToCollect: [], + expectedData: { + ltmPolicies: {} + }, + endpoints: [ + { + endpoint: /\/mgmt\/tm\/ltm\/policy\/stats/, + response: { + kind: 'tm:ltm:policy:policycollectionstats', + selfLink: 'https://localhost/mgmt/tm/ltm/policy/stats?ver=14.1.0' + } + } + ] + }, + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should collect ltm policies stats', + statsToCollect: ['ltmPolicies'], + contextToCollect: [], + expectedData: { + ltmPolicies: { + '/Common/asm_auto_l7_policy__test_vs': { + actions: { + 'default:1': { + invoked: 1, + succeeded: 1 + } + }, + invoked: 1, + name: '/Common/asm_auto_l7_policy__test_vs', + tenant: 'Common', + succeeded: 1 + } + } + }, + endpoints: [ + { + endpoint: /\/mgmt\/tm\/ltm\/policy\/stats/, + response: { + kind: 'tm:ltm:policy:policycollectionstats', + selfLink: 'https://localhost/mgmt/tm/ltm/policy/stats?ver=14.1.0', + entries: { + 'https://localhost/mgmt/tm/ltm/policy/~Common~asm_auto_l7_policy__test_vs/stats': { + nestedStats: { + kind: 'tm:ltm:policy:policystats', + selfLink: 'https://localhost/mgmt/tm/ltm/policy/~Common~asm_auto_l7_policy__test_vs/stats?ver=14.1.0', + entries: { + invoked: { + value: 1 + }, + policyName: { + description: '/Common/asm_auto_l7_policy__test_vs' + }, + succeeded: { + value: 1 + }, + vsName: { + description: 'N/A' + }, + 'https://localhost/mgmt/tm/ltm/policy/~Common~asm_auto_l7_policy__test_vs/actions/stats': { + nestedStats: { + entries: { + 'https://localhost/mgmt/tm/ltm/policy/~Common~asm_auto_l7_policy__test_vs/actions/default:1/stats': { + nestedStats: { + entries: { + action: { + description: 'enable' + }, + actionId: { + value: 1 + }, + invoked: { + value: 1 + }, + ruleName: { + description: 'default' + }, + succeeded: { + value: 1 + }, + tmTarget: { + description: 'asm' + } + } + } + } + } + } + } + } + } + } + } + } + } + ] + } + ] + }, + /** + * TEST SET DATA STARTS HERE + * */ + collectHttpProfiles: { + name: 'http profiles stats', + tests: [ + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should set http profiles to empty object if not configured', + statsToCollect: ['httpProfiles'], + contextToCollect: [], + expectedData: { + httpProfiles: {} + }, + endpoints: [ + { + endpoint: /\/mgmt\/tm\/ltm\/profile\/http\/stats/, + response: { + kind: 'tm:ltm:policy:policycollectionstats', + selfLink: 'https://localhost/mgmt/tm/ltm/policy/stats?ver=14.1.0' + } + } + ] + }, + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should collect http profiles stats', + statsToCollect: ['httpProfiles'], + contextToCollect: [], + expectedData: { + httpProfiles: { + '/Common/http': { + '2xxResp': 1, + '3xxResp': 0, + '4xxResp': 0, + '5xxResp': 0, + cookiePersistInserts: 0, + getReqs: 1, + maxKeepaliveReq: 1, + name: '/Common/http', + tenant: 'Common', + numberReqs: 1, + postReqs: 0, + respGreaterThan2m: 0, + respLessThan2m: 0, + v10Reqs: 0, + v10Resp: 0, + v11Reqs: 1, + v11Resp: 1, + v9Reqs: 0, + v9Resp: 0 + } + } + }, + endpoints: [ + { + endpoint: /\/mgmt\/tm\/ltm\/profile\/http\/stats/, + response: { + kind: 'tm:ltm:profile:http:httpcollectionstats', + selfLink: 'https://localhost/mgmt/tm/ltm/profile/http/stats?ver=14.1.0', + entries: { + 'https://localhost/mgmt/tm/ltm/profile/http/~Common~http/stats': { + nestedStats: { + kind: 'tm:ltm:profile:http:httpstats', + selfLink: 'https://localhost/mgmt/tm/ltm/profile/http/~Common~http/stats?ver=14.1.0', + entries: { + cookiePersistInserts: { + value: 0 + }, + getReqs: { + value: 1 + }, + maxKeepaliveReq: { + value: 1 + }, + tmName: { + description: '/Common/http' + }, + numberReqs: { + value: 1 + }, + passthroughConnect: { + value: 0 + }, + passthroughExcessClientHeaders: { + value: 0 + }, + passthroughExcessServerHeaders: { + value: 0 + }, + passthroughHeaders: { + value: 0 + }, + passthroughIrule: { + value: 0 + }, + passthroughOversizeClientHeaders: { + value: 0 + }, + passthroughOversizeServerHeaders: { + value: 0 + }, + passthroughPipeline: { + value: 0 + }, + passthroughUnknownMethod: { + value: 0 + }, + passthroughWebSockets: { + value: 0 + }, + postReqs: { + value: 0 + }, + proxyConnReqs: { + value: 0 + }, + proxyReqs: { + value: 0 + }, + resp_2xxCnt: { + value: 1 + }, + resp_3xxCnt: { + value: 0 + }, + resp_4xxCnt: { + value: 0 + }, + resp_5xxCnt: { + value: 0 + }, + respBucket_128k: { + value: 0 + }, + respBucket_16k: { + value: 1 + }, + respBucket_1k: { + value: 0 + }, + respBucket_2m: { + value: 0 + }, + respBucket_32k: { + value: 0 + }, + respBucket_4k: { + value: 0 + }, + respBucket_512k: { + value: 0 + }, + respBucket_64k: { + value: 0 + }, + respBucketLarge: { + value: 0 + }, + typeId: { + description: 'ltm profile http' + }, + v10Reqs: { + value: 0 + }, + v10Resp: { + value: 0 + }, + v11Reqs: { + value: 1 + }, + v11Resp: { + value: 1 + }, + v9Reqs: { + value: 0 + }, + v9Resp: { + value: 0 + }, + vsName: { + description: 'N/A' + } + } + } + } + } + } + } + ] + } + ] + }, + /** + * TEST SET DATA STARTS HERE + * */ + collectClientSslProfiles: { + name: 'client ssl profiles stats', + tests: [ + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should set client ssl to empty object if not configured', + statsToCollect: ['clientSslProfiles'], + contextToCollect: [], + expectedData: { + clientSslProfiles: {} + }, + endpoints: [ + { + endpoint: /\/mgmt\/tm\/ltm\/profile\/client-ssl\/stats/, + response: { + kind: 'tm:ltm:profile:client-ssl:client-sslcollectionstats', + selfLink: 'https://localhost/mgmt/tm/ltm/client-ssl/stats?ver=14.1.0' + } + } + ] + }, + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should collect client ssl profiles stats', + statsToCollect: ['clientSslProfiles'], + contextToCollect: [], + expectedData: { + clientSslProfiles: { + '/Common/clientssl': { + activeHandshakeRejected: 0, + 'cipherUses.chacha20Poly1305Bulk': 0, + currentCompatibleConnections: 0, + currentConnections: 0, + currentNativeConnections: 0, + currentActiveHandshakes: 0, + decryptedBytesIn: 0, + decryptedBytesOut: 0, + encryptedBytesIn: 0, + encryptedBytesOut: 0, + fatalAlerts: 0, + handshakeFailures: 0, + peercertInvalid: 0, + peercertNone: 0, + peercertValid: 0, + 'protocolUses.dtlsv1': 0, + 'protocolUses.sslv2': 0, + 'protocolUses.sslv3': 0, + 'protocolUses.tlsv1': 0, + 'protocolUses.tlsv1_1': 0, + 'protocolUses.tlsv1_2': 0, + 'protocolUses.tlsv1_3': 0, + recordsIn: 0, + recordsOut: 0, + sniRejects: 0, + name: '/Common/clientssl', + tenant: 'Common' + } + } + }, + endpoints: [ + { + endpoint: /\/mgmt\/tm\/ltm\/profile\/client-ssl\/stats/, + response: { + kind: 'tm:ltm:profile:client-ssl:client-sslcollectionstats', + selfLink: 'https://localhost/mgmt/tm/ltm/profile/client-ssl/stats?ver=14.1.0', + entries: { + 'https://localhost/mgmt/tm/ltm/profile/client-ssl/~Common~clientssl/stats': { + nestedStats: { + kind: 'tm:ltm:profile:client-ssl:client-sslstats', + selfLink: 'https://localhost/mgmt/tm/ltm/profile/client-ssl/~Common~clientssl/stats?ver=14.1.0', + entries: { + 'common.activeHandshakeRejected': { + value: 0 + }, + 'common.aggregateRenegotiationsRejected': { + value: 0 + }, + 'common.badRecords': { + value: 0 + }, + 'common.c3dUses.conns': { + value: 0 + }, + 'common.cipherUses.adhKeyxchg': { + value: 0 + }, + 'common.cipherUses.aesBulk': { + value: 0 + }, + 'common.cipherUses.aesGcmBulk': { + value: 0 + }, + 'common.cipherUses.camelliaBulk': { + value: 0 + }, + 'common.cipherUses.chacha20Poly1305Bulk': { + value: 0 + }, + 'common.cipherUses.desBulk': { + value: 0 + }, + 'common.cipherUses.dhRsaKeyxchg': { + value: 0 + }, + 'common.cipherUses.dheDssKeyxchg': { + value: 0 + }, + 'common.cipherUses.ecdhEcdsaKeyxchg': { + value: 0 + }, + 'common.cipherUses.ecdhRsaKeyxchg': { + value: 0 + }, + 'common.cipherUses.ecdheEcdsaKeyxchg': { + value: 0 + }, + 'common.cipherUses.ecdheRsaKeyxchg': { + value: 0 + }, + 'common.cipherUses.edhRsaKeyxchg': { + value: 0 + }, + 'common.cipherUses.ideaBulk': { + value: 0 + }, + 'common.cipherUses.md5Digest': { + value: 0 + }, + 'common.cipherUses.nullBulk': { + value: 0 + }, + 'common.cipherUses.nullDigest': { + value: 0 + }, + 'common.cipherUses.rc2Bulk': { + value: 0 + }, + 'common.cipherUses.rc4Bulk': { + value: 0 + }, + 'common.cipherUses.rsaKeyxchg': { + value: 0 + }, + 'common.cipherUses.shaDigest': { + value: 0 + }, + 'common.connectionMirroring.haCtxRecv': { + value: 0 + }, + 'common.connectionMirroring.haCtxSent': { + value: 0 + }, + 'common.connectionMirroring.haFailure': { + value: 0 + }, + 'common.connectionMirroring.haHsSuccess': { + value: 0 + }, + 'common.connectionMirroring.haPeerReady': { + value: 0 + }, + 'common.connectionMirroring.haTimeout': { + value: 0 + }, + 'common.curCompatConns': { + value: 0 + }, + 'common.curConns': { + value: 0 + }, + 'common.curNativeConns': { + value: 0 + }, + 'common.currentActiveHandshakes': { + value: 0 + }, + 'common.decryptedBytesIn': { + value: 0 + }, + 'common.decryptedBytesOut': { + value: 0 + }, + 'common.dtlsTxPushbacks': { + value: 0 + }, + 'common.encryptedBytesIn': { + value: 0 + }, + 'common.encryptedBytesOut': { + value: 0 + }, + 'common.extendedMasterSecrets': { + value: 0 + }, + 'common.fatalAlerts': { + value: 0 + }, + 'common.fullyHwAcceleratedConns': { + value: 0 + }, + 'common.fwdpUses.alertBypasses': { + value: 0 + }, + 'common.fwdpUses.cachedCerts': { + value: 0 + }, + 'common.fwdpUses.clicertFailBypasses': { + value: 0 + }, + 'common.fwdpUses.conns': { + value: 0 + }, + 'common.fwdpUses.dipBypasses': { + value: 0 + }, + 'common.fwdpUses.enforceResumeFailures': { + value: 0 + }, + 'common.fwdpUses.hnBypasses': { + value: 0 + }, + 'common.fwdpUses.sipBypasses': { + value: 0 + }, + 'common.fwdpUses.transparentResumeCnt': { + value: 0 + }, + 'common.fwdpUses.verifiedHsCnt': { + value: 0 + }, + 'common.handshakeFailures': { + value: 0 + }, + 'common.insecureHandshakeAccepts': { + value: 0 + }, + 'common.insecureHandshakeRejects': { + value: 0 + }, + 'common.insecureRenegotiationRejects': { + value: 0 + }, + 'common.maxCompatConns': { + value: 0 + }, + 'common.maxConns': { + value: 0 + }, + 'common.maxNativeConns': { + value: 0 + }, + 'common.midstreamRenegotiations': { + value: 0 + }, + 'common.nonHwAcceleratedConns': { + value: 0 + }, + 'common.ocspFwdpClientssl.cachedResp': { + value: 0 + }, + 'common.ocspFwdpClientssl.certStatusReq': { + value: 0 + }, + 'common.ocspFwdpClientssl.invalidCertResp': { + value: 0 + }, + 'common.ocspFwdpClientssl.respstatusErrResp': { + value: 0 + }, + 'common.ocspFwdpClientssl.revokedResp': { + value: 0 + }, + 'common.ocspFwdpClientssl.stapledResp': { + value: 0 + }, + 'common.ocspFwdpClientssl.unknownResp': { + value: 0 + }, + 'common.partiallyHwAcceleratedConns': { + value: 0 + }, + 'common.peercertInvalid': { + value: 0 + }, + 'common.peercertNone': { + value: 0 + }, + 'common.peercertValid': { + value: 0 + }, + 'common.prematureDisconnects': { + value: 0 + }, + 'common.protocolUses.dtlsv1': { + value: 0 + }, + 'common.protocolUses.sslv2': { + value: 0 + }, + 'common.protocolUses.sslv3': { + value: 0 + }, + 'common.protocolUses.tlsv1': { + value: 0 + }, + 'common.protocolUses.tlsv1_1': { + value: 0 + }, + 'common.protocolUses.tlsv1_2': { + value: 0 + }, + 'common.protocolUses.tlsv1_3': { + value: 0 + }, + 'common.recordsIn': { + value: 0 + }, + 'common.recordsOut': { + value: 0 + }, + 'common.renegotiationsRejected': { + value: 0 + }, + 'common.secureHandshakes': { + value: 0 + }, + 'common.sessCacheCurEntries': { + value: 0 + }, + 'common.sessCacheHits': { + value: 0 + }, + 'common.sessCacheInvalidations': { + value: 0 + }, + 'common.sessCacheLookups': { + value: 0 + }, + 'common.sessCacheOverflows': { + value: 0 + }, + 'common.sessionMirroring.failure': { + value: 0 + }, + 'common.sessionMirroring.success': { + value: 0 + }, + 'common.sesstickUses.reuseFailed': { + value: 0 + }, + 'common.sesstickUses.reused': { + value: 0 + }, + 'common.sniRejects': { + value: 0 + }, + 'common.totCompatConns': { + value: 0 + }, + 'common.totNativeConns': { + value: 0 + }, + 'dynamicRecord.x1': { + value: 0 + }, + 'dynamicRecord.x10': { + value: 0 + }, + 'dynamicRecord.x11': { + value: 0 + }, + 'dynamicRecord.x12': { + value: 0 + }, + 'dynamicRecord.x13': { + value: 0 + }, + 'dynamicRecord.x14': { + value: 0 + }, + 'dynamicRecord.x15': { + value: 0 + }, + 'dynamicRecord.x16': { + value: 0 + }, + 'dynamicRecord.x2': { + value: 0 + }, + 'dynamicRecord.x3': { + value: 0 + }, + 'dynamicRecord.x4': { + value: 0 + }, + 'dynamicRecord.x5': { + value: 0 + }, + 'dynamicRecord.x6': { + value: 0 + }, + 'dynamicRecord.x7': { + value: 0 + }, + 'dynamicRecord.x8': { + value: 0 + }, + 'dynamicRecord.x9': { + value: 0 + }, + tmName: { + description: '/Common/clientssl' + }, + total: { + value: 0 + }, + typeId: { + description: 'ltm profile client-ssl' + }, + vsName: { + description: 'N/A' + } + } + } + } + } + } + } + ] + } + ] + }, + /** + * TEST SET DATA STARTS HERE + * */ + collectServerSslProfiles: { + name: 'server ssl profiles stats', + tests: [ + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should set server ssl profiles to empty object if not configured', + statsToCollect: ['serverSslProfiles'], + contextToCollect: [], + expectedData: { + serverSslProfiles: {} + }, + endpoints: [ + { + endpoint: /\/mgmt\/tm\/ltm\/profile\/server-ssl\/stats/, + response: { + kind: 'tm:ltm:profile:server-ssl:server-sslcollectionstats', + selfLink: 'https://localhost/mgmt/tm/ltm/server-ssl/stats?ver=14.1.0' + } + } + ] + }, + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should collect server ssl profiles stats', + statsToCollect: ['serverSslProfiles'], + contextToCollect: [], + expectedData: { + serverSslProfiles: { + '/Common/apm-default-serverssl': { + activeHandshakeRejected: 0, + 'cipherUses.chacha20Poly1305Bulk': 0, + currentCompatibleConnections: 0, + currentConnections: 0, + currentNativeConnections: 0, + currentActiveHandshakes: 0, + decryptedBytesIn: 0, + decryptedBytesOut: 0, + encryptedBytesIn: 0, + encryptedBytesOut: 0, + fatalAlerts: 0, + handshakeFailures: 0, + peercertInvalid: 0, + peercertNone: 0, + peercertValid: 0, + 'protocolUses.dtlsv1': 0, + 'protocolUses.sslv2': 0, + 'protocolUses.sslv3': 0, + 'protocolUses.tlsv1': 0, + 'protocolUses.tlsv1_1': 0, + 'protocolUses.tlsv1_2': 0, + 'protocolUses.tlsv1_3': 0, + recordsIn: 0, + recordsOut: 0, + tenant: 'Common', + name: '/Common/apm-default-serverssl' + } + } + }, + endpoints: [ + { + endpoint: /\/mgmt\/tm\/ltm\/profile\/server-ssl\/stats/, + response: { + kind: 'tm:ltm:profile:server-ssl:server-sslcollectionstats', + selfLink: 'https://localhost/mgmt/tm/ltm/profile/server-ssl/stats?ver=14.1.0', + entries: { + 'https://localhost/mgmt/tm/ltm/profile/server-ssl/~Common~apm-default-serverssl/stats': { + nestedStats: { + kind: 'tm:ltm:profile:server-ssl:server-sslstats', + selfLink: 'https://localhost/mgmt/tm/ltm/profile/server-ssl/~Common~apm-default-serverssl/stats?ver=14.1.0', + entries: { + 'common.activeHandshakeRejected': { + value: 0 + }, + 'common.badRecords': { + value: 0 + }, + 'common.c3dUses.conns': { + value: 0 + }, + 'common.cipherUses.adhKeyxchg': { + value: 0 + }, + 'common.cipherUses.aesBulk': { + value: 0 + }, + 'common.cipherUses.aesGcmBulk': { + value: 0 + }, + 'common.cipherUses.camelliaBulk': { + value: 0 + }, + 'common.cipherUses.chacha20Poly1305Bulk': { + value: 0 + }, + 'common.cipherUses.desBulk': { + value: 0 + }, + 'common.cipherUses.dhRsaKeyxchg': { + value: 0 + }, + 'common.cipherUses.dheDssKeyxchg': { + value: 0 + }, + 'common.cipherUses.ecdhEcdsaKeyxchg': { + value: 0 + }, + 'common.cipherUses.ecdhRsaKeyxchg': { + value: 0 + }, + 'common.cipherUses.ecdheEcdsaKeyxchg': { + value: 0 + }, + 'common.cipherUses.ecdheRsaKeyxchg': { + value: 0 + }, + 'common.cipherUses.edhRsaKeyxchg': { + value: 0 + }, + 'common.cipherUses.ideaBulk': { + value: 0 + }, + 'common.cipherUses.md5Digest': { + value: 0 + }, + 'common.cipherUses.nullBulk': { + value: 0 + }, + 'common.cipherUses.nullDigest': { + value: 0 + }, + 'common.cipherUses.rc2Bulk': { + value: 0 + }, + 'common.cipherUses.rc4Bulk': { + value: 0 + }, + 'common.cipherUses.rsaKeyxchg': { + value: 0 + }, + 'common.cipherUses.shaDigest': { + value: 0 + }, + 'common.connectionMirroring.haCtxRecv': { + value: 0 + }, + 'common.connectionMirroring.haCtxSent': { + value: 0 + }, + 'common.connectionMirroring.haFailure': { + value: 0 + }, + 'common.connectionMirroring.haHsSuccess': { + value: 0 + }, + 'common.connectionMirroring.haPeerReady': { + value: 0 + }, + 'common.connectionMirroring.haTimeout': { + value: 0 + }, + 'common.curCompatConns': { + value: 0 + }, + 'common.curConns': { + value: 0 + }, + 'common.curNativeConns': { + value: 0 + }, + 'common.currentActiveHandshakes': { + value: 0 + }, + 'common.decryptedBytesIn': { + value: 0 + }, + 'common.decryptedBytesOut': { + value: 0 + }, + 'common.dtlsTxPushbacks': { + value: 0 + }, + 'common.encryptedBytesIn': { + value: 0 + }, + 'common.encryptedBytesOut': { + value: 0 + }, + 'common.extendedMasterSecrets': { + value: 0 + }, + 'common.fatalAlerts': { + value: 0 + }, + 'common.fullyHwAcceleratedConns': { + value: 0 + }, + 'common.fwdpUses.conns': { + value: 0 + }, + 'common.fwdpUses.enforceResumeFailures': { + value: 0 + }, + 'common.fwdpUses.transparentResumeCnt': { + value: 0 + }, + 'common.handshakeFailures': { + value: 0 + }, + 'common.insecureHandshakeAccepts': { + value: 0 + }, + 'common.insecureHandshakeRejects': { + value: 0 + }, + 'common.insecureRenegotiationRejects': { + value: 0 + }, + 'common.maxCompatConns': { + value: 0 + }, + 'common.maxConns': { + value: 0 + }, + 'common.maxNativeConns': { + value: 0 + }, + 'common.midstreamRenegotiations': { + value: 0 + }, + 'common.nonHwAcceleratedConns': { + value: 0 + }, + 'common.ocspServerssl.cachedResp': { + value: 0 + }, + 'common.ocspServerssl.certStatusRevoked': { + value: 0 + }, + 'common.ocspServerssl.certStatusUnknown': { + value: 0 + }, + 'common.ocspServerssl.responderQueries': { + value: 0 + }, + 'common.ocspServerssl.responseErrors': { + value: 0 + }, + 'common.ocspServerssl.stapledResp': { + value: 0 + }, + 'common.partiallyHwAcceleratedConns': { + value: 0 + }, + 'common.peercertInvalid': { + value: 0 + }, + 'common.peercertNone': { + value: 0 + }, + 'common.peercertValid': { + value: 0 + }, + 'common.prematureDisconnects': { + value: 0 + }, + 'common.protocolUses.dtlsv1': { + value: 0 + }, + 'common.protocolUses.sslv2': { + value: 0 + }, + 'common.protocolUses.sslv3': { + value: 0 + }, + 'common.protocolUses.tlsv1': { + value: 0 + }, + 'common.protocolUses.tlsv1_1': { + value: 0 + }, + 'common.protocolUses.tlsv1_2': { + value: 0 + }, + 'common.protocolUses.tlsv1_3': { + value: 0 + }, + 'common.recordsIn': { + value: 0 + }, + 'common.recordsOut': { + value: 0 + }, + 'common.secureHandshakes': { + value: 0 + }, + 'common.sessCacheCurEntries': { + value: 0 + }, + 'common.sessCacheHits': { + value: 0 + }, + 'common.sessCacheInvalidations': { + value: 0 + }, + 'common.sessCacheLookups': { + value: 0 + }, + 'common.sessCacheOverflows': { + value: 0 + }, + 'common.sessionMirroring.failure': { + value: 0 + }, + 'common.sessionMirroring.success': { + value: 0 + }, + 'common.sesstickUses.reuseFailed': { + value: 0 + }, + 'common.sesstickUses.reused': { + value: 0 + }, + 'common.totCompatConns': { + value: 0 + }, + 'common.totNativeConns': { + value: 0 + }, + tmName: { + description: '/Common/apm-default-serverssl' + }, + typeId: { + description: 'ltm profile server-ssl' + }, + vsName: { + description: 'N/A' + } + } + } + } + } + } + } + ] + } + ] + }, + /** + * TEST SET DATA STARTS HERE + * */ + collectSslCerts: { + name: 'ssl certs stats', + tests: [ + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should set ssl certs to empty object if not configured (with items property)', + statsToCollect: ['sslCerts'], + contextToCollect: [], + expectedData: { + sslCerts: {} + }, + endpoints: [ + { + endpoint: /\/mgmt\/tm\/sys\/file\/ssl-cert/, + response: { + kind: 'tm:sys:file:ssl-cert:ssl-certcollectionstate', + selfLink: 'https://localhost/mgmt/tm/sys/file/ssl-cert?ver=14.1.0', + items: [] + } + } + ] + }, + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should set ssl certs to empty object if not configured (without items property)', + statsToCollect: ['sslCerts'], + contextToCollect: [], + expectedData: { + sslCerts: {} + }, + endpoints: [ + { + endpoint: /\/mgmt\/tm\/sys\/file\/ssl-cert/, + response: { + kind: 'tm:sys:file:ssl-cert:ssl-certcollectionstate', + selfLink: 'https://localhost/mgmt/tm/sys/file/ssl-cert?ver=14.1.0' + } + } + ] + }, + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should collect ssl certs stats', + statsToCollect: ['sslCerts'], + contextToCollect: [], + expectedData: { + sslCerts: { + 'ca-bundle.crt': { + expirationDate: 1893455999, + expirationString: '2029-12-31T23:59:59.000Z', + issuer: 'CN=Starfield Services Root Certificate Authority,OU=http://certificates.starfieldtech.com/repository/,O=Starfield Technologies, Inc.,L=Scottsdale,ST=Arizona,C=US', + subject: 'CN=Starfield Services Root Certificate Authority,OU=http://certificates.starfieldtech.com/repository/,O=Starfield Technologies, Inc.,L=Scottsdale,ST=Arizona,C=US', + name: 'ca-bundle.crt' + } + } + }, + endpoints: [ + { + endpoint: /\/mgmt\/tm\/sys\/file\/ssl-cert/, + response: { + kind: 'tm:sys:file:ssl-cert:ssl-certcollectionstate', + selfLink: 'https://localhost/mgmt/tm/sys/file/ssl-cert?ver=14.1.0', + items: [ + { + kind: 'tm:sys:file:ssl-cert:ssl-certstate', + name: 'ca-bundle.crt', + partition: 'Common', + fullPath: '/Common/ca-bundle.crt', + generation: 1, + selfLink: 'https://localhost/mgmt/tm/sys/file/ssl-cert/~Common~ca-bundle.crt?ver=14.1.0', + certificateKeyCurveName: 'none', + certificateKeySize: 2048, + checksum: 'SHA1:5368116:a98ae01563290479a33b307ae763ff32d157ac9f', + createTime: '2020-01-23T01:10:20Z', + createdBy: 'root', + expirationDate: 1893455999, + expirationString: 'Dec 31 23:59:59 2029 GMT', + fingerprint: 'SHA256/B5:BD:2C:B7:9C:BD:19:07:29:8D:6B:DF:48:42:E5:16:D8:C7:8F:A6:FC:96:D2:5F:71:AF:81:4E:16:CC:24:5E', + isBundle: 'true', + issuer: 'CN=Starfield Services Root Certificate Authority,OU=http://certificates.starfieldtech.com/repository/,O=Starfield Technologies, Inc.,L=Scottsdale,ST=Arizona,C=US', + keyType: 'rsa-public', + lastUpdateTime: '2020-01-23T01:10:20Z', + mode: 33261, + revision: 1, + size: 5368116, + subject: 'CN=Starfield Services Root Certificate Authority,OU=http://certificates.starfieldtech.com/repository/,O=Starfield Technologies, Inc.,L=Scottsdale,ST=Arizona,C=US', + systemPath: '/config/ssl/ssl.crt/ca-bundle.crt', + updatedBy: 'root', + version: 3, + bundleCertificatesReference: { + link: 'https://localhost/mgmt/tm/sys/file/ssl-cert/~Common~ca-bundle.crt/bundle-certificates?ver=14.1.0', + isSubcollection: true + }, + certValidatorsReference: { + link: 'https://localhost/mgmt/tm/sys/file/ssl-cert/~Common~ca-bundle.crt/cert-validators?ver=14.1.0', + isSubcollection: true + } + } + ] + } + } + ] + } + ] + }, + /** + * TEST SET DATA STARTS HERE + * */ + collectNetworkTunnels: { + name: 'network tunnels stats', + tests: [ + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should set network tunnels to empty object if not configured', + statsToCollect: ['networkTunnels'], + contextToCollect: [], + expectedData: { + networkTunnels: {} + }, + endpoints: [ + { + endpoint: /\/mgmt\/tm\/net\/tunnels\/tunnel\/stats/, + response: { + kind: 'tm:net:tunnels:tunnel:tunnelcollectionstats', + selfLink: 'https://localhost/mgmt/tm/net/tunnels/tunnel/stats?ver=14.1.0' + } + } + ] + }, + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should collect network tunnels stats', + statsToCollect: ['networkTunnels'], + contextToCollect: [], + expectedData: { + networkTunnels: { + '/Common/http-tunnel': { + hcInBroadcastPkts: 0, + hcInMulticastPkts: 0, + hcInOctets: 0, + hcInUcastPkts: 0, + hcOutBroadcastPkts: 0, + hcOutMulticastPkts: 0, + hcOutOctets: 0, + hcOutUcastPkts: 0, + inDiscards: 0, + inErrors: 0, + inUnknownProtos: 0, + outDiscards: 0, + outErrors: 0, + tenant: 'Common', + name: '/Common/http-tunnel' + } + } + }, + endpoints: [ + { + endpoint: /\/mgmt\/tm\/net\/tunnels\/tunnel\/stats/, + response: { + kind: 'tm:net:tunnels:tunnel:tunnelcollectionstats', + selfLink: 'https://localhost/mgmt/tm/net/tunnels/tunnel/stats?ver=14.1.0', + entries: { + 'https://localhost/mgmt/tm/net/tunnels/tunnel/~Common~http-tunnel/stats': { + nestedStats: { + kind: 'tm:net:tunnels:tunnel:tunnelstats', + selfLink: 'https://localhost/mgmt/tm/net/tunnels/tunnel/~Common~http-tunnel/stats?ver=14.1.0', + entries: { + hcInBroadcastPkts: { + value: 0 + }, + hcInMulticastPkts: { + value: 0 + }, + hcInOctets: { + value: 0 + }, + hcInUcastPkts: { + value: 0 + }, + hcOutBroadcastPkts: { + value: 0 + }, + hcOutMulticastPkts: { + value: 0 + }, + hcOutOctets: { + value: 0 + }, + hcOutUcastPkts: { + value: 0 + }, + inDiscards: { + value: 0 + }, + inErrors: { + value: 0 + }, + inUnknownProtos: { + value: 0 + }, + tmName: { + description: '/Common/http-tunnel' + }, + outDiscards: { + value: 0 + }, + outErrors: { + value: 0 + }, + typeId: { + description: 'net tunnels tunnel' + } + } + } + } + } + } + } + ] + } + ] + }, + /** + * TEST SET DATA STARTS HERE + * */ + collectDeviceGroups: { + name: 'device groups stats', + tests: [ + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should set device groups to empty object if not configured (with items property)', + statsToCollect: ['deviceGroups'], + contextToCollect: [], + expectedData: { + deviceGroups: {} + }, + endpoints: [ + { + endpoint: /\/mgmt\/tm\/cm\/device-group/, + response: { + kind: 'tm:cm:device-group:device-groupcollectionstate', + selfLink: 'https://localhost/mgmt/tm/cm/device-group?ver=14.1.0', + items: [] + } + } + ] + }, + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should set device groups to empty object if not configured (without items property)', + statsToCollect: ['deviceGroups'], + contextToCollect: [], + expectedData: { + deviceGroups: {} + }, + endpoints: [ + { + endpoint: /\/mgmt\/tm\/cm\/device-group/, + response: { + kind: 'tm:cm:device-group:device-groupcollectionstate', + selfLink: 'https://localhost/mgmt/tm/cm/device-group?ver=14.1.0' + } + } + ] + }, + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should collect device groups stats', + statsToCollect: ['deviceGroups'], + contextToCollect: [], + expectedData: { + deviceGroups: { + '/Common/datasync-device-ts-big-inst.localhost.localdomain-dg': { + tenant: 'Common', + commitIdTime: '2020-01-30T07:48:42.000Z', + lssTime: '2020-01-30T07:48:42.000Z', + timeSinceLastSync: '-', + name: '/Common/datasync-device-ts-big-inst.localhost.localdomain-dg', + type: 'sync-only' + } + } + }, + endpoints: [ + { + endpoint: /\/mgmt\/tm\/cm\/device-group/, + response: { + kind: 'tm:cm:device-group:device-groupcollectionstate', + selfLink: 'https://localhost/mgmt/tm/cm/device-group?ver=14.1.0', + items: [ + { + kind: 'tm:cm:device-group:device-groupstate', + name: 'datasync-device-ts-big-inst.localhost.localdomain-dg', + partition: 'Common', + fullPath: '/Common/datasync-device-ts-big-inst.localhost.localdomain-dg', + generation: 1, + selfLink: 'https://localhost/mgmt/tm/cm/device-group/~Common~datasync-device-ts-big-inst.localhost.localdomain-dg?ver=14.1.0', + asmSync: 'disabled', + autoSync: 'enabled', + fullLoadOnSync: 'true', + incrementalConfigSyncSizeMax: 1024, + networkFailover: 'disabled', + saveOnAutoSync: 'false', + type: 'sync-only', + devicesReference: { + link: 'https://localhost/mgmt/tm/cm/device-group/~Common~datasync-device-ts-big-inst.localhost.localdomain-dg/devices?ver=14.1.0', + isSubcollection: true + } + } + ] + } + }, + { + endpoint: '/mgmt/tm/cm/device-group/~Common~datasync-device-ts-big-inst.localhost.localdomain-dg/stats', + response: { + kind: 'tm:cm:device-group:device-groupstats', + selfLink: 'https://localhost/mgmt/tm/cm/device-group/~Common~datasync-device-ts-big-inst.localhost.localdomain-dg/stats?ver=14.1.0', + entries: { + 'https://localhost/mgmt/tm/cm/device-group/~Common~datasync-device-ts-big-inst.localhost.localdomain-dg/~Common~datasync-device-ts-big-inst.localhost.localdomain-dg:~Common~ts-big-inst.localhost.localdomain/stats': { + nestedStats: { + kind: 'tm:cm:device-group:device-groupstats', + selfLink: 'https://localhost/mgmt/tm/cm/device-group/~Common~datasync-device-ts-big-inst.localhost.localdomain-dg/~Common~datasync-device-ts-big-inst.localhost.localdomain-dg:~Common~ts-big-inst.localhost.localdomain/stats?ver=14.1.0', + entries: { + commitIdOriginator: { + description: '/Common/ts-big-inst.localhost.localdomain' + }, + commitIdTime: { + description: '2020-01-30T07:48:42.000Z' + }, + device: { + description: '/Common/ts-big-inst.localhost.localdomain' + }, + devicegroup: { + description: '/Common/datasync-device-ts-big-inst.localhost.localdomain-dg' + }, + lastSyncType: { + description: 'none' + }, + lssOriginator: { + description: '/Common/ts-big-inst.localhost.localdomain' + }, + lssTime: { + description: '2020-01-30T07:48:42.000Z' + }, + timeSinceLastSync: { + description: '-' + } + } + } + } + } + } + } + ] + } + ] + }, + /** + * TEST SET DATA STARTS HERE + * */ + collectIRules: { + name: 'irules stats', + tests: [ + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should set irules stats to empty object if not configured', + statsToCollect: ['iRules'], + contextToCollect: [], + expectedData: { + iRules: {} + }, + endpoints: [ + { + endpoint: /\/mgmt\/tm\/ltm\/rule\/stats/, + response: { + kind: 'tm:ltm:rule:rulecollectionstats', + selfLink: 'https://localhost/mgmt/tm/ltm/rule/stats?ver=14.1.0' + } + } + ] + }, + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should collect irules stats', + statsToCollect: ['iRules'], + contextToCollect: [], + expectedData: { + iRules: { + '/Common/_sys_APM_ExchangeSupport_OA_BasicAuth': { + events: { + ACCESS_ACL_ALLOWED: { + aborts: 0, + avgCycles: 0, + failures: 0, + maxCycles: 0, + minCycles: 0, + priority: 500, + totalExecutions: 0 + } + }, + tenant: 'Common', + name: '/Common/_sys_APM_ExchangeSupport_OA_BasicAuth' + } + } + }, + endpoints: [ + { + endpoint: /\/mgmt\/tm\/ltm\/rule\/stats/, + response: { + kind: 'tm:ltm:rule:rulecollectionstats', + selfLink: 'https://localhost/mgmt/tm/ltm/rule/stats?ver=14.1.0', + entries: { + 'https://localhost/mgmt/tm/ltm/rule/~Common~_sys_APM_ExchangeSupport_OA_BasicAuth:ACCESS_ACL_ALLOWED/stats': { + nestedStats: { + kind: 'tm:ltm:rule:rulestats', + selfLink: 'https://localhost/mgmt/tm/ltm/rule/~Common~_sys_APM_ExchangeSupport_OA_BasicAuth:ACCESS_ACL_ALLOWED/stats?ver=14.1.0', + entries: { + aborts: { + value: 0 + }, + avgCycles: { + value: 0 + }, + eventType: { + description: 'ACCESS_ACL_ALLOWED' + }, + failures: { + value: 0 + }, + maxCycles: { + value: 0 + }, + minCycles: { + value: 0 + }, + tmName: { + description: '/Common/_sys_APM_ExchangeSupport_OA_BasicAuth' + }, + priority: { + value: 500 + }, + totalExecutions: { + value: 0 + } + } + } + } + } + } + } + ] + } + ] + }, + /** + * TEST SET DATA STARTS HERE + * */ + collectTmstats: { + name: 'tmstats stats', + tests: [ + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should set tmstats to empty folder', + statsToCollect: ['tmstats'], + contextToCollect: [], + expectedData: { + tmstats: {} + } + }, + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should collect tmstats', + statsToCollect: (stats) => { + const ret = { + tmstats: stats.tmstats + }; + Object.keys(stats).forEach((statKey) => { + const stat = stats[statKey]; + if (stat.structure && stat.structure.parentKey === 'tmstats') { + ret[statKey] = stat; + } + }); + return ret; + }, + contextToCollect: [], + expectedData: { + tmstats: { + asmCpuUtilStats: [ + { + a: '1', + b: '2', + c: 'spam', + vs_name: '/Tenant/app/test', + name: '/Tenant/app/test', + tenant: 'Tenant', + application: 'app' + }, + { + a: '3', + b: '4', + c: 'eggs', + vs_name: '/Tenant/test', + name: '/Tenant/test', + tenant: 'Tenant' + } + ], + cpuInfoStat: [ + { + a: '1', + b: '2', + c: 'spam', + someKey: '/Tenant/app/test' + }, + { + a: '3', + b: '4', + c: 'eggs', + someKey: '/Tenant/test' + } + ], + diskInfoStat: [ + { + a: '1', + b: '2', + c: 'spam', + someKey: '/Tenant/app/test' + }, + { + a: '3', + b: '4', + c: 'eggs', + someKey: '/Tenant/test' + } + ], + dnsCacheResolverStat: [ + { + a: '1', + b: '2', + c: 'spam', + someKey: '/Tenant/app/test' + }, + { + a: '3', + b: '4', + c: 'eggs', + someKey: '/Tenant/test' + } + ], + dnsexpressZoneStat: [ + { + a: '1', + b: '2', + c: 'spam', + someKey: '/Tenant/app/test' + }, + { + a: '3', + b: '4', + c: 'eggs', + someKey: '/Tenant/test' + } + ], + dosStat: [ + { + a: '1', + b: '2', + c: 'spam', + vs_name: '/Tenant/app/test', + name: '/Tenant/app/test', + tenant: 'Tenant', + application: 'app' + }, + { + a: '3', + b: '4', + c: 'eggs', + vs_name: '/Tenant/test', + name: '/Tenant/test', + tenant: 'Tenant' + } + ], + dosl7PluginStats: [ + { + a: '1', + b: '2', + c: 'spam', + someKey: '/Tenant/app/test' + }, + { + a: '3', + b: '4', + c: 'eggs', + someKey: '/Tenant/test' + } + ], + dosl7dStat: [ + { + a: '1', + b: '2', + c: 'spam', + someKey: '/Tenant/app/test' + }, + { + a: '3', + b: '4', + c: 'eggs', + someKey: '/Tenant/test' + } + ], + flowEvictionPolicyStat: [ + { + a: '1', + b: '2', + c: 'spam', + context_name: '/Tenant/app/test', + name: '/Tenant/app/test', + tenant: 'Tenant', + application: 'app' + }, + { + a: '3', + b: '4', + c: 'eggs', + context_name: '/Tenant/test', + name: '/Tenant/test', + tenant: 'Tenant' + } + ], + gtmDcStat: [ + { + a: '1', + b: '2', + c: 'spam', + someKey: '/Tenant/app/test' + }, + { + a: '3', + b: '4', + c: 'eggs', + someKey: '/Tenant/test' + } + ], + gtmWideipStat: [ + { + a: '1', + b: '2', + c: 'spam', + someKey: '/Tenant/app/test' + }, + { + a: '3', + b: '4', + c: 'eggs', + someKey: '/Tenant/test' + } + ], + hostInfoStat: [ + { + a: '1', + b: '2', + c: 'spam', + someKey: '/Tenant/app/test' + }, + { + a: '3', + b: '4', + c: 'eggs', + someKey: '/Tenant/test' + } + ], + ifcStats: [ + { + a: '1', + b: '2', + c: 'spam', + someKey: '/Tenant/app/test' + }, + { + a: '3', + b: '4', + c: 'eggs', + someKey: '/Tenant/test' + } + ], + interfaceStat: [ + { + a: '1', + b: '2', + c: 'spam', + someKey: '/Tenant/app/test' + }, + { + a: '3', + b: '4', + c: 'eggs', + someKey: '/Tenant/test' + } + ], + ipIntelligenceStat: [ + { + a: '1', + b: '2', + c: 'spam', + someKey: '/Tenant/app/test' + }, + { + a: '3', + b: '4', + c: 'eggs', + someKey: '/Tenant/test' + } + ], + ipStat: [ + { + a: '1', + b: '2', + c: 'spam', + someKey: '/Tenant/app/test' + }, + { + a: '3', + b: '4', + c: 'eggs', + someKey: '/Tenant/test' + } + ], + iprepdStat: [ + { + a: '1', + b: '2', + c: 'spam', + someKey: '/Tenant/app/test' + }, + { + a: '3', + b: '4', + c: 'eggs', + someKey: '/Tenant/test' + } + ], + kvmVcpuStat: [ + { + a: '1', + b: '2', + c: 'spam', + someKey: '/Tenant/app/test' + }, + { + a: '3', + b: '4', + c: 'eggs', + someKey: '/Tenant/test' + } + ], + kvmVmStat: [ + { + a: '1', + b: '2', + c: 'spam', + someKey: '/Tenant/app/test' + }, + { + a: '3', + b: '4', + c: 'eggs', + someKey: '/Tenant/test' + } + ], + mcpRequestStat: [ + { + a: '1', + b: '2', + c: 'spam', + someKey: '/Tenant/app/test' + }, + { + a: '3', + b: '4', + c: 'eggs', + someKey: '/Tenant/test' + } + ], + mcpTransactionStat: [ + { + a: '1', + b: '2', + c: 'spam', + someKey: '/Tenant/app/test' + }, + { + a: '3', + b: '4', + c: 'eggs', + someKey: '/Tenant/test' + } + ], + memoryUsageStat: [ + { + a: '1', + b: '2', + c: 'spam', + someKey: '/Tenant/app/test' + }, + { + a: '3', + b: '4', + c: 'eggs', + someKey: '/Tenant/test' + } + ], + monitorInstanceStat: [ + { + a: '1', + b: '2', + c: 'spam', + someKey: '/Tenant/app/test' + }, + { + a: '3', + b: '4', + c: 'eggs', + someKey: '/Tenant/test' + } + ], + monitorStat: [ + { + a: '1', + b: '2', + c: 'spam', + someKey: '/Tenant/app/test' + }, + { + a: '3', + b: '4', + c: 'eggs', + someKey: '/Tenant/test' + } + ], + poolMemberStat: [ + { + a: '1', + b: '2', + c: 'spam', + pool_name: '/Tenant/app/test', + name: '/Tenant/app/test', + tenant: 'Tenant', + application: 'app' + }, + { + a: '3', + b: '4', + c: 'eggs', + pool_name: '/Tenant/test', + name: '/Tenant/test', + tenant: 'Tenant' + } + ], + poolStat: [ + { + a: '1', + b: '2', + c: 'spam', + someKey: '/Tenant/app/test' + }, + { + a: '3', + b: '4', + c: 'eggs', + someKey: '/Tenant/test' + } + ], + procPidStat: [ + { + a: '1', + b: '2', + c: 'spam', + someKey: '/Tenant/app/test' + }, + { + a: '3', + b: '4', + c: 'eggs', + someKey: '/Tenant/test' + } + ], + profileBigprotoStat: [ + { + a: '1', + b: '2', + c: 'spam', + vs_name: '/Tenant/app/test', + name: '/Tenant/app/test', + tenant: 'Tenant', + application: 'app' + }, + { + a: '3', + b: '4', + c: 'eggs', + vs_name: '/Tenant/test', + name: '/Tenant/test', + tenant: 'Tenant' + } + ], + profileClientsslStat: [ + { + a: '1', + b: '2', + c: 'spam', + vs_name: '/Tenant/app/test', + name: '/Tenant/app/test', + tenant: 'Tenant', + application: 'app' + }, + { + a: '3', + b: '4', + c: 'eggs', + vs_name: '/Tenant/test', + name: '/Tenant/test', + tenant: 'Tenant' + } + ], + profileConnpoolStat: [ + { + a: '1', + b: '2', + c: 'spam', + vs_name: '/Tenant/app/test', + name: '/Tenant/app/test', + tenant: 'Tenant', + application: 'app' + }, + { + a: '3', + b: '4', + c: 'eggs', + vs_name: '/Tenant/test', + name: '/Tenant/test', + tenant: 'Tenant' + } + ], + profileDnsStat: [ + { + a: '1', + b: '2', + c: 'spam', + vs_name: '/Tenant/app/test', + name: '/Tenant/app/test', + tenant: 'Tenant', + application: 'app' + }, + { + a: '3', + b: '4', + c: 'eggs', + vs_name: '/Tenant/test', + name: '/Tenant/test', + tenant: 'Tenant' + } + ], + profileFtpStat: [ + { + a: '1', + b: '2', + c: 'spam', + vs_name: '/Tenant/app/test', + name: '/Tenant/app/test', + tenant: 'Tenant', + application: 'app' + }, + { + a: '3', + b: '4', + c: 'eggs', + vs_name: '/Tenant/test', + name: '/Tenant/test', + tenant: 'Tenant' + } + ], + profileHttpStat: [ + { + a: '1', + b: '2', + c: 'spam', + vs_name: '/Tenant/app/test', + name: '/Tenant/app/test', + tenant: 'Tenant', + application: 'app' + }, + { + a: '3', + b: '4', + c: 'eggs', + vs_name: '/Tenant/test', + name: '/Tenant/test', + tenant: 'Tenant' + } + ], + profileHttpcompressionStat: [ + { + a: '1', + b: '2', + c: 'spam', + vs_name: '/Tenant/app/test', + name: '/Tenant/app/test', + tenant: 'Tenant', + application: 'app' + }, + { + a: '3', + b: '4', + c: 'eggs', + vs_name: '/Tenant/test', + name: '/Tenant/test', + tenant: 'Tenant' + } + ], + profileServersslStat: [ + { + a: '1', + b: '2', + c: 'spam', + vs_name: '/Tenant/app/test', + name: '/Tenant/app/test', + tenant: 'Tenant', + application: 'app' + }, + { + a: '3', + b: '4', + c: 'eggs', + vs_name: '/Tenant/test', + name: '/Tenant/test', + tenant: 'Tenant' + } + ], + profileTcpStat: [ + { + a: '1', + b: '2', + c: 'spam', + vs_name: '/Tenant/app/test', + name: '/Tenant/app/test', + tenant: 'Tenant', + application: 'app' + }, + { + a: '3', + b: '4', + c: 'eggs', + vs_name: '/Tenant/test', + name: '/Tenant/test', + tenant: 'Tenant' + } + ], + profileUdpStat: [ + { + a: '1', + b: '2', + c: 'spam', + vs_name: '/Tenant/app/test', + name: '/Tenant/app/test', + tenant: 'Tenant', + application: 'app' + }, + { + a: '3', + b: '4', + c: 'eggs', + vs_name: '/Tenant/test', + name: '/Tenant/test', + tenant: 'Tenant' + } + ], + profileWebaccelerationStat: [ + { + a: '1', + b: '2', + c: 'spam', + vs_name: '/Tenant/app/test', + name: '/Tenant/app/test', + tenant: 'Tenant', + application: 'app' + }, + { + a: '3', + b: '4', + c: 'eggs', + vs_name: '/Tenant/test', + name: '/Tenant/test', + tenant: 'Tenant' + } + ], + ruleStat: [ + { + a: '1', + b: '2', + c: 'spam', + someKey: '/Tenant/app/test' + }, + { + a: '3', + b: '4', + c: 'eggs', + someKey: '/Tenant/test' + } + ], + tmmStat: [ + { + a: '1', + b: '2', + c: 'spam', + someKey: '/Tenant/app/test' + }, + { + a: '3', + b: '4', + c: 'eggs', + someKey: '/Tenant/test' + } + ], + tmmdnsServerStat: [ + { + a: '1', + b: '2', + c: 'spam', + someKey: '/Tenant/app/test' + }, + { + a: '3', + b: '4', + c: 'eggs', + someKey: '/Tenant/test' + } + ], + tmmdnsZoneStat: [ + { + a: '1', + b: '2', + c: 'spam', + someKey: '/Tenant/app/test' + }, + { + a: '3', + b: '4', + c: 'eggs', + someKey: '/Tenant/test' + } + ], + vcmpGlobalStat: [ + { + a: '1', + b: '2', + c: 'spam', + someKey: '/Tenant/app/test' + }, + { + a: '3', + b: '4', + c: 'eggs', + someKey: '/Tenant/test' + } + ], + vcmpStat: [ + { + a: '1', + b: '2', + c: 'spam', + someKey: '/Tenant/app/test' + }, + { + a: '3', + b: '4', + c: 'eggs', + someKey: '/Tenant/test' + } + ], + virtualServerConnStat: [ + { + a: '1', + b: '2', + c: 'spam', + someKey: '/Tenant/app/test' + }, + { + a: '3', + b: '4', + c: 'eggs', + someKey: '/Tenant/test' + } + ], + virtualServerCpuStat: [ + { + a: '1', + b: '2', + c: 'spam', + name: '/Tenant/app/test', + tenant: 'Tenant', + application: 'app' + }, + { + a: '3', + b: '4', + c: 'eggs', + name: '/Tenant/test', + tenant: 'Tenant' + } + ], + virtualServerStat: [ + { + a: '1', + b: '2', + c: 'spam', + name: '/Tenant/app/test', + tenant: 'Tenant', + application: 'app' + }, + { + a: '3', + b: '4', + c: 'eggs', + name: '/Tenant/test', + tenant: 'Tenant' + } + ] + } + }, + endpoints: [ + { + endpoint: '/mgmt/tm/util/bash', + method: 'post', + options: { + times: 47 + }, + request: body => body && body.utilCmdArgs && body.utilCmdArgs.indexOf('tmctl') !== -1, + response: (uri, requestBody) => { + // requestBody is string + let tmctlTable = requestBody.match(TMCTL_CMD_REGEXP); + if (!tmctlTable) { + throw new Error(`Unable to find tmctl table in request: ${JSON.stringify(requestBody)}`); + } + tmctlTable = tmctlTable[1]; + let tmctlStat; + + Object.keys(defaultProperties.stats).some((statKey) => { + const stat = defaultProperties.stats[statKey]; + if (stat.structure && stat.structure.parentKey === 'tmstats' + && stat.keyArgs.replaceStrings['\\$tmctlArgs'].indexOf(tmctlTable) !== -1) { + tmctlStat = stat; + return true; + } + return false; + }); + if (!tmctlStat) { + throw new Error(`Unable to find stat for ${tmctlTable}`); + } + const mapKey = tmctlStat.normalization[0].runFunctions[0].args.mapKey; + return { + kind: 'tm:util:bash:runstate', + commandResult: [ + ['a', 'b', 'c', mapKey || 'someKey'], + [1, 2, 'spam', '/Tenant/app/test'], + [3, 4, 'eggs', '/Tenant/test'] + ].join('\n') + }; + } + } + ] + }, + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should not fail when command retruns headers only', + statsToCollect: (stats) => { + const ret = { + tmstats: stats.tmstats + }; + Object.keys(stats).forEach((statKey) => { + const stat = stats[statKey]; + if (stat.structure && stat.structure.parentKey === 'tmstats') { + ret[statKey] = stat; + } + }); + return ret; + }, + contextToCollect: [], + expectedData: { + tmstats: { + asmCpuUtilStats: undefined, + cpuInfoStat: undefined, + diskInfoStat: undefined, + dnsCacheResolverStat: undefined, + dnsexpressZoneStat: undefined, + dosStat: undefined, + dosl7PluginStats: undefined, + dosl7dStat: undefined, + flowEvictionPolicyStat: undefined, + gtmDcStat: undefined, + gtmWideipStat: undefined, + hostInfoStat: undefined, + ifcStats: undefined, + interfaceStat: undefined, + ipIntelligenceStat: undefined, + ipStat: undefined, + iprepdStat: undefined, + kvmVcpuStat: undefined, + kvmVmStat: undefined, + mcpRequestStat: undefined, + mcpTransactionStat: undefined, + memoryUsageStat: undefined, + monitorInstanceStat: undefined, + monitorStat: undefined, + poolMemberStat: undefined, + poolStat: undefined, + procPidStat: undefined, + profileBigprotoStat: undefined, + profileClientsslStat: undefined, + profileConnpoolStat: undefined, + profileDnsStat: undefined, + profileFtpStat: undefined, + profileHttpStat: undefined, + profileHttpcompressionStat: undefined, + profileServersslStat: undefined, + profileTcpStat: undefined, + profileUdpStat: undefined, + profileWebaccelerationStat: undefined, + ruleStat: undefined, + tmmStat: undefined, + tmmdnsServerStat: undefined, + tmmdnsZoneStat: undefined, + vcmpGlobalStat: undefined, + vcmpStat: undefined, + virtualServerConnStat: undefined, + virtualServerCpuStat: undefined, + virtualServerStat: undefined + } + }, + endpoints: [ + { + endpoint: '/mgmt/tm/util/bash', + method: 'post', + options: { + times: 47 + }, + request: body => body && body.utilCmdArgs && body.utilCmdArgs.indexOf('tmctl') !== -1, + response: (uri, requestBody) => { + // requestBody is string + let tmctlTable = requestBody.match(TMCTL_CMD_REGEXP); + if (!tmctlTable) { + throw new Error(`Unable to find tmctl table in request: ${JSON.stringify(requestBody)}`); + } + tmctlTable = tmctlTable[1]; + let tmctlStat; + + Object.keys(defaultProperties.stats).some((statKey) => { + const stat = defaultProperties.stats[statKey]; + if (stat.structure && stat.structure.parentKey === 'tmstats' + && stat.keyArgs.replaceStrings['\\$tmctlArgs'].indexOf(tmctlTable) !== -1) { + tmctlStat = stat; + return true; + } + return false; + }); + if (!tmctlStat) { + throw new Error(`Unable to find stat for ${tmctlTable}`); + } + const mapKey = tmctlStat.normalization[0].runFunctions[0].args.mapKey; + return { + kind: 'tm:util:bash:runstate', + commandResult: [ + ['a', 'b', 'c', mapKey || 'someKey'] + ].join('\n') + }; + } + } + ] + }, + /** + * TEST DATA STARTS HERE + * */ + { + name: 'should not fail when table doesn\'t exist', + statsToCollect: (stats) => { + const ret = { + tmstats: stats.tmstats + }; + Object.keys(stats).forEach((statKey) => { + const stat = stats[statKey]; + if (stat.structure && stat.structure.parentKey === 'tmstats') { + ret[statKey] = stat; + } + }); + return ret; + }, + contextToCollect: [], + expectedData: { + tmstats: { + asmCpuUtilStats: undefined, + cpuInfoStat: undefined, + diskInfoStat: undefined, + dnsCacheResolverStat: undefined, + dnsexpressZoneStat: undefined, + dosStat: undefined, + dosl7PluginStats: undefined, + dosl7dStat: undefined, + flowEvictionPolicyStat: undefined, + gtmDcStat: undefined, + gtmWideipStat: undefined, + hostInfoStat: undefined, + ifcStats: undefined, + interfaceStat: undefined, + ipIntelligenceStat: undefined, + ipStat: undefined, + iprepdStat: undefined, + kvmVcpuStat: undefined, + kvmVmStat: undefined, + mcpRequestStat: undefined, + mcpTransactionStat: undefined, + memoryUsageStat: undefined, + monitorInstanceStat: undefined, + monitorStat: undefined, + poolMemberStat: undefined, + poolStat: undefined, + procPidStat: undefined, + profileBigprotoStat: undefined, + profileClientsslStat: undefined, + profileConnpoolStat: undefined, + profileDnsStat: undefined, + profileFtpStat: undefined, + profileHttpStat: undefined, + profileHttpcompressionStat: undefined, + profileServersslStat: undefined, + profileTcpStat: undefined, + profileUdpStat: undefined, + profileWebaccelerationStat: undefined, + ruleStat: undefined, + tmmStat: undefined, + tmmdnsServerStat: undefined, + tmmdnsZoneStat: undefined, + vcmpGlobalStat: undefined, + vcmpStat: undefined, + virtualServerConnStat: undefined, + virtualServerCpuStat: undefined, + virtualServerStat: undefined + } + }, + endpoints: [ + { + endpoint: '/mgmt/tm/util/bash', + method: 'post', + options: { + times: 47 + }, + request: body => body && body.utilCmdArgs && body.utilCmdArgs.indexOf('tmctl') !== -1, + response: { + kind: 'tm:util:bash:runstate', + commandResult: 'tmctl: qwerty: No such table' + } + } + ] + } + ] + } +}; diff --git a/test/unit/restWorkerTests.js b/test/unit/restWorkerTests.js new file mode 100644 index 00000000..775c1780 --- /dev/null +++ b/test/unit/restWorkerTests.js @@ -0,0 +1,293 @@ +/* + * Copyright 2020. F5 Networks, Inc. See End User License Agreement ("EULA") for + * license terms. Notwithstanding anything to the contrary in the EULA, Licensee + * may copy and modify this software product for its internal business purposes. + * Further, Licensee may upload, publish and distribute the modified version of + * the software product on devcentral.f5.com. + */ + +'use strict'; + +/* eslint-disable import/order */ + +require('./shared/restoreCache')(); +require('./shared/disableAjv'); + +const chai = require('chai'); +const chaiAsPromised = require('chai-as-promised'); +const sinon = require('sinon'); +const urllib = require('url'); + +const baseSchema = require('../../src/schema/latest/base_schema.json'); +const constants = require('../../src/lib/constants'); +const config = require('../../src/lib/config'); +const deviceUtil = require('../../src/lib/deviceUtil'); +const iHealthPoller = require('../../src/lib/ihealth'); +const RestWorker = require('../../src/nodejs/restWorker'); +const systemPoller = require('../../src/lib/systemPoller'); +const util = require('../../src/lib/util'); +const testUtil = require('./shared/util'); + +chai.use(chaiAsPromised); +const assert = chai.assert; + +describe('restWorker', () => { + let restWorker; + let loadConfigStub; + let gatherHostDeviceInfoStub; + + let parseURL; + if (process.versions.node.startsWith('4.')) { + parseURL = urllib.parse; + } else { + parseURL = url => new urllib.URL(url); + } + + const baseState = { + _data_: { + config: { + raw: {}, + parsed: {} + } + } + }; + + before(() => { + RestWorker.prototype.loadState = function (first, cb) { + cb(null, testUtil.deepCopy(baseState)); + }; + RestWorker.prototype.saveState = function (first, state, cb) { + cb(null); + }; + }); + + beforeEach(() => { + restWorker = new RestWorker(); + // remove all existing listeners as consumers, systemPoller and + // prev instances of RestWorker + config.removeAllListeners(); + loadConfigStub = sinon.stub(config, 'loadConfig'); + loadConfigStub.resolves(); + gatherHostDeviceInfoStub = sinon.stub(deviceUtil, 'gatherHostDeviceInfo'); + gatherHostDeviceInfoStub.resolves(); + }); + + afterEach(() => { + sinon.restore(); + }); + + describe('constructor', () => { + it('should set WORKER_URI_PATH to shared/telemetry', () => { + assert.strictEqual(restWorker.WORKER_URI_PATH, 'shared/telemetry'); + }); + }); + + describe('.onStart()', () => { + it('should call success callback', () => { + const fakeSuccess = sinon.fake(); + const fakeFailure = sinon.fake(); + restWorker.onStart(fakeSuccess, fakeFailure); + + assert.strictEqual(fakeSuccess.callCount, 1); + assert.strictEqual(fakeFailure.callCount, 0); + }); + }); + + describe('.onStartCompleted()', () => { + it('should call failure callback if unable to start application', () => { + sinon.stub(restWorker, '_initializeApplication').throws(new Error('test error')); + const fakeSuccess = sinon.fake(); + const fakeFailure = sinon.spy(); + + restWorker.onStartCompleted(fakeSuccess, fakeFailure); + assert.strictEqual(fakeSuccess.callCount, 0); + assert.strictEqual(fakeFailure.callCount, 1); + assert.ok(/onStartCompleted error/.test(fakeFailure.args[0][0])); + }); + + it('should call failure callback if unable to start application when promise chain failed', () => { + loadConfigStub.rejects(new Error('loadConfig error')); + return new Promise((resolve, reject) => { + restWorker.onStartCompleted( + () => reject(new Error('should not call success callback')), + () => resolve() + ); + }) + .then(() => { + assert.notStrictEqual(loadConfigStub.callCount, 0); + }); + }); + + it('should gather host device info', () => new Promise((resolve, reject) => { + restWorker.onStartCompleted(resolve, msg => reject(new Error(msg || 'no message provided'))); + }) + .then(() => new Promise((resolve, reject) => { + setTimeout(() => { + try { + // should be 1 because gatherHostDeviceInfo resolves on first attempt + assert.strictEqual(gatherHostDeviceInfoStub.callCount, 1); + resolve(); + } catch (err) { + reject(err); + } + }, 200); + }))); + + it('should not fail when unable to gather host device info', () => { + gatherHostDeviceInfoStub.rejects(new Error('expected error')); + return new Promise((resolve, reject) => { + restWorker.onStartCompleted(resolve, msg => reject(new Error(msg || 'no message provided'))); + }); + }); + }); + + describe('.configChangeHandler()', () => { + beforeEach(() => { + sinon.stub(systemPoller, 'processClientRequest').callsFake((restOperation) => { + restOperation.setBody('replyBody'); + }); + return new Promise((resolve, reject) => { + restWorker.onStartCompleted(resolve, msg => reject(new Error(msg || 'no message provided'))); + }); + }); + + it('should not allow to access debug endpoints by default', () => { + config.emit('change', {}); + const requestMock = new testUtil.MockRestOperation({ method: 'GET' }); + requestMock.uri = parseURL('http://localhost/shared/telemetry/systempoller'); + + restWorker.onGet(requestMock); + assert.ok(/Bad URL/.test(requestMock.getBody()), 'should be Bad URL message'); + }); + + it('should enable debug endpoints', () => { + config.emit('change', { Controls: { controls: { debug: true } } }); + const requestMock = new testUtil.MockRestOperation({ method: 'GET' }); + requestMock.uri = parseURL('http://localhost/shared/telemetry/systempoller'); + + restWorker.onGet(requestMock); + assert.strictEqual(requestMock.getBody(), 'replyBody'); + }); + + it('should disable debug endpoints', () => { + config.emit('change', { Controls: { controls: { debug: true } } }); + config.emit('change', { }); + const requestMock = new testUtil.MockRestOperation({ method: 'GET' }); + requestMock.uri = parseURL('http://localhost/shared/telemetry/systempoller'); + + restWorker.onGet(requestMock); + assert.ok(/Bad URL/.test(requestMock.getBody()), 'should be Bad URL message'); + }); + }); + + describe('requests processing', () => { + beforeEach(() => { + sinon.stub(systemPoller, 'processClientRequest').callsFake((restOperation) => { + util.restOperationResponder(restOperation, 200, 'systemPollerReplyBody'); + }); + sinon.stub(iHealthPoller, 'processClientRequest').callsFake((restOperation) => { + util.restOperationResponder(restOperation, 200, 'iHealthPollerReplyBody'); + }); + sinon.stub(config, 'processClientRequest').callsFake((restOperation) => { + util.restOperationResponder(restOperation, 200, 'configReplyBody'); + }); + return new Promise((resolve, reject) => { + restWorker.onStartCompleted(resolve, msg => reject(new Error(msg || 'no message provided'))); + }); + }); + + it('should return HTTP 405 when method not allowed', (done) => { + const requestMock = new testUtil.MockRestOperation({ method: 'POST' }); + requestMock.uri = parseURL('http://localhost/shared/telemetry/info'); + requestMock.complete = () => { + const responseBody = requestMock.getBody(); + assert.strictEqual(requestMock.statusCode, 405); + assert.strictEqual(responseBody.code, 405); + assert.strictEqual(responseBody.message, 'Method Not Allowed'); + assert.deepStrictEqual(responseBody.allow, ['GET']); + done(); + }; + restWorker.onGet(requestMock); + }); + + it('should return HTTP 415 when request has invalid content-type ', (done) => { + const requestMock = new testUtil.MockRestOperation({ method: 'POST' }); + requestMock.uri = parseURL('http://localhost/shared/telemetry/info'); + requestMock.body = 'body'; + requestMock.getContentType = () => ''; + requestMock.complete = () => { + const responseBody = requestMock.getBody(); + assert.strictEqual(requestMock.statusCode, 415); + assert.strictEqual(responseBody.code, 415); + assert.strictEqual(responseBody.message, 'Unsupported Media Type'); + assert.deepStrictEqual(responseBody.accept, ['application/json']); + done(); + }; + restWorker.onGet(requestMock); + }); + + it('should return HTTP 500 when failed to process request', (done) => { + const requestMock = new testUtil.MockRestOperation({ method: 'POST' }); + requestMock.uri = parseURL('http://localhost/shared/telemetry/info'); + requestMock.complete = () => { + if (requestMock.statusCode !== 500) { + throw new Error('expected error'); + } + const responseBody = requestMock.getBody(); + assert.strictEqual(requestMock.statusCode, 500); + assert.strictEqual(responseBody.code, 500); + assert.strictEqual(responseBody.message, 'Internal Server Error'); + done(); + }; + restWorker.onGet(requestMock); + }); + + const schemaVersionEnum = testUtil.deepCopy(baseSchema.properties.schemaVersion.enum); + const testDataArray = [ + { + endpoint: '/info', + allowedMethods: ['GET'], + expectedResponse: { + nodeVersion: process.version, + version: constants.VERSION, + release: constants.RELEASE, + schemaCurrent: schemaVersionEnum[0], + schemaMinimum: schemaVersionEnum[schemaVersionEnum.length - 1] + } + }, + { + endpoint: '/declare', + allowedMethods: ['GET', 'POST'], + expectedResponse: 'configReplyBody' + }, + { + endpoint: '/systempoller', + allowedMethods: ['GET'], + expectedResponse: 'systemPollerReplyBody' + }, + { + endpoint: '/ihealthpoller', + allowedMethods: ['GET'], + expectedResponse: 'iHealthPollerReplyBody' + } + ]; + + testDataArray.forEach((testData) => { + describe(`endpoint ${testData.endpoint}`, () => { + testData.allowedMethods.forEach((allowedMethod) => { + it(`should process ${allowedMethod} request`, (done) => { + const requestMock = new testUtil.MockRestOperation({ method: allowedMethod }); + requestMock.uri = parseURL(`http://localhost/shared/telemetry${testData.endpoint}`); + requestMock.complete = () => { + assert.deepStrictEqual(requestMock.getBody(), testData.expectedResponse); + done(); + }; + + config.emit('change', { Controls: { controls: { debug: true } } }); + restWorker.onGet(requestMock); + }); + }); + }); + }); + }); +}); diff --git a/test/unit/shared/bootstrap.js b/test/unit/shared/bootstrap.js new file mode 100644 index 00000000..05c9312e --- /dev/null +++ b/test/unit/shared/bootstrap.js @@ -0,0 +1,18 @@ +/* + * Copyright 2020. F5 Networks, Inc. See End User License Agreement ("EULA") for + * license terms. Notwithstanding anything to the contrary in the EULA, Licensee + * may copy and modify this software product for its internal business purposes. + * Further, Licensee may upload, publish and distribute the modified version of + * the software product on devcentral.f5.com. + */ + +'use strict'; + +require('./restoreCache'); + +/* eslint-disable no-console */ + +process.on('unhandledRejection', (reason, promise) => { + console.log('Unhandled Rejection at:', promise, 'reason:', reason); + throw reason; +}); diff --git a/test/unit/shared/disableAjv.js b/test/unit/shared/disableAjv.js new file mode 100644 index 00000000..b6482062 --- /dev/null +++ b/test/unit/shared/disableAjv.js @@ -0,0 +1,37 @@ +/* + * Copyright 2020. F5 Networks, Inc. See End User License Agreement ("EULA") for + * license terms. Notwithstanding anything to the contrary in the EULA, Licensee + * may copy and modify this software product for its internal business purposes. + * Further, Licensee may upload, publish and distribute the modified version of + * the software product on devcentral.f5.com. + */ + +'use strict'; + +/** + * Disable Ajv to speed up test process on node 4/6 + */ +const Ajv = require('ajv'); + +/** + * Restore initial Ajv state + * + * @returns {Function} that restores initial Ajv state + */ +module.exports = (function () { + const funcNames = [ + 'addKeyword', + 'addSchema', + 'compile' + ]; + const originFuncs = {}; + funcNames.forEach((funcName) => { + originFuncs[funcName] = Ajv.prototype[funcName]; + Ajv.prototype[funcName] = function () {}; + }); + return function restore() { + funcNames.forEach((funcName) => { + Ajv.prototype[funcName] = originFuncs[funcName]; + }); + }; +}()); diff --git a/test/unit/shared/restoreCache.js b/test/unit/shared/restoreCache.js new file mode 100644 index 00000000..14025c21 --- /dev/null +++ b/test/unit/shared/restoreCache.js @@ -0,0 +1,93 @@ +/* + * Copyright 2020. F5 Networks, Inc. See End User License Agreement ("EULA") for + * license terms. Notwithstanding anything to the contrary in the EULA, Licensee + * may copy and modify this software product for its internal business purposes. + * Further, Licensee may upload, publish and distribute the modified version of + * the software product on devcentral.f5.com. + */ + +'use strict'; + +/* + This module helps to restore cache state. + Be careful with it, read node's documentation about modules and node's internals. + This approach doesn't solve all issues with cache. For example: + + > content of module_1: + module.exports = EventEmitter(); + + > content of module_2: + const eventEmitter = require('./module_1'); + const moduleId = randomNumber(); + eventEmitter.on('change', () => console.log(`on change event, moduleId = ${moduleId}`)); + + > content of module_3: + const eventEmitter = require('./module_1'); + const module2Inst1 = require('./module_2'); + eventEmitter.emit('change'); + // you will see just ONE line from module2Inst1 + delete require.cache[require.resolve('./module_2')]; + const module2Inst2 = require('./module_2'); + eventEmitter.emit('change'); + // you will see TWO lines from module2Inst1 and module2Inst2 + + So, actually it is memory leak but for tests it should be fine. +*/ + +// preload popular libs here for optimization +/* eslint-disable no-unused-vars */ +const chai = require('chai'); +const chaiAsPromised = require('chai-as-promised'); +const sinon = require('sinon'); +const fileLogger = require('../../winstonLogger'); + +chai.use(chaiAsPromised); + +let BASE_DIR = __dirname.split('/'); +BASE_DIR = BASE_DIR.slice(0, BASE_DIR.length - 3).join('/'); +const SRC_DIR = `${BASE_DIR}/src`; + +/** + * Verify that modules from '/src' are not cached + * + * @param {Object} cache - copy of require.cache + */ +function checkCache(cache) { + Object.keys(cache).forEach((key) => { + if (key.startsWith(SRC_DIR)) { + /* eslint-disable no-console */ + console.warn(`WARN: Module '${key.slice(BASE_DIR.length)}' found in require.cache!`); + // might be better to raise error here + } + }); +} + +/** + * Restore require.cache to desired state. It will force node to reload modules on + * attempt to import them. All *Tests.js file will have their own instances of 'fs', 'os' and etc. + * + * @param {Object} preExistedCache - copy of require.cache + */ +function restoreCache(preExistedCache) { + Object.keys(require.cache).forEach((key) => { + delete require.cache[key]; + }); + Object.assign(require.cache, preExistedCache); + checkCache(require.cache); +} + +/** + * Restore initial require.cache state + * + * @returns {Function} that restores initial require.cache state + */ +module.exports = (function () { + console.info('Saving initial \'require.cache\' state'); + console.info(`Source code directory - ${SRC_DIR}`); + // just to be sure that modules from /src are not imported yet + checkCache(require.cache); + const preExistedCache = Object.assign({}, require.cache); + return function restore() { + restoreCache(preExistedCache); + }; +}()); diff --git a/test/unit/shared/util.js b/test/unit/shared/util.js index 74742ddb..1df0d3af 100644 --- a/test/unit/shared/util.js +++ b/test/unit/shared/util.js @@ -8,10 +8,33 @@ 'use strict'; +const nock = require('nock'); + const systemPollerData = require('../consumers/data/systemPollerData.json'); const avrData = require('../consumers/data/avrData.json'); + +function MockRestOperation(opts) { + opts = opts || {}; + this.method = opts.method || 'GET'; + this.body = opts.body; + this.statusCode = null; + this.uri = {}; + this.uri.pathname = opts.uri; +} +MockRestOperation.prototype.getBody = function () { return this.body; }; +MockRestOperation.prototype.setBody = function (body) { this.body = body; }; +MockRestOperation.prototype.getMethod = function () { return this.method; }; +MockRestOperation.prototype.setMethod = function (method) { this.method = method; }; +MockRestOperation.prototype.getStatusCode = function () { return this.statusCode; }; +MockRestOperation.prototype.setStatusCode = function (code) { this.statusCode = code; }; +MockRestOperation.prototype.getUri = function () { return this.uri; }; +MockRestOperation.prototype.complete = function () { }; + + module.exports = { + MockRestOperation, + /** * Deep copy * @@ -58,5 +81,107 @@ module.exports = { } }; return context; + }, + + /** + * Returns mocha's 'it' or 'it.only' + * + * @param {Object} testConf - test config + * @param {Object} [testConf.testOpts] - test options + * @param {Boolean} [testConf.testOpts.only] - true if use .only + * + * @returns {Function} mocha's 'it' function + */ + getCallableIt(testConf) { + return testConf.testOpts && testConf.testOpts.only ? it.only : it; + }, + + /** + * Returns mocha's 'describe' or 'describe.only' + * + * @param {Object} testConf - test config + * @param {Object} [testConf.testOpts] - test options + * @param {Boolean} [testConf.testOpts.only] - true if use .only + * + * @returns {Function} mocha's 'describe' function + */ + getCallableDescribe(testConf) { + return testConf.testOpts && testConf.testOpts.only ? describe.only : describe; + }, + + /** + * Setup endpoints mocks via nock + * + * @param {Array} endpointMocks - array of mocks + * @param {String} endpointMocks[].endpoint - endpoint + * @param {String} [endpointMocks[].method] - request method, by default 'get' + * @param {Any} [endpointMocks[].request] - request body + * @param {Object} [endpointMocks[].requestHeaders] - request headers + * @param {Any} [endpointMocks[].response] - response body + * @param {Object} [endpointMocks[].responseHeaders] - response headers + * @param {Integer} [endpointMocks[].code] - response code, by default 200 + * @param {Integer} [endpointMocks[].options.times] - repeat response N times + * @param {Object} [options] - options + * @param {String} [options.host] - host, by default 'localhost' + * @param {Integer} [options.port] - port, by default 8100 + * @param {String} [options.proto] - protocol, by default 'http' + * @param {Function} [options.responseChecker] - function to check response + */ + mockEndpoints(endpointMocks, options) { + options = options || {}; + endpointMocks.forEach((endpointMock) => { + let mockOpts; + if (typeof endpointMock.requestHeaders !== 'undefined') { + mockOpts = { + reqheaders: endpointMock.requestHeaders + }; + } + + const hostMock = nock(`${options.proto || 'http'}://${options.host || 'localhost'}:${options.port || 8100}`, mockOpts); + + let request = endpointMock.request; + if (typeof request === 'object') { + request = this.deepCopy(request); + } + let apiMock = hostMock[(endpointMock.method || 'GET').toLowerCase()](endpointMock.endpoint, request); + if (endpointMock.options) { + const opts = endpointMock.options; + if (opts.times) { + apiMock = apiMock.times(opts.times); + } + } + let response = endpointMock.response; + if (typeof response === 'object') { + // deep copy in any case, do not rely on nock lib + response = this.deepCopy(response); + } + if (options.responseChecker) { + if (typeof response === 'object') { + options.responseChecker(endpointMock, response); + } else if (typeof response === 'function') { + const originResponse = response; + response = (uri, requestBody) => { + const ret = originResponse(uri, requestBody); + options.responseChecker(endpointMock, ret); + return ret; + }; + } + } + apiMock.reply(endpointMock.code || 200, response, endpointMock.responseHeaders); + }); + }, + + /** + * Check if nock has unused mocks and raise assertion error if so + * + * @param {Object} nockInstance - instance of nock library + * @param {Object} assertInstance - instance of assert library + */ + checkNockActiveMocks(nockInstance, assertInstance) { + const activeMocks = nockInstance.activeMocks().join('\n'); + assertInstance.ok( + activeMocks.length === 0, + `nock should have no active mocks after the test, instead mocks are still active:\n${activeMocks}\n` + ); } }; diff --git a/test/unit/systemPollerTests.js b/test/unit/systemPollerTests.js index be7012ec..571e78e2 100644 --- a/test/unit/systemPollerTests.js +++ b/test/unit/systemPollerTests.js @@ -1,5 +1,5 @@ /* - * Copyright 2018. F5 Networks, Inc. See End User License Agreement ('EULA') for + * Copyright 2020. F5 Networks, Inc. See End User License Agreement ('EULA') for * license terms. Notwithstanding anything to the contrary in the EULA, Licensee * may copy and modify this software product for its internal business purposes. * Further, Licensee may upload, publish and distribute the modified version of @@ -8,116 +8,494 @@ 'use strict'; +/* eslint-disable import/order */ + +require('./shared/restoreCache')(); + const chai = require('chai'); const chaiAsPromised = require('chai-as-promised'); const sinon = require('sinon'); +const configWorker = require('../../src/lib/config'); +const constants = require('../../src/lib/constants'); +const deviceUtil = require('../../src/lib/deviceUtil'); const systemPoller = require('../../src/lib/systemPoller'); -const config = require('../../src/lib/config'); -const systemStats = require('../../src/lib/systemStats'); +const SystemStats = require('../../src/lib/systemStats'); +const util = require('../../src/lib/util'); + +const systemPollerConfigTestsData = require('./systemPollerTestsData'); +const testUtil = require('./shared/util'); chai.use(chaiAsPromised); const assert = chai.assert; -function MockRestOperation(opts) { - this.method = opts.method || 'GET'; - this.body = opts.body; - this.statusCode = null; - this.uri = {}; - this.uri.pathname = opts.uri; -} -MockRestOperation.prototype.getUri = function () { return this.uri; }; -MockRestOperation.prototype.setStatusCode = function (status) { this.statusCode = status; }; -MockRestOperation.prototype.getStatusCode = function () { return this.statusCode; }; -MockRestOperation.prototype.setBody = function (body) { this.body = body; }; -MockRestOperation.prototype.getBody = function () { return this.body; }; -MockRestOperation.prototype.complete = function () { }; - -describe('systemPoller', () => { - describe('.processClientRequest', () => { - let getConfigData; +describe('System Poller', () => { + const validateAndFormat = function (declaration) { + return configWorker.validate(declaration) + .then(validated => Promise.resolve(util.formatConfig(validated))) + .then(validated => deviceUtil.decryptAllSecrets(validated)); + }; + + beforeEach(() => { + sinon.stub(deviceUtil, 'encryptSecret').resolvesArg(0); + sinon.stub(deviceUtil, 'decryptSecret').resolvesArg(0); + sinon.stub(deviceUtil, 'getDeviceType').resolves(constants.DEVICE_TYPE.BIG_IP); + sinon.stub(util, 'networkCheck').resolves(); + }); + + afterEach(() => { + sinon.restore(); + }); + + describe('.safeProcess()', () => { + let config; + let returnCtx; + let sinonClock; beforeEach(() => { - sinon.stub(config, 'getConfig').callsFake(() => Promise.resolve(getConfigData)); + sinonClock = sinon.useFakeTimers(); + config = { + dataOpts: { + actions: [] + }, + interval: 100 + }; + returnCtx = null; + + sinon.stub(SystemStats.prototype, 'collect').callsFake(() => { + if (typeof returnCtx === 'object') { + return Promise.resolve(util.deepCopy(returnCtx)); + } + return returnCtx(); + }); }); afterEach(() => { - sinon.restore(); + sinonClock.restore(); + }); + + it('should fail when .process rejects promise (requestFromUser)', () => { + returnCtx = () => Promise.reject(new Error('some error')); + return assert.isRejected( + systemPoller.safeProcess(config, { requestFromUser: true }), + /some error/ + ); + }); + + it('should not fail when .process rejects promise (background process)', () => { + returnCtx = () => Promise.reject(new Error('some error')); + return assert.isFulfilled(systemPoller.safeProcess(config)); }); - it('should set status code of 400', () => { - const restOperation = new MockRestOperation({ uri: 'path/' }); - systemPoller.processClientRequest(restOperation); - assert.equal(restOperation.getStatusCode(), 400); - assert.deepEqual(restOperation.getBody(), { code: 400, message: 'Bad Request. System\'s or System Poller\'s name not specified.' }); + it('should fail when .process throws error (requestFromUser)', () => { + sinon.stub(systemPoller, 'process').throws(new Error('some error')); + return assert.isRejected( + systemPoller.safeProcess(config, { requestFromUser: true }), + /systemPoller:safeProcess unhandled exception.*some error/ + ); }); - it('should set status code of 200', (done) => { - getConfigData = { - parsed: { - Controls: { - controls: { - class: 'Controls', - debug: true, - logLevel: 'info' + it('should not fail when .process throws error (background process)', () => { + sinon.stub(systemPoller, 'process').throws(new Error('some error')); + return assert.isFulfilled(systemPoller.safeProcess(config)); + }); + + it('should resolve with data', () => { + // thanks to fakeTimers - Date returns the same data + const dataString = (new Date()).toISOString(); + returnCtx = () => Promise.resolve({ data: 'data' }); + return assert.becomes( + systemPoller.safeProcess(config, { requestFromUser: true }), + { + data: { + data: 'data', + telemetryEventCategory: 'systemInfo', + telemetryServiceInfo: { + cycleStart: dataString, + cycleEnd: dataString, + pollingInterval: 100 } }, - Telemetry_System: { - My_System: { - class: 'Telemetry_System_Poller', - systemPoller: { - interval: 60, - actions: [ - { - setTag: { - facility: 'facilityValue' - }, - locations: { - system: true - } - } - ] - }, + isCustom: undefined, + type: 'systemInfo' + } + ); + }); + }); + + describe('.processClientRequest()', () => { + let declaration; + let returnCtx; + + beforeEach(() => { + returnCtx = null; + + sinon.stub(configWorker, 'getConfig').callsFake(() => configWorker.validate(declaration) + .then(validated => Promise.resolve(util.formatConfig(validated))) + .then(validated => Promise.resolve({ parsed: validated }))); + + sinon.stub(systemPoller, 'process').callsFake((config) => { + if (returnCtx) { + return returnCtx(); + } + return Promise.resolve({ data: { poller: config.name } }); + }); + }); + /* eslint-disable implicit-arrow-linebreak */ + systemPollerConfigTestsData.processClientRequest.forEach(testConf => + testUtil.getCallableIt(testConf)(testConf.name, () => { + declaration = testConf.declaration; + const restOpMock = new testUtil.MockRestOperation(testConf.requestOpts); + + if (typeof testConf.returnCtx !== 'undefined') { + returnCtx = testConf.returnCtx; + } + return new Promise((resolve) => { + restOpMock.complete = function () { + resolve(); + }; + systemPoller.processClientRequest(restOpMock); + }) + .then(() => { + assert.deepStrictEqual( + { body: restOpMock.body, code: restOpMock.statusCode }, + testConf.expectedResponse + ); + }); + })); + }); + + describe('.getTraceValue()', () => { + it('should preserve trace config', () => { + const matrix = systemPollerConfigTestsData.getTraceValue; + const systemTraceValues = matrix[0]; + + for (let i = 1; i < matrix.length; i += 1) { + const pollerTrace = matrix[i][0]; + + for (let j = 1; j < systemTraceValues.length; j += 1) { + const systemTrace = systemTraceValues[j]; + const expectedTrace = matrix[i][j]; + assert.strictEqual( + systemPoller.getTraceValue(systemTrace, pollerTrace), + expectedTrace, + `Expected to be ${expectedTrace} when systemTrace=${systemTrace} and pollerTrace=${pollerTrace}` + ); + } + } + }); + }); + + describe('config "on change" event', () => { + const defaultDeclaration = { + class: 'Telemetry', + My_System: { + class: 'Telemetry_System', + trace: true, + systemPoller: { + interval: 180 + } + } + }; + let activeTracersStub; + let allTracersStub; + let pollerTimers; + let utilStub; + + beforeEach(() => { + activeTracersStub = []; + allTracersStub = []; + pollerTimers = {}; + utilStub = { start: [], stop: [], update: [] }; + + sinon.stub(util, 'start').callsFake((func, args, interval) => { + utilStub.start.push({ args, interval }); + return interval; + }); + sinon.stub(util, 'update').callsFake((id, func, args, interval) => { + utilStub.update.push({ args, interval }); + return interval; + }); + sinon.stub(util, 'stop').callsFake((arg) => { + utilStub.stop.push({ arg }); + }); + sinon.stub(util.tracer, 'createFromConfig').callsFake((className, objName, config) => { + allTracersStub.push(objName); + if (config.trace) { + activeTracersStub.push(objName); + } + return null; + }); + sinon.stub(systemPoller, 'getPollerTimers').returns(pollerTimers); + + return validateAndFormat(defaultDeclaration) + .then((config) => { + // expecting the code responsible for 'change' event to be synchronous + configWorker.emit('change', config); + assert.strictEqual(pollerTimers['My_System::SystemPoller_1'], 180); + assert.strictEqual(allTracersStub.length, 1); + assert.strictEqual(activeTracersStub.length, 1); + assert.strictEqual(utilStub.start.length, 1); + assert.strictEqual(utilStub.update.length, 0); + assert.strictEqual(utilStub.stop.length, 0); + + utilStub = { start: [], stop: [], update: [] }; + allTracersStub = []; + activeTracersStub = []; + }); + }); + + it('should stop existing poller(s)', () => { + // expecting the code responsible for 'change' event to be synchronous + configWorker.emit('change', {}); + assert.deepStrictEqual(pollerTimers, {}); + assert.strictEqual(allTracersStub.length, 0); + assert.strictEqual(activeTracersStub.length, 0); + assert.strictEqual(utilStub.start.length, 0); + assert.strictEqual(utilStub.update.length, 0); + assert.strictEqual(utilStub.stop.length, 1); + }); + + it('should update existing poller(s)', () => { + const newDeclaration = testUtil.deepCopy(defaultDeclaration); + newDeclaration.My_System.systemPoller.interval = 500; + newDeclaration.My_System.systemPoller.trace = true; + return validateAndFormat(newDeclaration) + .then((config) => { + // expecting the code responsible for 'change' event to be synchronous + configWorker.emit('change', config); + assert.strictEqual(allTracersStub.length, 1); + assert.strictEqual(activeTracersStub.length, 1); + assert.strictEqual(utilStub.start.length, 0); + assert.strictEqual(utilStub.update.length, 1); + assert.strictEqual(utilStub.stop.length, 0); + assert.deepStrictEqual(pollerTimers, { 'My_System::SystemPoller_1': 500 }); + assert.deepStrictEqual(utilStub.update[0].args, { + name: 'My_System::SystemPoller_1', + enable: true, + interval: 500, + trace: true, + tracer: null, + credentials: { + username: undefined, + passphrase: undefined + }, + connection: { + allowSelfSignedCert: false, host: 'localhost', port: 8100, protocol: 'http' - } - } - } - }; - sinon.stub(systemStats.prototype, 'collect').callsFake(() => Promise.resolve({})); + }, + dataOpts: { + noTMStats: true, + tags: undefined, + actions: [ + { + enable: true, + setTag: { + application: '`A`', + tenant: '`T`' + } + } + ] + }, + endpointList: undefined + }); + }); + }); - const restOperation = new MockRestOperation({ uri: 'shared/telemetry/systempoller/Telemetry_System/My_System' }); - restOperation.complete = function () { - assert.strictEqual(restOperation.body.telemetryEventCategory, 'systemInfo'); - assert.strictEqual(restOperation.getStatusCode(), 200); - done(); - }; - systemPoller.processClientRequest(restOperation); + it('should ignore disabled pollers (existing poller)', () => { + const newDeclaration = testUtil.deepCopy(defaultDeclaration); + newDeclaration.My_System.enable = false; + return validateAndFormat(newDeclaration) + .then((config) => { + // expecting the code responsible for 'change' event to be synchronous + configWorker.emit('change', config); + assert.deepStrictEqual(pollerTimers, {}); + assert.strictEqual(allTracersStub.length, 0); + assert.strictEqual(activeTracersStub.length, 0); + assert.strictEqual(utilStub.start.length, 0); + assert.strictEqual(utilStub.update.length, 0); + assert.strictEqual(utilStub.stop.length, 1); + }); + }); + + it('should ignore disabled pollers (non-existing poller)', () => { + const newDeclaration = testUtil.deepCopy(defaultDeclaration); + newDeclaration.My_System_New = testUtil.deepCopy(newDeclaration.My_System); + newDeclaration.My_System_New.enable = false; + return validateAndFormat(newDeclaration) + .then((config) => { + // expecting the code responsible for 'change' event to be synchronous + configWorker.emit('change', config); + assert.deepStrictEqual(pollerTimers, { 'My_System::SystemPoller_1': 180 }); + assert.strictEqual(allTracersStub.length, 1); + assert.strictEqual(activeTracersStub.length, 1); + assert.strictEqual(utilStub.start.length, 0); + assert.strictEqual(utilStub.update.length, 1); + assert.strictEqual(utilStub.stop.length, 0); + }); + }); + + it('should ignore System without poller (existing poller)', () => { + const newDeclaration = testUtil.deepCopy(defaultDeclaration); + delete newDeclaration.My_System.systemPoller; + return validateAndFormat(newDeclaration) + .then((config) => { + // expecting the code responsible for 'change' event to be synchronous + configWorker.emit('change', config); + assert.deepStrictEqual(pollerTimers, {}); + assert.strictEqual(allTracersStub.length, 0); + assert.strictEqual(activeTracersStub.length, 0); + assert.strictEqual(utilStub.start.length, 0); + assert.strictEqual(utilStub.update.length, 0); + assert.strictEqual(utilStub.stop.length, 1); + }); + }); + + it('should ignore System without poller (non-existing poller)', () => { + const newDeclaration = testUtil.deepCopy(defaultDeclaration); + newDeclaration.My_System_New = testUtil.deepCopy(newDeclaration.My_System); + delete newDeclaration.My_System_New.systemPoller; + return validateAndFormat(newDeclaration) + .then((config) => { + // expecting the code responsible for 'change' event to be synchronous + configWorker.emit('change', config); + assert.deepStrictEqual(pollerTimers, { 'My_System::SystemPoller_1': 180 }); + assert.strictEqual(allTracersStub.length, 1); + assert.strictEqual(activeTracersStub.length, 1); + assert.strictEqual(utilStub.start.length, 0); + assert.strictEqual(utilStub.update.length, 1); + assert.strictEqual(utilStub.stop.length, 0); + }); }); - it('should reject with 404 status', (done) => { - getConfigData = { - parsed: { - Controls: { - controls: { - class: 'Controls', - debug: true, - logLevel: 'info' + it('should start new poller (non-existing poller, inline declaration)', () => { + const newDeclaration = testUtil.deepCopy(defaultDeclaration); + newDeclaration.My_System_New = testUtil.deepCopy(newDeclaration.My_System); + newDeclaration.My_System_New.trace = false; + newDeclaration.My_System_New.systemPoller.interval = 500; + return validateAndFormat(newDeclaration) + .then((config) => { + // expecting the code responsible for 'change' event to be synchronous + configWorker.emit('change', config); + assert.deepStrictEqual(pollerTimers, { + 'My_System::SystemPoller_1': 180, + 'My_System_New::SystemPoller_1': 500 + }); + assert.strictEqual(allTracersStub.length, 2); + assert.strictEqual(activeTracersStub.length, 1); + assert.strictEqual(utilStub.start.length, 1); + assert.strictEqual(utilStub.update.length, 1); + assert.strictEqual(utilStub.stop.length, 0); + }); + }); + + it('should handle multiple pollers per system', () => { + const newDeclaration = testUtil.deepCopy(defaultDeclaration); + newDeclaration.My_System_New = testUtil.deepCopy(newDeclaration.My_System); + newDeclaration.My_System_New.systemPoller = [ + { + interval: 10, + endpointList: { + basePath: 'mgmt/', + items: { + endpoint1: { + path: 'ltm/pool' + } } } - } - }; - const expected = { - code: 404, - message: 'Error: System Poller declaration not found.' - }; - const restOperation = new MockRestOperation({ uri: 'shared/telemetry/systempoller/Telemetry_System/My_System' }); - restOperation.complete = function () { - assert.deepEqual(restOperation.getBody(), expected); - done(); + }, + 'My_Poller' + ]; + newDeclaration.My_Poller = { + class: 'Telemetry_System_Poller', + trace: true, + interval: 500 }; - systemPoller.processClientRequest(restOperation); + return validateAndFormat(newDeclaration) + .then((config) => { + // expecting the code responsible for 'change' event to be synchronous + configWorker.emit('change', config); + assert.deepStrictEqual(pollerTimers, { + 'My_System::SystemPoller_1': 180, + 'My_System_New::SystemPoller_1': 10, + 'My_System_New::My_Poller': 500 + }); + assert.strictEqual(allTracersStub.length, 3); + assert.strictEqual(activeTracersStub.length, 3); + assert.strictEqual(utilStub.start.length, 2); + assert.strictEqual(utilStub.update.length, 1); + assert.strictEqual(utilStub.stop.length, 0); + assert.deepStrictEqual(utilStub.start[0].args, { + name: 'My_System_New::SystemPoller_1', + enable: true, + interval: 10, + trace: true, + tracer: null, + credentials: { + username: undefined, + passphrase: undefined + }, + connection: { + allowSelfSignedCert: false, + host: 'localhost', + port: 8100, + protocol: 'http' + }, + dataOpts: { + noTMStats: true, + tags: undefined, + actions: [ + { + enable: true, + setTag: { + application: '`A`', + tenant: '`T`' + } + } + ] + }, + endpointList: { + endpoint1: { + enable: true, + name: 'endpoint1', + path: '/mgmt/ltm/pool' + } + } + }); + assert.deepStrictEqual(utilStub.start[1].args, { + name: 'My_System_New::My_Poller', + enable: true, + interval: 500, + trace: true, + tracer: null, + credentials: { + username: undefined, + passphrase: undefined + }, + connection: { + allowSelfSignedCert: false, + host: 'localhost', + port: 8100, + protocol: 'http' + }, + dataOpts: { + noTMStats: true, + tags: undefined, + actions: [ + { + enable: true, + setTag: { + application: '`A`', + tenant: '`T`' + } + } + ] + }, + endpointList: undefined + }); + }); }); }); }); diff --git a/test/unit/systemPollerTestsData.js b/test/unit/systemPollerTestsData.js new file mode 100644 index 00000000..f7222174 --- /dev/null +++ b/test/unit/systemPollerTestsData.js @@ -0,0 +1,421 @@ +/* + * Copyright 2020. F5 Networks, Inc. See End User License Agreement ('EULA') for + * license terms. Notwithstanding anything to the contrary in the EULA, Licensee + * may copy and modify this software product for its internal business purposes. + * Further, Licensee may upload, publish and distribute the modified version of + * the software product on devcentral.f5.com. + */ + +'use strict'; + +module.exports = { + /** + * Set of data to check actual and expected results only. + * If you need some additional check feel free to add additional + * property or write separate test. + * + * Note: you can specify 'testOpts' property on the same level as 'name'. + * Following options available: + * - only (bool) - run this test only (it.only) + * */ + processClientRequest: [ + // TEST RELATED DATA STARTS HERE + { + name: 'should fail when \'.safeProcess()\' failed (just basic Error thrown)', + declaration: { + class: 'Telemetry', + My_System_Poller: { + class: 'Telemetry_System_Poller' + }, + My_System: { + class: 'Telemetry_System' + } + }, + returnCtx: () => { throw new Error('expected error'); }, + requestOpts: { + uri: '/shared/telemetry/systempoller/My_System/My_System_Poller' + }, + expectedResponse: { + code: 500, + body: { + code: 500, + message: 'Error: systemPoller:safeProcess unhandled exception: Error: expected error' + } + } + }, + // TEST RELATED DATA STARTS HERE + { + name: 'should fail when \'.safeProcess()\' failed (Promise rejection)', + declaration: { + class: 'Telemetry', + My_System_Poller: { + class: 'Telemetry_System_Poller' + }, + My_System: { + class: 'Telemetry_System' + } + }, + returnCtx: () => Promise.reject(new Error('expected error')), + requestOpts: { + uri: '/shared/telemetry/systempoller/My_System/My_System_Poller' + }, + expectedResponse: { + code: 500, + body: { + code: 500, + message: 'Error: expected error' + } + } + }, + // TEST RELATED DATA STARTS HERE + { + name: 'should fail when no System or System Poller name specified', + requestOpts: { + uri: '/shared/telemetry/systempoller' + }, + expectedResponse: { + code: 400, + body: { + code: 400, + message: 'Error: Bad Request. Name for System or System Poller was not specified.' + } + } + }, + // TEST RELATED DATA STARTS HERE + { + name: 'should fail when System with such name doesn\'t exist (uri = system)', + declaration: { + class: 'Telemetry' + }, + requestOpts: { + uri: '/shared/telemetry/systempoller/systemName' + }, + expectedResponse: { + code: 404, + body: { + code: 404, + message: 'Error: System or System Poller with name \'systemName\' doesn\'t exist' + } + } + }, + // TEST RELATED DATA STARTS HERE + { + name: 'should fail when System has no configured System Poller (uri = system)', + declaration: { + class: 'Telemetry', + My_System: { + class: 'Telemetry_System' + } + }, + requestOpts: { + uri: '/shared/telemetry/systempoller/My_System' + }, + expectedResponse: { + code: 404, + body: { + code: 404, + message: 'Error: System with name \'My_System\' has no System Poller configured' + } + } + }, + // TEST RELATED DATA STARTS HERE + { + name: 'should fail when System with such name doesn\'t exist (uri = system/poller)', + declaration: { + class: 'Telemetry' + }, + requestOpts: { + uri: '/shared/telemetry/systempoller/systemName/systemPoller' + }, + expectedResponse: { + code: 404, + body: { + code: 404, + message: 'Error: System with name \'systemName\' doesn\'t exist' + } + } + }, + // TEST RELATED DATA STARTS HERE + { + name: 'should fail when System Poller with such name doesn\'t exist (uri = system/poller)', + declaration: { + class: 'Telemetry', + My_System: { + class: 'Telemetry_System' + } + }, + requestOpts: { + uri: '/shared/telemetry/systempoller/My_System/systemPoller' + }, + expectedResponse: { + code: 404, + body: { + code: 404, + message: 'Error: System Poller with name \'systemPoller\' doesn\'t exist' + } + } + }, + // TEST RELATED DATA STARTS HERE + { + name: 'should return expected data on 200 (System Poller)(uri = poller)', + declaration: { + class: 'Telemetry', + My_System_Poller: { + class: 'Telemetry_System_Poller' + }, + My_System: { + class: 'Telemetry_System', + systemPoller: 'My_System_Poller' + } + }, + requestOpts: { + uri: '/shared/telemetry/systempoller/My_System_Poller' + }, + expectedResponse: { + code: 200, + body: [ + { + poller: 'My_System_Poller::My_System_Poller' + } + ] + } + }, + // TEST RELATED DATA STARTS HERE + { + name: 'should return expected data on 200 (System + inline System Poller)(uri = system)', + declaration: { + class: 'Telemetry', + My_System_Poller: { + class: 'Telemetry_System_Poller' + }, + My_System: { + class: 'Telemetry_System', + systemPoller: { + interval: 100 + } + } + }, + requestOpts: { + uri: '/shared/telemetry/systempoller/My_System' + }, + expectedResponse: { + code: 200, + body: [ + { + poller: 'My_System::SystemPoller_1' + } + ] + } + }, + // TEST RELATED DATA STARTS HERE + { + name: 'should return expected data on 200 (System + System Poller reference override)(uri = system/poller)', + declaration: { + class: 'Telemetry', + My_System_Poller: { + class: 'Telemetry_System_Poller' + }, + My_System: { + class: 'Telemetry_System', + systemPoller: { + interval: 100 + } + } + }, + requestOpts: { + uri: '/shared/telemetry/systempoller/My_System/My_System_Poller' + }, + expectedResponse: { + code: 200, + body: [ + { + poller: 'My_System::My_System_Poller' + } + ] + } + }, + // TEST RELATED DATA STARTS HERE + { + name: 'should return expected data on 200 (System + System Poller reference)(uri = system/poller)', + declaration: { + class: 'Telemetry', + My_System_Poller: { + class: 'Telemetry_System_Poller' + }, + My_System: { + class: 'Telemetry_System', + systemPoller: 'My_System_Poller' + } + }, + requestOpts: { + uri: '/shared/telemetry/systempoller/My_System/My_System_Poller' + }, + expectedResponse: { + code: 200, + body: [ + { + poller: 'My_System::My_System_Poller' + } + ] + } + }, + // TEST RELATED DATA STARTS HERE + { + name: 'should return expected data on 200 (System + System Poller reference)(uri = system)', + declaration: { + class: 'Telemetry', + My_System_Poller: { + class: 'Telemetry_System_Poller' + }, + My_System: { + class: 'Telemetry_System', + systemPoller: 'My_System_Poller' + } + }, + requestOpts: { + uri: '/shared/telemetry/systempoller/My_System' + }, + expectedResponse: { + code: 200, + body: [ + { + poller: 'My_System::My_System_Poller' + } + ] + } + }, + // TEST RELATED DATA STARTS HERE + { + name: 'should return expected data on 200 (System + array of System Poller references)(uri = system)', + declaration: { + class: 'Telemetry', + SystemPoller1: { + class: 'Telemetry_System_Poller' + }, + SystemPoller2: { + class: 'Telemetry_System_Poller' + }, + My_System: { + class: 'Telemetry_System', + systemPoller: [ + 'SystemPoller1', + 'SystemPoller2' + ] + } + }, + requestOpts: { + uri: '/shared/telemetry/systempoller/My_System' + }, + expectedResponse: { + code: 200, + body: [ + { + poller: 'My_System::SystemPoller1' + }, + { + poller: 'My_System::SystemPoller2' + } + ] + } + }, + // TEST RELATED DATA STARTS HERE + { + name: 'should return expected data on 200 (System + array of inlined System Pollers)(uri = system)', + declaration: { + class: 'Telemetry', + SystemPoller1: { + class: 'Telemetry_System_Poller' + }, + SystemPoller_2: { + class: 'Telemetry_System_Poller' + }, + My_System: { + class: 'Telemetry_System', + systemPoller: [ + { + interval: 100 + }, + 'SystemPoller_2', + { + interval: 200 + } + ] + } + }, + requestOpts: { + uri: '/shared/telemetry/systempoller/My_System' + }, + expectedResponse: { + code: 200, + body: [ + { + poller: 'My_System::SystemPoller_1' + }, + { + poller: 'My_System::SystemPoller_2' + }, + { + poller: 'My_System::SystemPoller_3' + } + ] + } + }, + // TEST RELATED DATA STARTS HERE + { + name: 'should return expected data on 200 even if System or System Poller disabled (System + array of inlined System Pollers)(uri = system)', + declaration: { + class: 'Telemetry', + SystemPoller1: { + class: 'Telemetry_System_Poller', + enable: false + }, + SystemPoller2: { + class: 'Telemetry_System_Poller', + enable: false + }, + My_System: { + class: 'Telemetry_System', + enable: false, + systemPoller: [ + { + interval: 100, + enable: false + }, + 'SystemPoller2', + { + interval: 200, + enable: false + } + ] + } + }, + requestOpts: { + uri: '/shared/telemetry/systempoller/My_System' + }, + expectedResponse: { + code: 200, + body: [ + { + poller: 'My_System::SystemPoller_1' + }, + { + poller: 'My_System::SystemPoller2' + }, + { + poller: 'My_System::SystemPoller_2' + } + ] + } + } + ], + getTraceValue: [ + // matrix (like boolean logic), + // first array is system's trace value (ignore element at index 0) + // first element of each next line (starting from line 1) is poller's trace value + ['', undefined, 'system', true, false], // eslint-disable-line no-multi-spaces + [undefined, false, 'system', true, false], // eslint-disable-line no-multi-spaces + ['poller', 'poller', 'poller', 'poller', false], // eslint-disable-line no-multi-spaces + [true, true, 'system', true, false], // eslint-disable-line no-multi-spaces + [false, false, false, false, false] // eslint-disable-line no-multi-spaces + ] +}; diff --git a/test/unit/systemStatsTests.js b/test/unit/systemStatsTests.js index e1db928a..75ceb840 100644 --- a/test/unit/systemStatsTests.js +++ b/test/unit/systemStatsTests.js @@ -8,22 +8,37 @@ 'use strict'; +/* eslint-disable import/order */ + +require('./shared/restoreCache')(); + const chai = require('chai'); const chaiAsPromised = require('chai-as-promised'); -const nock = require('nock'); const sinon = require('sinon'); +const defaultProperties = require('../../src/lib/properties.json'); +const systemStatsTestsData = require('./systemStatsTestsData'); const SystemStats = require('../../src/lib/systemStats'); -const paths = require('../../src/lib/paths.json'); -const allProperties = require('../../src/lib/properties.json'); -const systemStatsTestsData = require('./systemStatsTestsData.js'); +const testUtil = require('./shared/util'); chai.use(chaiAsPromised); const assert = chai.assert; -describe('systemStats', () => { +describe('System Stats', () => { + // global vars - to avoid problems with 'before(Each)' + let allProperties = testUtil.deepCopy(defaultProperties); + + beforeEach(() => { + // do copy before each test to avoid modifications + allProperties = testUtil.deepCopy(defaultProperties); + }); + describe('.processData', () => { - const sysStats = new SystemStats({}, {}); + let sysStats; + + beforeEach(() => { + sysStats = new SystemStats(); + }); it('should skip normalization', () => { const property = { @@ -38,7 +53,7 @@ describe('systemStats', () => { kind: 'dataKind' }; const result = sysStats._processData(property, data, key); - assert.deepEqual(result, expected); + assert.deepStrictEqual(result, expected); }); it('should normalize data', () => { @@ -81,7 +96,7 @@ describe('systemStats', () => { } }; const result = sysStats._processData(property, data, key); - assert.deepEqual(result, expected); + assert.deepStrictEqual(result, expected); }); it('should normalize data and use defaults without normalization array', () => { @@ -102,7 +117,7 @@ describe('systemStats', () => { } }; const result = sysStats._processData(property, data, key); - assert.deepEqual(result, expected); + assert.deepStrictEqual(result, expected); }); it('should normalize data and use defaults with normalization array', () => { @@ -129,452 +144,14 @@ describe('systemStats', () => { } }; const result = sysStats._processData(property, data, key); - assert.deepEqual(result, expected); - }); - }); - - describe('.collect()', () => { - function filterPaths(name) { - return { - endpoints: [paths.endpoints.find( - p => p.name === name || p.endpoint === name - )] - - }; - } - - describe('tmstats', () => { - function assertTmStat(statKey, tmctlKey) { - nock('http://localhost:8100') - .post( - '/mgmt/tm/util/bash', - { - command: 'run', - utilCmdArgs: `-c '/bin/tmctl -c ${tmctlKey}'` - } - ) - .reply(200, { - commandResult: [ - 'a,b,c', - '0,0,spam', - '0,1,eggs' - ].join('\n') - }); - - const host = {}; - const options = { - paths: filterPaths('tmctl'), - properties: { - stats: { - tmstats: allProperties.stats.tmstats, - [statKey]: allProperties.stats[statKey] - }, - global: allProperties.global - } - }; - const stats = new SystemStats(host, options); - return assert.becomes(stats.collect(), { - tmstats: { - [statKey]: [ - { - a: '0', - b: '0', - c: 'spam' - }, - { - a: '0', - b: '1', - c: 'eggs' - } - ] - } - }); - } - - const stats = allProperties.stats; - const tmctlArgs = '\\$tmctlArgs'; - - Object.keys(stats).forEach((stat) => { - if ((stats[stat].structure || {}).parentKey === 'tmstats') { - const tableName = stats[stat].keyArgs.replaceStrings[tmctlArgs].split('-c').pop().trim(); - it(`should collect ${stat}`, () => assertTmStat(stat, tableName)); - } - }); - }); - - describe('system info', () => { - it('should collect hostname and machineId', () => { - nock('http://localhost:8100') - .get('/mgmt/shared/identified-devices/config/device-info') - .times(2) - .reply(200, { - machineId: 'cc4826c5-d557-40c0-aa3f-3fc3aca0e40c', - hostname: 'test.local' - }); - const host = {}; - const options = { - paths: filterPaths('deviceInfo'), - properties: { - stats: { - system: allProperties.stats.system, - hostname: allProperties.stats.hostname, - machineId: allProperties.stats.machineId - }, - global: allProperties.global - } - }; - const stats = new SystemStats(host, options); - return assert.becomes(stats.collect(), { - system: { - hostname: 'test.local', - machineId: 'cc4826c5-d557-40c0-aa3f-3fc3aca0e40c' - } - }); - }); - }); - - describe('virtual servers', () => { - before(() => { - const mockResps = systemStatsTestsData.collectVirtualServers; - mockResps.forEach((mock) => { - nock('http://localhost:8100') - .get(mock.endpoint) - .reply(200, mock.response); - }); - }); - - it('should collect virtualServers config and stats', () => { - const host = {}; - const options = { - paths: filterPaths('virtualServers'), - properties: { - stats: { - virtualServers: allProperties.stats.virtualServers - }, - global: allProperties.global - } - }; - const stats = new SystemStats(host, options); - return assert.becomes(stats.collect(), { - virtualServers: { - '/Common/test': { - availabilityState: 'unknown', - 'clientside.bitsIn': 1, - 'clientside.bitsOut': 2, - 'clientside.curConns': 3, - destination: '192.0.2.1:80', - enabledState: 'enabled', - ipProtocol: 'tcp', - mask: '255.255.255.255', - name: '/Common/test', - pool: '/Common/pool' - } - } - }); - }); - }); - - describe('dns and gslb', () => { - let stats; - - before(() => { - sinon.stub(SystemStats.prototype, '_computeContextData').resolves(); - }); - - beforeEach(() => { - const mockResps = systemStatsTestsData.collectGtm; - mockResps.forEach((mock) => { - nock('http://localhost:8100') - .get(mock.endpoint) - .reply(200, mock.response); - }); - const endpointNames = [ - 'aWideIps', - 'cnameWideIps', - 'aPools', - 'mxPools' - ]; - const host = {}; - const options = { - paths: { - endpoints: paths.endpoints.filter(p => endpointNames.indexOf(p.name) > -1) - }, - properties: { - stats: { - aWideIps: allProperties.stats.aWideIps, - cnameWideIps: allProperties.stats.cnameWideIps, - aPools: allProperties.stats.aPools, - mxPools: allProperties.stats.mxPools - }, - global: allProperties.global - } - }; - stats = new SystemStats(host, options); - SystemStats.prototype._computeContextData.restore(); - sinon.stub(SystemStats.prototype, '_computeContextData').callsFake(() => { - stats.contextData = { - provisioning: { - gtm: { - name: 'gtm', - level: 'minimum' - } - } - }; - return Promise.resolve(); - }); - }); - - afterEach(() => { - nock.cleanAll(); - }); - - it('should collect wideip config and stats', () => stats.collect() - .then((actualStats) => { - assert.deepEqual(actualStats.aWideIps, - { - '/Common/www.aone.tstest.com': { - alternate: 0, - cnameResolutions: 0, - dropped: 0, - fallback: 0, - persisted: 0, - preferred: 2, - rcode: 0, - requests: 8, - resolutions: 2, - returnFromDns: 0, - returnToDns: 3, - 'status.availabilityState': 'offline', - 'status.enabledState': 'enabled', - 'status.statusReason': 'No enabled pools available', - wipType: 'A', - lastResortPool: '', - name: '/Common/www.aone.tstest.com', - partition: 'Common', - persistCidrIpv4: 32, - loadBalancingDecisionLogVerbosity: [ - 'pool-selection', - 'pool-traversal', - 'pool-member-selection', - 'pool-member-traversal' - ], - pools: [ - '/Common/ts_a_pool' - ], - poolLbMode: 'round-robin', - persistence: 'disabled', - ttlPersistence: 3600, - failureRcode: 'noerror', - minimalResponse: 'enabled', - failureRcodeTtl: 0, - aliases: [ - 'www.aone.com' - ], - enabled: true, - persistCidrIpv6: 128, - failureRcodeResponse: 'disabled' - } - }); - - assert.deepEqual(actualStats.cnameWideIps, - { - '/Common/www.cnameone.tstest.com': { - alternate: 0, - cnameResolutions: 0, - dropped: 0, - fallback: 0, - persisted: 0, - preferred: 0, - rcode: 0, - requests: 0, - resolutions: 0, - returnFromDns: 0, - returnToDns: 0, - 'status.availabilityState': 'unknown', - 'status.enabledState': 'enabled', - 'status.statusReason': 'Checking', - wipType: 'CNAME', - name: 'www.cnameone.tstest.com', - partition: 'Common', - enabled: true, - failureRcode: 'noerror', - failureRcodeResponse: 'disabled', - failureRcodeTtl: 0, - lastResortPool: '', - minimalResponse: 'enabled', - persistCidrIpv4: 32, - persistCidrIpv6: 128, - persistence: 'disabled', - poolLbMode: 'round-robin', - ttlPersistence: 3600 - } - }); - })); - - it('should collect pools and members config and stats', () => stats.collect().then((actualStats) => { - assert.deepEqual(actualStats.aPools, - { - '/Common/ts_a_pool': { - alternate: 10, - dropped: 10, - fallback: 10, - poolType: 'A', - preferred: 10, - returnFromDns: 10, - returnToDns: 10, - availabilityState: 'offline', - enabledState: 'enabled', - 'status.statusReason': 'No enabled pool members available', - name: '/Common/ts_a_pool', - alternateMode: 'round-robin', - dynamicRatio: 'disabled', - enabled: true, - fallbackIp: '8.8.8.8', - fallbackMode: 'return-to-dns', - limitMaxBps: 0, - limitMaxBpsStatus: 'disabled', - limitMaxConnections: 0, - limitMaxConnectionsStatus: 'disabled', - limitMaxPps: 0, - limitMaxPpsStatus: 'disabled', - loadBalancingMode: 'ratio', - manualResume: 'disabled', - maxAnswersReturned: 1, - monitor: '/Common/gateway_icmp', - partition: 'Common', - qosHitRatio: 5, - qosHops: 0, - qosKilobytesSecond: 3, - qosLcs: 30, - qosPacketRate: 1, - qosRtt: 50, - qosTopology: 0, - qosVsCapacity: 0, - qosVsScore: 0, - ttl: 30, - verifyMemberAvailability: 'disabled', - members: { - 'vs1:/Common/server1': { - enabled: true, - limitMaxBps: 100, - limitMaxBpsStatus: 'disabled', - limitMaxConnections: 100, - limitMaxConnectionsStatus: 'disabled', - limitMaxPps: 100, - limitMaxPpsStatus: 'disabled', - memberOrder: 100, - monitor: 'default', - name: 'server1:vs1', - ratio: 1, - alternate: 20, - fallback: 20, - partition: 'Common', - poolName: '/Common/ts_a_pool', - poolType: 'A', - preferred: 20, - serverName: '/Common/server1', - availabilityState: 'offline', - enabledState: 'enabled', - 'status.statusReason': ' Monitor /Common/gateway_icmp from 172.16.100.17 : no route', - vsName: 'vs1' - } - } - } - }); - - assert.deepEqual(actualStats.mxPools, - { - '/Common/ts_mx_pool': { - alternate: 0, - dropped: 0, - fallback: 0, - poolType: 'MX', - preferred: 0, - returnFromDns: 0, - returnToDns: 0, - availabilityState: 'offline', - enabledState: 'enabled', - 'status.statusReason': 'No enabled pool members available', - fallbackMode: 'return-to-dns', - ttl: 30, - name: '/Common/ts_mx_pool', - partition: 'Common', - members: { - 'www.aaaaone.tstest.com': { - alternate: 0, - fallback: 0, - poolName: '/Common/ts_mx_pool', - poolType: 'MX', - preferred: 0, - serverName: 'www.aaaaone.tstest.com', - availabilityState: 'offline', - enabledState: 'enabled', - 'status.statusReason': 'No Wide IPs available: No enabled pools available', - vsName: ' ' - }, - 'www.aone.tstest.com': { - alternate: 0, - fallback: 0, - poolName: '/Common/ts_mx_pool', - poolType: 'MX', - preferred: 0, - serverName: 'www.aone.tstest.com', - availabilityState: 'offline', - enabledState: 'enabled', - 'status.statusReason': 'No Wide IPs available: No enabled pools available', - vsName: ' ' - } - }, - alternateMode: 'topology', - qosHops: 0, - verifyMemberAvailability: 'enabled', - qosPacketRate: 1, - qosRtt: 50, - enabled: true, - qosLcs: 30, - qosVsCapacity: 0, - qosVsScore: 0, - maxAnswersReturned: 12, - loadBalancingMode: 'round-robin', - qosHitRatio: 5, - qosKilobytesSecond: 3, - qosTopology: 0, - manualResume: 'enabled', - dynamicRatio: 'enabled' - } - }); - })); - - it('data should be undefined if gtm is not provisioned', () => { - SystemStats.prototype._computeContextData.restore(); - sinon.stub(SystemStats.prototype, '_computeContextData').callsFake(() => { - stats.contextData = { - provisioning: { - ltm: { - name: 'ltm', - level: 'nominal' - } - } - }; - return Promise.resolve(); - }); - - return assert.becomes(stats.collect(), { - aWideIps: undefined, - aPools: undefined, - cnameWideIps: undefined, - mxPools: undefined - }); - }); + assert.deepStrictEqual(result, expected); }); }); describe('._filterStats', () => { - const getCallableIt = testConf => (testConf.testOpts && testConf.testOpts.only ? it.only : it); - systemStatsTestsData._filterStats.forEach((testConf) => { - getCallableIt(testConf)(testConf.name, () => { - const systemStats = new SystemStats({}, { noTmstats: true, actions: testConf.actions }); + testUtil.getCallableIt(testConf)(testConf.name, () => { + const systemStats = new SystemStats({ dataOpts: { noTMstats: true, actions: testConf.actions } }); systemStats._filterStats(); const statsKeys = Object.keys(systemStats.stats); @@ -621,7 +198,7 @@ describe('systemStats', () => { describe('._processProperty()', () => { it('should return empty promise when noTmstats is true', () => { - const systemStats = new SystemStats({}, { noTmstats: true }); + const systemStats = new SystemStats({ dataOpts: { noTMStats: true } }); const property = { structure: { parentKey: 'tmstats' @@ -629,23 +206,23 @@ describe('systemStats', () => { }; return systemStats._processProperty('', property) .then(() => { - assert.deepEqual(systemStats.collectedData, {}); + assert.deepStrictEqual(systemStats.collectedData, {}); }); }); it('should return empty promise when disabled', () => { - const systemStats = new SystemStats({}, { noTmstats: true }); + const systemStats = new SystemStats({ dataOpts: { noTMStats: true } }); const property = { disabled: true }; return systemStats._processProperty('', property) .then(() => { - assert.deepEqual(systemStats.collectedData, {}); + assert.deepStrictEqual(systemStats.collectedData, {}); }); }); it('should add theKey to collectedData', () => { - const systemStats = new SystemStats({}, {}); + const systemStats = new SystemStats(); const property = { structure: { folder: true @@ -653,12 +230,12 @@ describe('systemStats', () => { }; return systemStats._processProperty('theKey', property) .then(() => { - assert.deepEqual(systemStats.collectedData.theKey, {}); + assert.deepStrictEqual(systemStats.collectedData.theKey, {}); }); }); it('should add to collectedData', () => { - const systemStats = new SystemStats({}, {}); + const systemStats = new SystemStats(); const property = { key: 'theKey' }; @@ -667,10 +244,10 @@ describe('systemStats', () => { key: 'theKey' } }; - sinon.stub(systemStats, '_loadData').callsFake(() => Promise.resolve(property)); + sinon.stub(systemStats, '_loadData').resolves(property); return systemStats._processProperty('theKey', property) .then(() => { - assert.deepEqual(systemStats.collectedData, expected); + assert.deepStrictEqual(systemStats.collectedData, expected); }); }); }); diff --git a/test/unit/systemStatsTestsData.js b/test/unit/systemStatsTestsData.js index 85710539..a690baa7 100644 --- a/test/unit/systemStatsTestsData.js +++ b/test/unit/systemStatsTestsData.js @@ -378,775 +378,213 @@ module.exports = { } ], shouldKeepOnly: ['system', 'hostname', 'virtualServers'] - } - ], - collectVirtualServers: [ - { - endpoint: '/mgmt/tm/ltm/virtual?$select=name,fullPath,selfLink,appService,ipProtocol,mask,pool', - response: { - items: [ - { - name: 'test', - fullPath: '/Common/test', - selfLink: 'https://localhost/mgmt/tm/ltm/virtual/~Common~test?ver=13.1.1.3', - ipProtocol: 'tcp', - mask: '255.255.255.255', - pool: '/Common/pool', - poolReference: { - link: 'https://localhost/mgmt/tm/ltm/pool/~Common~pool?ver=13.1.1.3' - } - } - ] - } }, + // TEST RELATED DATA STARTS HERE { - endpoint: '/mgmt/tm/ltm/virtual/~Common~test/stats', - response: { - kind: 'tm:ltm:virtual:virtualstats', - generation: 1, - selfLink: 'https://localhost/mgmt/tm/ltm/virtual/~Common~test/stats?ver=13.1.1.3', - entries: { - 'https://localhost/mgmt/tm/ltm/virtual/~Common~test/~Common~test/stats': { - nestedStats: { - kind: 'tm:ltm:virtual:virtualstats', - selfLink: 'https://localhost/mgmt/tm/ltm/virtual/~Common~test/~Common~test/stats?ver=13.1.1.3', - entries: { - 'clientside.bitsIn': { - value: 1 - }, - 'clientside.bitsOut': { - value: 2 - }, - 'clientside.curConns': { - value: 3 - }, - 'clientside.evictedConns': { - value: 0 - }, - 'clientside.maxConns': { - value: 0 - }, - 'clientside.pktsIn': { - value: 0 - }, - 'clientside.pktsOut': { - value: 0 - }, - 'clientside.slowKilled': { - value: 0 - }, - 'clientside.totConns': { - value: 0 - }, - cmpEnableMode: { - description: 'all-cpus' - }, - cmpEnabled: { - description: 'enabled' - }, - csMaxConnDur: { - value: 0 - }, - csMeanConnDur: { - value: 0 - }, - csMinConnDur: { - value: 0 - }, - destination: { - description: '192.0.2.1:80' - }, - 'ephemeral.bitsIn': { - value: 0 - }, - 'ephemeral.bitsOut': { - value: 0 - }, - 'ephemeral.curConns': { - value: 0 - }, - 'ephemeral.evictedConns': { - value: 0 - }, - 'ephemeral.maxConns': { - value: 0 - }, - 'ephemeral.pktsIn': { - value: 0 - }, - 'ephemeral.pktsOut': { - value: 0 - }, - 'ephemeral.slowKilled': { - value: 0 - }, - 'ephemeral.totConns': { - value: 0 - }, - fiveMinAvgUsageRatio: { - value: 0 - }, - fiveSecAvgUsageRatio: { - value: 0 - }, - tmName: { - description: '/Common/test' - }, - oneMinAvgUsageRatio: { - value: 0 - }, - 'status.availabilityState': { - description: 'unknown' - }, - 'status.enabledState': { - description: 'enabled' - }, - 'status.statusReason': { - description: 'The children pool member(s) either don\'t have service checking enabled, or service check results are not available yet' - }, - syncookieStatus: { - description: 'not-activated' - }, - 'syncookie.accepts': { - value: 0 - }, - 'syncookie.hwAccepts': { - value: 0 - }, - 'syncookie.hwSyncookies': { - value: 0 - }, - 'syncookie.hwsyncookieInstance': { - value: 0 - }, - 'syncookie.rejects': { - value: 0 - }, - 'syncookie.swsyncookieInstance': { - value: 0 - }, - 'syncookie.syncacheCurr': { - value: 0 - }, - 'syncookie.syncacheOver': { - value: 0 - }, - 'syncookie.syncookies': { - value: 0 - }, - totRequests: { - value: 0 - } - } + name: 'should preserve ifAnyMatch locations (example 1)', + actions: [ + { + setTags: {}, + enable: true, + ifAnyMatch: [{ + system: { + hostname: 'hostname' } - } - } - } - } - ], - collectGtm: [ - { - endpoint: '/mgmt/tm/gtm/wideip/a', - response: { - kind: 'tm:gtm:wideip:a:acollectionstate', - selfLink: 'https://localhost/mgmt/tm/gtm/wideip/a?ver=13.1.1.4', - items: [ - { - kind: 'tm:gtm:wideip:a:astate', - name: 'www.aone.tstest.com', - partition: 'Common', - fullPath: '/Common/www.aone.tstest.com', - generation: 1498, - selfLink: 'https://localhost/mgmt/tm/gtm/wideip/a/~Common~www.aone.tstest.com?ver=13.1.1.4', - enabled: true, - failureRcode: 'noerror', - failureRcodeResponse: 'disabled', - failureRcodeTtl: 0, - lastResortPool: '', - loadBalancingDecisionLogVerbosity: [ - 'pool-selection', - 'pool-traversal', - 'pool-member-selection', - 'pool-member-traversal' - ], - minimalResponse: 'enabled', - persistCidrIpv4: 32, - persistCidrIpv6: 128, - persistence: 'disabled', - poolLbMode: 'round-robin', - ttlPersistence: 3600, - aliases: [ - 'www.aone.com' - ], - pools: [ - { - name: 'ts_a_pool', - partition: 'Common', - order: 0, - ratio: 1, - nameReference: { - link: 'https://localhost/mgmt/tm/gtm/pool/a/~Common~ts_a_pool?ver=13.1.1.4' - } - } - ] - } - ] - } - }, - { - endpoint: '/mgmt/tm/gtm/wideip/a/~Common~www.aone.tstest.com/stats', - response: { - kind: 'tm:gtm:wideip:a:acollectionstats', - selfLink: 'https://localhost/mgmt/tm/gtm/wideip/a/~Common~www.aone.tstest.com/stats?ver=13.1.1.4', - entries: { - 'https://localhost/mgmt/tm/gtm/wideip/a/~Common~www.aone.tstest.com:A/stats': { - nestedStats: { - kind: 'tm:gtm:wideip:a:astats', - selfLink: 'https://localhost/mgmt/tm/gtm/wideip/a/~Common~www.aone.tstest.com:A/stats?ver=13.1.1.4', - entries: { - alternate: { value: 0 }, - cnameResolutions: { value: 0 }, - dropped: { value: 0 }, - fallback: { value: 0 }, - persisted: { value: 0 }, - preferred: { value: 2 }, - rcode: { value: 0 }, - requests: { value: 8 }, - resolutions: { value: 2 }, - returnFromDns: { value: 0 }, - returnToDns: { value: 3 }, - 'status.availabilityState': { description: 'offline' }, - 'status.enabledState': { description: 'enabled' }, - 'status.statusReason': { description: 'No enabled pools available' }, - wipName: { description: '/Common/www.aone.tstest.com' }, - wipType: { description: 'A' } - } + }] + }, + { + excludeData: {}, + enable: true, + locations: { + system: { + hostname: true } } - } - } - }, - { - endpoint: '/mgmt/tm/gtm/wideip/cname', - response: { - kind: 'tm:gtm:wideip:cname:cnamecollectionstate', - selfLink: 'https://localhost/mgmt/tm/gtm/wideip/cname?ver=13.1.1.4', - items: [ - { - kind: 'tm:gtm:wideip:cname:cnamestate', - name: 'www.cnameone.tstest.com', - partition: 'Common', - fullPath: '/Common/www.cnameone.tstest.com', - generation: 1600, - selfLink: 'https://localhost/mgmt/tm/gtm/wideip/cname/~Common~www.cnameone.tstest.com?ver=13.1.1.4', - enabled: true, - failureRcode: 'noerror', - failureRcodeResponse: 'disabled', - failureRcodeTtl: 0, - lastResortPool: '', - minimalResponse: 'enabled', - persistCidrIpv4: 32, - persistCidrIpv6: 128, - persistence: 'disabled', - poolLbMode: 'round-robin', - ttlPersistence: 3600 - } - ] - } - }, - { - endpoint: '/mgmt/tm/gtm/wideip/cname/~Common~www.cnameone.tstest.com/stats', - response: { - kind: 'tm:gtm:wideip:cname:cnamestats', - generation: 1600, - selfLink: 'https://localhost/mgmt/tm/gtm/wideip/cname/~Common~www.cnameone.tstest.com/stats?ver=13.1.1.4', - entries: { - 'https://localhost/mgmt/tm/gtm/wideip/cname/~Common~www.cnameone.tstest.com/~Common~www.cnameone.tstest.com:CNAME/stats': { - nestedStats: { - kind: 'tm:gtm:wideip:cname:cnamestats', - selfLink: 'https://localhost/mgmt/tm/gtm/wideip/cname/~Common~www.cnameone.tstest.com/~Common~www.cnameone.tstest.com:CNAME/stats?ver=13.1.1.4', - entries: { - alternate: { - value: 0 - }, - cnameResolutions: { - value: 0 - }, - dropped: { - value: 0 - }, - fallback: { - value: 0 - }, - persisted: { - value: 0 - }, - preferred: { - value: 0 - }, - rcode: { - value: 0 - }, - requests: { - value: 0 - }, - resolutions: { - value: 0 - }, - returnFromDns: { - value: 0 - }, - returnToDns: { - value: 0 - }, - 'status.availabilityState': { - description: 'unknown' - }, - 'status.enabledState': { - description: 'enabled' - }, - 'status.statusReason': { - description: 'Checking' - }, - wipName: { - description: '/Common/www.cnameone.tstest.com' - }, - wipType: { - description: 'CNAME' - } - } - } + }, + { + includeData: {}, + enable: true, + locations: { + virtualServers: true } } - } + ], + shouldKeepOnly: ['system', 'hostname', 'virtualServers'] }, + // TEST RELATED DATA STARTS HERE { - endpoint: '/mgmt/tm/gtm/pool/a', - response: { - kind: 'tm:gtm:pool:a:acollectionstate', - selfLink: 'https://localhost/mgmt/tm/gtm/pool/a?ver=13.1.1.4', - items: [ - { - kind: 'tm:gtm:pool:a:astate', - name: 'ts_a_pool', - partition: 'Common', - fullPath: '/Common/ts_a_pool', - generation: 1501, - selfLink: 'https://localhost/mgmt/tm/gtm/pool/a/~Common~ts_a_pool?ver=13.1.1.4', - alternateMode: 'round-robin', - dynamicRatio: 'disabled', - enabled: true, - fallbackIp: '8.8.8.8', - fallbackMode: 'return-to-dns', - limitMaxBps: 0, - limitMaxBpsStatus: 'disabled', - limitMaxConnections: 0, - limitMaxConnectionsStatus: 'disabled', - limitMaxPps: 0, - limitMaxPpsStatus: 'disabled', - loadBalancingMode: 'ratio', - manualResume: 'disabled', - maxAnswersReturned: 1, - monitor: '/Common/gateway_icmp', - qosHitRatio: 5, - qosHops: 0, - qosKilobytesSecond: 3, - qosLcs: 30, - qosPacketRate: 1, - qosRtt: 50, - qosTopology: 0, - qosVsCapacity: 0, - qosVsScore: 0, - ttl: 30, - verifyMemberAvailability: 'disabled', - membersReference: { - link: 'https://localhost/mgmt/tm/gtm/pool/a/~Common~ts_a_pool/members?ver=13.1.1.4', - isSubcollection: true + name: 'should preserve ifAnyMatch locations (example 2)', + actions: [ + { + includeData: {}, + ifAnyMatch: [{ + system: { + hostname: 'hostname' + } + }], + enable: true, + locations: { + system: { + hostname: true } } - ] - } + }, + { + excludeData: {}, + enable: true, + locations: { + '.*': true + } + } + ], + shouldKeepOnly: ['system', 'hostname'] }, + // TEST RELATED DATA STARTS HERE { - endpoint: '/mgmt/tm/gtm/pool/a/~Common~ts_a_pool/stats', - response: { - kind: 'tm:gtm:pool:a:astats', - generation: 1495, - selfLink: 'https://localhost/mgmt/tm/gtm/pool/a/~Common~ts_a_pool/stats?ver=13.1.1.4', - entries: { - 'https://localhost/mgmt/tm/gtm/pool/a/~Common~ts_a_pool/~Common~ts_a_pool:A/stats': { - nestedStats: { - kind: 'tm:gtm:pool:a:astats', - selfLink: 'https://localhost/mgmt/tm/gtm/pool/a/~Common~ts_a_pool/~Common~ts_a_pool:A/stats?ver=13.1.1.4', - entries: { - alternate: { - value: 10 - }, - dropped: { - value: 10 - }, - fallback: { - value: 10 - }, - tmName: { - description: '/Common/ts_a_pool' - }, - poolType: { - description: 'A' - }, - preferred: { - value: 10 - }, - returnFromDns: { - value: 10 - }, - returnToDns: { - value: 10 - }, - 'status.availabilityState': { - description: 'offline' - }, - 'status.enabledState': { - description: 'enabled' - }, - 'status.statusReason': { - description: 'No enabled pool members available' + name: 'should preserve ifAnyMatch locations (example 3)', + actions: [ + { + setTags: {}, + enable: true, + ifAnyMatch: [ + { + system: { + hostname: 'hostname' + }, + virtualServers: { + '.*': { + enabledState: 'enabled' + } + }, + pools: { + '.*': { + availabilityState: 'offline' } } } - } - } - } - }, - { - endpoint: '/mgmt/tm/gtm/pool/a/~Common~ts_a_pool/members', - response: { - kind: 'tm:gtm:pool:a:members:memberscollectionstate', - selfLink: 'https://localhost/mgmt/tm/gtm/pool/a/~Common~ts_a_pool/members?ver=13.1.1.4', - items: [ - { - kind: 'tm:gtm:pool:a:members:membersstate', - name: 'server1:vs1', - partition: 'Common', - fullPath: '/Common/server1:vs1', - generation: 2703, - selfLink: 'https://localhost/mgmt/tm/gtm/pool/a/~Common~ts_a_pool/members/~Common~server1:vs1?ver=13.1.1.4', - enabled: true, - limitMaxBps: 100, - limitMaxBpsStatus: 'disabled', - limitMaxConnections: 100, - limitMaxConnectionsStatus: 'disabled', - limitMaxPps: 100, - limitMaxPpsStatus: 'disabled', - memberOrder: 100, - monitor: 'default', - ratio: 1 - } - ] - } - }, - { - endpoint: '/mgmt/tm/gtm/pool/a/~Common~ts_a_pool/members/stats', - response: { - kind: 'tm:gtm:pool:a:members:memberscollectionstats', - selfLink: 'https://localhost/mgmt/tm/gtm/pool/a/~Common~ts_a_pool/members/stats?ver=13.1.1.4', - entries: { - 'https://localhost/mgmt/tm/gtm/pool/a/~Common~ts_a_pool/members/vs1:~Common~server1/stats': { - nestedStats: { - kind: 'tm:gtm:pool:a:members:membersstats', - selfLink: 'https://localhost/mgmt/tm/gtm/pool/a/~Common~ts_a_pool/members/vs1:~Common~server1/stats?ver=13.1.1.4', - entries: { - alternate: { - value: 20 - }, - fallback: { - value: 20 - }, - poolName: { - description: '/Common/ts_a_pool' - }, - poolType: { - description: 'A' - }, - preferred: { - value: 20 - }, - serverName: { - description: '/Common/server1' - }, - 'status.availabilityState': { - description: 'offline' - }, - 'status.enabledState': { - description: 'enabled' - }, - 'status.statusReason': { - description: ' Monitor /Common/gateway_icmp from 172.16.100.17 : no route' - }, - vsName: { - description: 'vs1' + ] + }, + { + excludeData: {}, + enable: true, + ifAnyMatch: [{ + system: { + diskStorage: { + '/usr': { + name: '/usr' } } } - } - } - } - }, - { - endpoint: '/mgmt/tm/gtm/pool/mx', - response: { - kind: 'tm:gtm:pool:mx:mxcollectionstate', - selfLink: 'https://localhost/mgmt/tm/gtm/pool/mx?ver=13.1.1.4', - items: [ - { - kind: 'tm:gtm:pool:mx:mxstate', - name: 'ts_mx_pool', - partition: 'Common', - fullPath: '/Common/ts_mx_pool', - generation: 237, - selfLink: 'https://localhost/mgmt/tm/gtm/pool/mx/~Common~ts_mx_pool?ver=13.1.1.4', - alternateMode: 'topology', - dynamicRatio: 'enabled', - enabled: true, - fallbackMode: 'return-to-dns', - loadBalancingMode: 'round-robin', - manualResume: 'enabled', - maxAnswersReturned: 12, - qosHitRatio: 5, - qosHops: 0, - qosKilobytesSecond: 3, - qosLcs: 30, - qosPacketRate: 1, - qosRtt: 50, - qosTopology: 0, - qosVsCapacity: 0, - qosVsScore: 0, - ttl: 30, - verifyMemberAvailability: 'enabled', - membersReference: { - link: 'https://localhost/mgmt/tm/gtm/pool/mx/~Common~ts_mx_pool/members?ver=13.1.1.4', - isSubcollection: true + }], + locations: { + system: { + hostname: true } } - ] - } + }, + { + includeData: {}, + enable: true, + locations: { + virtualServers: true + } + } + ], + shouldKeepOnly: ['system', 'hostname', 'virtualServers', 'pools', 'diskStorage'] }, + // TEST RELATED DATA STARTS HERE { - endpoint: '/mgmt/tm/gtm/pool/mx/stats', - response: { - kind: 'tm:gtm:pool:mx:mxcollectionstats', - selfLink: 'https://localhost/mgmt/tm/gtm/pool/mx/stats?ver=13.1.1.4', - entries: { - 'https://localhost/mgmt/tm/gtm/pool/mx/~Common~ts_mx_pool:MX/stats': { - nestedStats: { - kind: 'tm:gtm:pool:mx:mxstats', - selfLink: 'https://localhost/mgmt/tm/gtm/pool/mx/~Common~ts_mx_pool:MX/stats?ver=13.1.1.4', - entries: { - alternate: { - value: 0 - }, - dropped: { - value: 0 - }, - fallback: { - value: 0 - }, - tmName: { - description: '/Common/ts_mx_pool' - }, - poolType: { - description: 'MX' - }, - preferred: { - value: 0 - }, - returnFromDns: { - value: 0 - }, - returnToDns: { - value: 0 - }, - 'status.availabilityState': { - description: 'offline' - }, - 'status.enabledState': { - description: 'enabled' - }, - 'status.statusReason': { - description: 'No enabled pool members available' + name: 'should preserve with ifAllMatch and ifAnyMatch locations (example 1)', + actions: [ + { + setTags: {}, + enable: true, + ifAnyMatch: [ + { + system: { + hostname: 'hostname' + }, + virtualServers: { + '.*': { + enabledState: 'enabled' } } } - } - } - } - }, - { - endpoint: '/mgmt/tm/gtm/pool/mx/~Common~ts_mx_pool/stats', - response: { - kind: 'tm:gtm:pool:mx:mxstats', - generation: 237, - selfLink: 'https://localhost/mgmt/tm/gtm/pool/mx/~Common~ts_mx_pool/stats?ver=13.1.1.4', - entries: { - 'https://localhost/mgmt/tm/gtm/pool/mx/~Common~ts_mx_pool/~Common~ts_mx_pool:MX/stats': { - nestedStats: { - kind: 'tm:gtm:pool:mx:mxstats', - selfLink: 'https://localhost/mgmt/tm/gtm/pool/mx/~Common~ts_mx_pool/~Common~ts_mx_pool:MX/stats?ver=13.1.1.4', - entries: { - alternate: { - value: 0 - }, - dropped: { - value: 0 - }, - fallback: { - value: 0 - }, - tmName: { - description: '/Common/ts_mx_pool' - }, - poolType: { - description: 'MX' - }, - preferred: { - value: 0 - }, - returnFromDns: { - value: 0 - }, - returnToDns: { - value: 0 - }, - 'status.availabilityState': { - description: 'offline' - }, - 'status.enabledState': { - description: 'enabled' - }, - 'status.statusReason': { - description: 'No enabled pool members available' + ] + }, + { + excludeData: {}, + enable: true, + ifAllMatch: { + system: { + diskStorage: { + '/usr': { + name: '/usr' } } } + }, + locations: { + system: { + hostname: true + } + } + }, + { + includeData: {}, + enable: true, + locations: { + pools: true } } - } + ], + shouldKeepOnly: ['system', 'hostname', 'virtualServers', 'diskStorage', 'pools'] }, + // TEST RELATED DATA STARTS HERE { - endpoint: '/mgmt/tm/gtm/pool/mx/~Common~ts_mx_pool/members', - response: { - kind: 'tm:gtm:pool:mx:members:memberscollectionstate', - selfLink: 'https://localhost/mgmt/tm/gtm/pool/mx/~Common~ts_mx_pool/members?ver=13.1.1.4', - items: [ - { - kind: 'tm:gtm:pool:mx:members:membersstate', - name: 'www.aaaaone.tstest.com', - fullPath: 'www.aaaaone.tstest.com', - generation: 237, - selfLink: 'https://localhost/mgmt/tm/gtm/pool/mx/~Common~ts_mx_pool/members/www.aaaaone.tstest.com?ver=13.1.1.4', - enabled: true, - memberOrder: 0, - priority: 100, - ratio: 1 + name: 'should preserve with ifAllMatch and ifAnyMatch locations (example 2)', + actions: [ + { + includeData: {}, + enable: true, + ifAllMatch: { + system: { + version: '12' + } }, - { - kind: 'tm:gtm:pool:mx:members:membersstate', - name: 'www.aone.tstest.com', - fullPath: 'www.aone.tstest.com', - generation: 237, - selfLink: 'https://localhost/mgmt/tm/gtm/pool/mx/~Common~ts_mx_pool/members/www.aone.tstest.com?ver=13.1.1.4', - enabled: true, - memberOrder: 1, - priority: 1, - ratio: 10 + locations: { + system: { + hostname: true + } } - ] - } - }, - { - endpoint: '/mgmt/tm/gtm/pool/mx/~Common~ts_mx_pool/members/stats', - response: { - kind: 'tm:gtm:pool:mx:members:memberscollectionstats', - selfLink: 'https://localhost/mgmt/tm/gtm/pool/mx/~Common~ts_mx_pool/members/stats?ver=13.1.1.4', - entries: { - 'https://localhost/mgmt/tm/gtm/pool/mx/~Common~ts_mx_pool/members/%20:www.aaaaone.tstest.com/stats': { - nestedStats: { - kind: 'tm:gtm:pool:mx:members:membersstats', - selfLink: 'https://localhost/mgmt/tm/gtm/pool/mx/~Common~ts_mx_pool/members/%20:www.aaaaone.tstest.com/stats?ver=13.1.1.4', - entries: { - alternate: { - value: 0 - }, - fallback: { - value: 0 - }, - poolName: { - description: '/Common/ts_mx_pool' - }, - poolType: { - description: 'MX' - }, - preferred: { - value: 0 - }, - serverName: { - description: 'www.aaaaone.tstest.com' - }, - 'status.availabilityState': { - description: 'offline' - }, - 'status.enabledState': { - description: 'enabled' - }, - 'status.statusReason': { - description: 'No Wide IPs available: No enabled pools available' - }, - vsName: { - description: ' ' + }, + { + setTags: {}, + enable: true, + ifAnyMatch: [ + { + virtualServers: { + '.*': { + enabledState: 'enabled' } - } - } - }, - 'https://localhost/mgmt/tm/gtm/pool/mx/~Common~ts_mx_pool/members/%20:www.aone.tstest.com/stats': { - nestedStats: { - kind: 'tm:gtm:pool:mx:members:membersstats', - selfLink: 'https://localhost/mgmt/tm/gtm/pool/mx/~Common~ts_mx_pool/members/%20:www.aone.tstest.com/stats?ver=13.1.1.4', - entries: { - alternate: { - value: 0 - }, - fallback: { - value: 0 - }, - poolName: { - description: '/Common/ts_mx_pool' - }, - poolType: { - description: 'MX' - }, - preferred: { - value: 0 - }, - serverName: { - description: 'www.aone.tstest.com' - }, - 'status.availabilityState': { - description: 'offline' - }, - 'status.enabledState': { - description: 'enabled' - }, - 'status.statusReason': { - description: 'No Wide IPs available: No enabled pools available' - }, - vsName: { - description: ' ' + }, + pools: { + '.*': { + availabilityState: 'offline' } } } + ] + }, + { + includeData: {}, + enable: true, + locations: { + virtualServers: true } } - } + ], + shouldKeepOnly: ['system', 'version', 'pools', 'virtualServers'] } ] }; diff --git a/test/unit/systemStatsUtilTests.js b/test/unit/systemStatsUtilTests.js new file mode 100644 index 00000000..3471e0a8 --- /dev/null +++ b/test/unit/systemStatsUtilTests.js @@ -0,0 +1,82 @@ +/* + * Copyright 2018. F5 Networks, Inc. See End User License Agreement ('EULA') for + * license terms. Notwithstanding anything to the contrary in the EULA, Licensee + * may copy and modify this software product for its internal business purposes. + * Further, Licensee may upload, publish and distribute the modified version of + * the software product on devcentral.f5.com. + */ + +'use strict'; + +/* eslint-disable import/order */ + +require('./shared/restoreCache')(); + +const chai = require('chai'); +const chaiAsPromised = require('chai-as-promised'); + +const systemStatsUtil = require('../../src/lib/systemStatsUtil'); +const systemStatsUtilTestsData = require('./systemStatsUtilTestsData'); +const testUtil = require('./shared/util'); + +chai.use(chaiAsPromised); +const assert = chai.assert; + +describe('System Stats Utils', () => { + describe('._resolveConditional()', () => { + systemStatsUtilTestsData._resolveConditional.forEach((testConf) => { + testUtil.getCallableIt(testConf)(testConf.name, () => { + const promise = new Promise((resolve, reject) => { + try { + resolve(systemStatsUtil._resolveConditional( + testUtil.deepCopy(testConf.contextData), + testUtil.deepCopy(testConf.conditionalBlock) + )); + } catch (err) { + reject(err); + } + }); + if (testConf.errorMessage) { + return assert.isRejected(promise, testConf.errorMessage); + } + return assert.becomes(promise, testConf.expectedData); + }); + }); + }); + + describe('._preprocessProperty()', () => { + systemStatsUtilTestsData._preprocessProperty.forEach((testConf) => { + testUtil.getCallableIt(testConf)(testConf.name, () => assert.deepStrictEqual( + systemStatsUtil._preprocessProperty( + testUtil.deepCopy(testConf.contextData), + testUtil.deepCopy(testConf.propertyData) + ), + testConf.expectedData + )); + }); + }); + + describe('._renderTemplate()', () => { + systemStatsUtilTestsData._renderTemplate.forEach((testConf) => { + testUtil.getCallableIt(testConf)(testConf.name, () => assert.deepStrictEqual( + systemStatsUtil._renderTemplate( + testUtil.deepCopy(testConf.contextData), + testUtil.deepCopy(testConf.propertyData) + ), + testConf.expectedData + )); + }); + }); + + describe('.renderProperty()', () => { + systemStatsUtilTestsData.renderProperty.forEach((testConf) => { + testUtil.getCallableIt(testConf)(testConf.name, () => assert.deepStrictEqual( + systemStatsUtil.renderProperty( + testUtil.deepCopy(testConf.contextData), + testUtil.deepCopy(testConf.propertyData) + ), + testConf.expectedData + )); + }); + }); +}); diff --git a/test/unit/systemStatsUtilTestsData.js b/test/unit/systemStatsUtilTestsData.js new file mode 100644 index 00000000..57c93d2a --- /dev/null +++ b/test/unit/systemStatsUtilTestsData.js @@ -0,0 +1,443 @@ +/* + * Copyright 2018. F5 Networks, Inc. See End User License Agreement ('EULA') for + * license terms. Notwithstanding anything to the contrary in the EULA, Licensee + * may copy and modify this software product for its internal business purposes. + * Further, Licensee may upload, publish and distribute the modified version of + * the software product on devcentral.f5.com. + */ + +'use strict'; + +/* eslint-disable no-useless-escape */ + +module.exports = { + /** + * Set of data to check actual and expected results only. + * If you need some additional check feel free to add additional + * property or write separate test. + * + * Note: you can specify 'testOpts' property on the same level as 'name'. + * Following options available: + * - only (bool) - run this test only (it.only) + * */ + _resolveConditional: [ + { + name: 'should fail when unknown function in conditional block', + contextData: {}, + conditionalBlock: { + unknownFunction: 'something' + }, + errorMessage: /Unknown property 'unknownFunction' in conditional block/ + }, + { + name: 'should fail when no deviceVersion in contextData', + contextData: {}, + conditionalBlock: { + deviceVersionGreaterOrEqual: '11.0' + }, + errorMessage: /deviceVersionGreaterOrEqual: context has no property 'deviceVersion'/ + }, + { + name: 'should fail when no provisioning in contextData', + contextData: {}, + conditionalBlock: { + isModuleProvisioned: 'afm' + }, + errorMessage: /isModuleProvisioned: context has no property 'provisioning'/ + }, + { + name: 'should return true when device version is greater or equal', + contextData: { + deviceVersion: '11.6.5' + }, + conditionalBlock: { + deviceVersionGreaterOrEqual: '11.6.0' + }, + expectedData: true + }, + { + name: 'should return false when device version is not greater or equal', + contextData: { + deviceVersion: '11.6.5' + }, + conditionalBlock: { + deviceVersionGreaterOrEqual: '14.1.0' + }, + expectedData: false + }, + { + name: 'should return true when module provisioned', + contextData: { + provisioning: { + afm: { + level: 'nominal' + } + } + }, + conditionalBlock: { + isModuleProvisioned: 'afm' + }, + expectedData: true + }, + { + name: 'should return false when module not provisioned', + contextData: { + provisioning: { + afm: { + level: 'none' + } + } + }, + conditionalBlock: { + isModuleProvisioned: 'afm' + }, + expectedData: false + }, + { + name: 'should return false when module absent', + contextData: { + provisioning: { + ltm: { + level: 'none' + } + } + }, + conditionalBlock: { + isModuleProvisioned: 'afm' + }, + expectedData: false + } + ], + _preprocessProperty: [ + { + name: 'should process property without conditional blocks', + contextData: {}, + propertyData: { + foo: 'bar' + }, + expectedData: { + foo: 'bar' + } + }, + { + name: 'should process property with null', + contextData: {}, + propertyData: { + foo: null + }, + expectedData: { + foo: null + } + }, + { + name: 'should process conditional block without then/else branches', + contextData: { + deviceVersion: '11.6' + }, + propertyData: { + if: { + deviceVersionGreaterOrEqual: '11.0' + }, + foo: 'bar' + }, + expectedData: { + foo: 'bar' + } + }, + { + name: 'should process conditional block on top level (follow \'then\' block)', + contextData: { + deviceVersion: '11.6' + }, + propertyData: { + if: { + deviceVersionGreaterOrEqual: '11.0' + }, + then: { + foo: 'bar' + }, + else: { + foo: 'else' + } + }, + expectedData: { + foo: 'bar' + } + }, + { + name: 'should process conditional block on top level (follow \'else\' block)', + contextData: { + deviceVersion: '11.6' + }, + propertyData: { + if: { + deviceVersionGreaterOrEqual: '14.0' + }, + then: { + foo: 'bar' + }, + else: { + foo: 'else' + } + }, + expectedData: { + foo: 'else' + } + }, + { + name: 'should process nested conditional blocks (example 1)', + contextData: { + deviceVersion: '11.6' + }, + propertyData: { + if: { + deviceVersionGreaterOrEqual: '11.0' + }, + then: { + if: { + deviceVersionGreaterOrEqual: '11.1' + }, + then: { + foo: 'bar' + } + }, + else: { + foo: 'else' + } + }, + expectedData: { + foo: 'bar' + } + }, + { + name: 'should process nested conditional blocks (example 2)', + contextData: { + deviceVersion: '11.6' + }, + propertyData: { + if: { + deviceVersionGreaterOrEqual: '11.0' + }, + then: { + obj2: [ + { + if: { + deviceVersionGreaterOrEqual: '11.1' + }, + then: { + foo: 'bar' + } + } + ] + }, + else: { + foo: 'else' + } + }, + expectedData: { + obj2: [ + { + foo: 'bar' + } + ] + } + }, + { + name: 'should process nested conditional blocks (example 3)', + contextData: { + deviceVersion: '11.6' + }, + propertyData: { + obj1: { + obj2: [ + { + if: { + deviceVersionGreaterOrEqual: '11.0' + }, + then: { + obj3: [ + { + if: { + deviceVersionGreaterOrEqual: '11.1' + }, + then: { + foo: 'bar' + }, + else: { + foo: 'else' + } + } + ] + }, + else: { + foo: 'else' + } + } + ] + } + }, + expectedData: { + obj1: { + obj2: [ + { + obj3: [ + { + foo: 'bar' + } + ] + } + ] + } + } + } + ], + _renderTemplate: [ + { + name: 'should return string as is when no template', + contextData: {}, + propertyData: { + key: 'something' + }, + expectedData: { + key: 'something' + } + }, + { + name: 'should not fail when key not in context', + contextData: {}, + propertyData: { + key: '{{ something }}' + }, + expectedData: { + key: '' + } + }, + { + name: 'should not fail when key is null', + contextData: {}, + propertyData: { + key: null + }, + expectedData: { + key: null + } + }, + { + name: 'should replace tokens with data from context', + contextData: { + someKey: 'something' + }, + propertyData: { + key: '{{ someKey }}' + }, + expectedData: { + key: 'something' + } + }, + { + name: 'should replace tokens with data from context (any depth)', + contextData: { + someKey: 'something' + }, + propertyData: { + key: '{{ someKey }}', + intKey: 10, + boolKey: true, + obj1: { + arr1: [ + { + obj2: { + key: '{{ someKey }}' + } + } + ] + }, + obj2: { + key: '{{ someKey }}' + } + }, + expectedData: { + key: 'something', + intKey: 10, + boolKey: true, + obj1: { + arr1: [ + { + obj2: { + key: 'something' + } + } + ] + }, + obj2: { + key: 'something' + } + } + } + ], + renderProperty: [ + { + name: 'should render property without template and conditionals', + contextData: {}, + propertyData: { + obj1: { + obj2: { + key: true + } + }, + foo: 'bar' + }, + expectedData: { + obj1: { + obj2: { + key: true + } + }, + foo: 'bar' + } + }, + { + name: 'should render property with template and conditionals', + contextData: { + version13: '13.0', + version14: '14.0', + deviceVersion: '13.1' + }, + propertyData: { + obj1: { + template1: '{{ version13 }}, {{ version14 }}' + }, + obj2: { + if: { + deviceVersionGreaterOrEqual: '{{ version13 }}' + }, + then: { + foo: 'bar', + if: { + deviceVersionGreaterOrEqual: '{{ version14 }}' + }, + then: { + foo: 'bar2' + } + }, + else: { + if: { + deviceVersionGreaterOrEqual: '{{ version14 }}' + }, + then: { + foo: 'bar2' + } + } + } + }, + expectedData: { + obj1: { + template1: '13.0, 14.0' + }, + obj2: { + foo: 'bar' + } + } + } + ] +}; diff --git a/test/unit/utilTests.js b/test/unit/utilTests.js index dd9be64d..167e095b 100644 --- a/test/unit/utilTests.js +++ b/test/unit/utilTests.js @@ -8,494 +8,736 @@ 'use strict'; -const assert = require('assert'); -const os = require('os'); +/* eslint-disable import/order */ + +require('./shared/restoreCache')(); + +const chai = require('chai'); +const chaiAsPromised = require('chai-as-promised'); const fs = require('fs'); +const net = require('net'); +const nock = require('nock'); +const os = require('os'); const path = require('path'); +const request = require('request'); +const sinon = require('sinon'); -const constants = require('../../src/lib/constants.js'); +const constants = require('../../src/lib/constants'); +const util = require('../../src/lib/util'); +const testUtil = require('./shared/util'); -/* eslint-disable global-require */ +chai.use(chaiAsPromised); +const assert = chai.assert; describe('Util', () => { - let util; - let request; - - const setupRequestMock = (res, body, mockOpts) => { - mockOpts = mockOpts || {}; - ['get', 'post', 'delete'].forEach((method) => { - request[method] = (opts, cb) => { - cb(mockOpts.err, res, mockOpts.toJSON === false ? body : JSON.stringify(body)); - }; - }); - }; + describe('.start()', () => { + it('should start function on interval', () => assert.isFulfilled( + new Promise((resolve) => { + const intervalID = util.start( + (args) => { + util.stop(intervalID); + assert.strictEqual(args, 'test'); + resolve(); + }, + 'test', + 0.01 + ); + }) + )); + }); - before(() => { - util = require('../../src/lib/util.js'); - request = require('request'); + describe('.update()', () => { + it('should update function\'s interval', () => assert.isFulfilled( + new Promise((resolve) => { + const intervalID = util.start( + () => { + const newIntervalID = util.update( + intervalID, + (args) => { + util.stop(newIntervalID); + assert.strictEqual(args, 'test'); + resolve(); + }, + 'test', + 0.01 + ); + }, + 0.01 + ); + }) + )); }); - after(() => { - Object.keys(require.cache).forEach((key) => { - delete require.cache[key]; + + describe('.stop()', () => { + it('should stop function', () => assert.isFulfilled( + new Promise((resolve) => { + const intervalID = util.start( + (args) => { + util.stop(intervalID); + assert.strictEqual(args, 'test'); + resolve(); + }, + 'test', + 0.01 + ); + }) + )); + }); + + describe('.restOperationResponder()', () => { + it('should complete rest operation', () => { + const mock = { + setStatusCode: (code) => { + mock.code = code; + }, + setBody: (body) => { + mock.body = body; + }, + complete: () => { + assert.strictEqual(mock.code, 200); + assert.strictEqual(mock.body, 'body'); + } + }; + util.restOperationResponder(mock, 200, 'body'); }); }); - it('should stringify object', () => { - const obj = { - foo: 'bar' - }; - const newObj = util.stringify(obj); - assert.notStrictEqual(newObj.indexOf('{"foo":"bar"}'), -1); + describe('.getDeclarationByName()', () => { + it('should get object by name', () => { + const obj = { + my_item: { + class: 'Consumer' + } + }; + const formattedObj = util.formatConfig(obj); + assert.deepStrictEqual( + util.getDeclarationByName(formattedObj, 'Consumer', 'my_item'), + { + class: 'Consumer' + } + ); + }); }); - it('should stringify object', () => { - const obj = { - name: 'foo' - }; - const stringifiedObj = util.stringify(obj); - assert.deepEqual(stringifiedObj, '{"name":"foo"}'); + describe('.stringify()', () => { + it('should stringify object', () => { + assert.strictEqual( + util.stringify({ foo: 'bar' }), + JSON.stringify({ foo: 'bar' }) + ); + }); + + it('should return non-object as is', () => { + assert.strictEqual(util.stringify(1), 1); + }); + + it('should silently continue when unable to stringify object', () => { + const obj = { a: 1 }; + obj.b = obj; + util.stringify(obj); + }); }); - it('should format data by class', () => { - const obj = { - my_item: { - class: 'Consumer' - } - }; - const expectedObj = { - Consumer: { + describe('.formatDataByClass()', () => { + it('should return empty object when no declaration', () => { + assert.deepStrictEqual(util.formatDataByClass(), {}); + }); + + it('should not fail on null', () => { + // typeof null === 'object' + assert.deepStrictEqual(util.formatDataByClass(), {}); + }); + + it('should format data by class', () => { + const obj = { my_item: { class: 'Consumer' } - } - }; - const formattedObj = util.formatDataByClass(obj); - assert.deepEqual(formattedObj, expectedObj); - }); + }; + const expectedObj = { + Consumer: { + my_item: { + class: 'Consumer' + } + } + }; + const formattedObj = util.formatDataByClass(obj); + assert.deepStrictEqual(formattedObj, expectedObj); + }); - it('should format data by class', () => { - const obj = { - my_item: { - class: 'Consumer' - } - }; - const expectedObj = { - Consumer: { + it('should format data by class', () => { + const obj = { my_item: { class: 'Consumer' } - } - }; - const formattedObj = util.formatDataByClass(obj); - assert.deepEqual(formattedObj, expectedObj); + }; + const expectedObj = { + Consumer: { + my_item: { + class: 'Consumer' + } + } + }; + const formattedObj = util.formatDataByClass(obj); + assert.deepStrictEqual(formattedObj, expectedObj); + }); }); - it('should format config', () => { - const obj = { - my_item: { - class: 'Consumer' - } - }; - const expectedObj = { - Consumer: { + describe('.formatConfig()', () => { + it('should format config', () => { + const obj = { my_item: { class: 'Consumer' } - } - }; - const formattedObj = util.formatConfig(obj); - assert.deepEqual(formattedObj, expectedObj); + }; + const expectedObj = { + Consumer: { + my_item: { + class: 'Consumer' + } + } + }; + const formattedObj = util.formatConfig(obj); + assert.deepStrictEqual(formattedObj, expectedObj); + }); }); - it('should make request', () => { - const mockRes = { statusCode: 200, statusMessage: 'message' }; - const mockBody = { key: 'value' }; - setupRequestMock(mockRes, mockBody); + describe('.makeRequest()', () => { + afterEach(() => { + testUtil.checkNockActiveMocks(nock, assert); + sinon.restore(); + nock.cleanAll(); + }); - return util.makeRequest('example.com', '/', {}) - .then((data) => { - assert.deepEqual(data, mockBody); - return Promise.resolve(); + it('should make request with non-defaults', () => { + nock('https://example.com:443', { + reqheaders: { + 'User-Agent': /f5-telemetry/, + CustomHeader: 'CustomValue' + } }) - .catch(err => Promise.reject(err)); - }); - - it('should fail request', () => { - const mockRes = { statusCode: 404, statusMessage: 'message' }; - const mockBody = { key: 'value' }; - setupRequestMock(mockRes, mockBody); + .post('/') + .reply(200, { key: 'value' }); - return util.makeRequest('example.com', '/', {}) - .then(() => { - assert.fail('Should throw an error'); - }) - .catch((err) => { - if (err.code === 'ERR_ASSERTION') return Promise.reject(err); - if (/Bad status code/.test(err)) return Promise.resolve(); - assert.fail(err); - return Promise.reject(err); + const originGet = request.get; + sinon.stub(request, 'get').callsFake((opts, cb) => { + assert.strictEqual(opts.strictSSL, false); + originGet(opts, cb); }); - }); - it('should fail request with error', () => { - const mockRes = { statusCode: 404, statusMessage: 'message' }; - const mockBody = { key: 'value' }; - setupRequestMock(mockRes, mockBody, { err: new Error('test error') }); + const opts = { + port: 443, + protocol: 'https', + method: 'POST', + headers: { + CustomHeader: 'CustomValue' + }, + allowSelfSignedCert: true + }; + return assert.becomes( + util.makeRequest('example.com', opts), + { key: 'value' } + ); + }); - return util.makeRequest('example.com', '/', {}) - .then(() => { - assert.fail('Should throw an error'); + it('should make request with defaults (response code 200)', () => { + nock('http://example.com', { + reqheaders: { + 'User-Agent': /f5-telemetry/ + } }) - .catch((err) => { - if (err.code === 'ERR_ASSERTION') return Promise.reject(err); - if (/HTTP error/.test(err)) return Promise.resolve(); - assert.fail(err); - return Promise.reject(err); + .get('/') + .reply(200, { key: 'value' }); + + const originGet = request.get; + sinon.stub(request, 'get').callsFake((opts, cb) => { + assert.strictEqual(opts.strictSSL, true); + originGet(opts, cb); }); - }); + return assert.becomes( + util.makeRequest('example.com'), + { key: 'value' } + ); + }); - it('should return non-JSON body', () => { - const mockRes = { statusCode: 200, statusMessage: 'message' }; - const mockBody = '{someInvalidJSONData'; - setupRequestMock(mockRes, mockBody, { toJSON: false }); + it('should make request (response code 400)', () => { + nock('http://example.com') + .get('/') + .reply(400, { key: 'value' }); - return util.makeRequest('example.com', '/', {}) - .then((body) => { - assert.strictEqual(body, mockBody); - }) - .catch(err => Promise.reject(err)); - }); + return assert.becomes( + util.makeRequest('example.com', { expectedResponseCode: 400 }), + { key: 'value' } + ); + }); - it('should continue on error code for request', () => { - const mockRes = { statusCode: 404, statusMessage: 'message' }; - const mockBody = { key: 'value' }; - setupRequestMock(mockRes, mockBody); + it('should fail on response with code 400', () => { + nock('http://example.com') + .get('/') + .reply(400, { key: 'value' }); - return util.makeRequest('example.com', '/', { continueOnErrorCode: true }) - .then(() => Promise.resolve()) - .catch(err => Promise.reject(err)); - }); + return assert.isRejected( + util.makeRequest('example.com'), + /Bad status code/ + ); + }); - it('should base64 decode', () => { - const string = 'f5string'; - const encString = Buffer.from(string, 'ascii').toString('base64'); + it('should continue on response with code 400 (expected 200 by default)', () => { + nock('http://example.com') + .get('/') + .reply(400, { key: 'value' }); - const decString = util.base64('decode', encString); - assert.strictEqual(decString, string); - }); + return assert.becomes( + util.makeRequest('example.com', { continueOnErrorCode: true }), + { key: 'value' } + ); + }); - it('should error on incorrect base64 action', () => { - try { - util.base64('someaction', 'foo'); - assert.fail('Error expected'); - } catch (err) { - const msg = err.message || err; - assert.notStrictEqual(msg.indexOf('Unsupported action'), -1); - } - }); + it('should fail request with error', () => { + nock('http://example.com') + .get('/') + .replyWithError('error message'); - it('should fail network check', () => { - const host = 'localhost'; - const port = 0; + return assert.isRejected( + util.makeRequest('example.com'), + /HTTP error:.*error message.*/ + ); + }); - return util.networkCheck(host, port) - .then(() => { - assert.fail('Should throw an error'); - }) - .catch((err) => { - if (err.code === 'ERR_ASSERTION') return Promise.reject(err); - if (/networkCheck:/.test(err)) return Promise.resolve(); - assert.fail(err); - return Promise.reject(err); + it('should return non-JSON body', () => { + nock('http://example.com') + .get('/') + .reply(200, '{someInvalidJSONData'); + + return assert.becomes( + util.makeRequest('example.com'), + '{someInvalidJSONData' + ); + }); + + it('should return raw response data (as Buffer)', () => { + nock('http://example.com') + .get('/') + .reply(200, '{"someValidJSONData": 1}'); + + return assert.becomes( + util.makeRequest('example.com', { rawResponseBody: true }), + Buffer.from('{"someValidJSONData": 1}') + ); + }); + + it('should return parsed data', () => { + nock('http://example.com') + .get('/') + .reply(200, '{"someValidJSONData": 1}'); + + return assert.becomes( + util.makeRequest('example.com'), + { someValidJSONData: 1 } + ); + }); + + it('should convert request data to string', () => { + sinon.stub(request, 'get').callsFake((opts, cb) => { + assert.strictEqual(typeof opts.body, 'string'); + cb(null, { statusCode: 200, statusMessage: 'message' }, {}); }); - }); + return assert.isFulfilled(util.makeRequest('example.com', { body: { key: 'value' } })); + }); - it('should compare version strings', () => { - assert.throws( - () => { - util.compareVersionStrings('14.0', '<>', '14.0'); - }, - (err) => { - if ((err instanceof Error) && /Invalid comparator/.test(err)) { - return true; - } - return false; - }, - 'unexpected error' - ); - assert.strictEqual(util.compareVersionStrings('14.1.0', '>', '14.0'), true); - assert.strictEqual(util.compareVersionStrings('14.0', '>', '14.1.0'), false); - assert.strictEqual(util.compareVersionStrings('14.0', '<', '14.1.0'), true); - assert.strictEqual(util.compareVersionStrings('14.1', '>', '14.0'), true); - assert.strictEqual(util.compareVersionStrings('14.0', '>', '14.1'), false); - assert.strictEqual(util.compareVersionStrings('14.0', '<', '14.1'), true); - assert.strictEqual(util.compareVersionStrings('14.0', '==', '14.0'), true); - assert.strictEqual(util.compareVersionStrings('14.0', '===', '14.0'), true); - assert.strictEqual(util.compareVersionStrings('14.0', '<', '14.0'), false); - assert.strictEqual(util.compareVersionStrings('14.0', '<=', '14.0'), true); - assert.strictEqual(util.compareVersionStrings('14.0', '>', '14.0'), false); - assert.strictEqual(util.compareVersionStrings('14.0', '>=', '14.0'), true); - assert.strictEqual(util.compareVersionStrings('14.0', '!=', '14.0'), false); - assert.strictEqual(util.compareVersionStrings('14.0', '!==', '14.0'), false); - assert.strictEqual(util.compareVersionStrings('14.0', '==', '15.0'), false); - assert.strictEqual(util.compareVersionStrings('15.0', '==', '14.0'), false); - }); + it('should return data and response object', () => { + nock('http://example.com') + .get('/') + .reply(200, '{"someValidJSONData": 1}'); + + return assert.isFulfilled( + util.makeRequest('example.com', { includeResponseObject: true }) + .then((resp) => { + assert.strictEqual(resp.length, 2, 'should return array of 2'); + assert.deepStrictEqual(resp[0], { someValidJSONData: 1 }); + }) + ); + }); - it('should return response as-is (Buffer) when requested', () => { - const mockRes = { statusCode: 200, statusMessage: 'message' }; - const mockBody = Buffer.from('something'); - request.get = (opts, cb) => { - cb(null, mockRes, mockBody); - }; + it('should fail when unable to build URI', () => { + assert.throws( + () => util.makeRequest({}), + /makeRequest: no fullURI or host provided/ + ); + }); - const opts = { - method: 'GET', - rawResponseBody: true - }; - return util.makeRequest(constants.LOCAL_HOST, '/', opts) - .then((res) => { - assert.ok(mockBody.equals(res), 'should equal to origin Buffer'); + it('should fail when no arguments passed to function', () => { + assert.throws( + () => util.makeRequest(), + /makeRequest: no arguments were passed to function/ + ); + }); + + it('should have no custom TS options in request options', () => { + const tsReqOptions = [ + 'allowSelfSignedCert', + 'continueOnErrorCode', + 'expectedResponseCode', + 'fullURI', + 'includeResponseObject', + 'json', + 'port', + 'protocol', + 'rawResponseBody' + ]; + sinon.stub(request, 'get').callsFake((opts, cb) => { + const optsKeys = Object.keys(opts); + tsReqOptions.forEach((tsOptKey) => { + assert.ok(optsKeys.indexOf(tsOptKey) === -1, `Found '${tsOptKey}' in request options`); + }); + cb(null, { statusCode: 200, statusMessage: 'message' }, {}); }); - }); - it('should correctly process different args', () => { - const expectedURI = 'someproto://somehost:someport/someuri'; - const testData = [ + return assert.isFulfilled( + util.makeRequest('host', { protocol: 'http', port: 456, continueOnErrorCode: true }) + ); + }); + + [ { - args: [{ fullURI: expectedURI }], - expected: expectedURI + name: 'fullURI only', + args: [{ fullURI: 'someproto://somehost:someport/someuri' }], + expected: 'someproto://somehost:someport/someuri' }, { + name: 'host, uri, protocol and port', args: ['somehost', '/someuri', { protocol: 'someproto', port: 'someport' }], - expected: expectedURI + expected: 'someproto://somehost:someport/someuri' }, { + name: 'host, protocol and port', args: ['somehost', { protocol: 'someproto', port: 'someport' }], expected: 'someproto://somehost:someport' }, { + name: 'host only', args: ['somehost'], expected: 'http://somehost:80' } - ]; + ].forEach((testConf) => { + it(`should correctly process set of args: ${testConf.name}`, () => { + sinon.stub(request, 'get').callsFake((opts, cb) => { + assert.strictEqual(opts.uri, testConf.expected); + cb(null, { statusCode: 200, statusMessage: 'message' }, {}); + }); + /* eslint-disable-next-line prefer-spread */ + return assert.isFulfilled(util.makeRequest.apply(util, testConf.args)); + }); + }); + }); - let idx = 0; - const mockRes = { statusCode: 200, statusMessage: 'message' }; - request.get = (opts, cb) => { - assert.strictEqual(opts.uri, testData[idx].expected); - cb(null, mockRes, {}); - }; + describe('.base64()', () => { + it('should base64 decode', () => { + assert.strictEqual( + util.base64('decode', Buffer.from('f5string', 'ascii').toString('base64')), + 'f5string' + ); + }); - function _test() { - return util.makeRequest.apply(null, testData[idx].args) - .then(() => { - idx += 1; - if (idx < testData.length) { - return _test(); - } - return Promise.resolve(); - }); - } - return _test(); + it('should error on incorrect base64 action', () => { + assert.throws( + () => util.base64('someaction', 'foo'), + /Unsupported action/ + ); + }); }); - it('should fail when unable to build URI', () => { - assert.throws( - () => { - util.makeRequest({}); - }, - (err) => { - if ((err instanceof Error) && /makeRequest: No fullURI or host provided/.test(err)) { - return true; + describe('.networkCheck()', () => { + let socketMock; + + beforeEach(() => { + socketMock = { + events: {}, + end: () => socketMock.events.end(), + on: (event, cb) => { + socketMock.events[event] = cb; + return socketMock; + }, + connect: () => { + Promise.resolve() + .then(() => { + if (socketMock.testCallback) { + socketMock.testCallback(socketMock); + } else if (socketMock.events.connect) { + socketMock.events.connect(); + } + }); + return socketMock; } - return false; - }, - 'unexpected error' - ); - }); + }; + sinon.stub(net, 'createConnection').callsFake(() => socketMock.connect()); + }); - it('should have no custom TS options in request options', () => { - const tsReqOptions = ['rawResponseBody', 'continueOnErrorCode', - 'expectedResponseCode', 'includeResponseObject', - 'port', 'protocol', 'fullURI', 'allowSelfSignedCert', 'returnRequestOnly' - ]; - - const mockRes = { statusCode: 200, statusMessage: 'message' }; - request.get = (opts, cb) => { - const optsKeys = Object.keys(opts); - tsReqOptions.forEach((tsOptKey) => { - assert.ok(optsKeys.indexOf(tsOptKey) === -1, `Found '${tsOptKey} in request options`); - }); - cb(null, mockRes, {}); - }; + afterEach(() => { + sinon.restore(); + }); - return util.makeRequest('host', { protocol: 'http', port: 456, continueOnErrorCode: true }); - }); + it('should fail network check (real connection to localhost:0)', () => { + // force 'restore' to use real net.createConnection + sinon.restore(); + return assert.isRejected( + util.networkCheck('localhost', 0), + /networkCheck/ + ); + }); + + it('should check that host:port is reachable', () => assert.isFulfilled( + util.networkCheck('localhost', 0, { period: 10 }) + )); + + it('should fail to check that host:port is reachable', () => { + socketMock.testCallback = () => { + socketMock.events.error(new Error('some error')); + }; + return assert.isRejected( + util.networkCheck('localhost', 0, { period: 10 }), + /networkCheck.*some error/ + ); + }); + + it('should fail on timeout', () => { + socketMock.testCallback = () => {}; + return assert.isRejected( + util.networkCheck('localhost', 0, { timeout: 10, period: 2 }), + /networkCheck.*timeout exceeded/ + ); + }); - it('should copy object', () => { - const src = { schedule: { frequency: 'daily', time: { start: '04:20', end: '6:00' } } }; - assert.deepStrictEqual(util.deepCopy(src), src); + it('should use timeout as period', () => { + socketMock.testCallback = () => {}; + return assert.isRejected( + util.networkCheck('localhost', 0, { timeout: 10, period: 100 }), + /networkCheck.*timeout exceeded/ + ); + }).timeout(30); }); - it('should pass empty object check', () => { - assert.strictEqual(util.isObjectEmpty({}), true, 'empty object'); - assert.strictEqual(util.isObjectEmpty(null), true, 'null'); - assert.strictEqual(util.isObjectEmpty(undefined), true, 'undefined'); - assert.strictEqual(util.isObjectEmpty([]), true, 'empty array'); - assert.strictEqual(util.isObjectEmpty(''), true, 'empty string'); - assert.strictEqual(util.isObjectEmpty(0), true, 'number'); - assert.strictEqual(util.isObjectEmpty({ 1: 1, 2: 2 }), false, 'object'); - assert.strictEqual(util.isObjectEmpty([1, 2, 3]), false, 'array'); + describe('.compareVersionStrings()', () => { + it('should compare version strings', () => { + assert.throws( + () => util.compareVersionStrings('14.0', '<>', '14.0'), + /Invalid comparator/ + ); + assert.strictEqual(util.compareVersionStrings('14.1.0', '>', '14.0'), true); + assert.strictEqual(util.compareVersionStrings('14.0', '>', '14.1.0'), false); + assert.strictEqual(util.compareVersionStrings('14.0', '<', '14.1.0'), true); + assert.strictEqual(util.compareVersionStrings('14.1', '>', '14.0'), true); + assert.strictEqual(util.compareVersionStrings('14.0', '>', '14.1'), false); + assert.strictEqual(util.compareVersionStrings('14.0', '<', '14.1'), true); + assert.strictEqual(util.compareVersionStrings('14.0', '==', '14.0'), true); + assert.strictEqual(util.compareVersionStrings('14.0', '=', '14.0'), true); + assert.strictEqual(util.compareVersionStrings('14.0', '===', '14.0'), true); + assert.strictEqual(util.compareVersionStrings('14.0', '<', '14.0'), false); + assert.strictEqual(util.compareVersionStrings('14.0', '<=', '14.0'), true); + assert.strictEqual(util.compareVersionStrings('14.0', '>', '14.0'), false); + assert.strictEqual(util.compareVersionStrings('14.0', '>=', '14.0'), true); + assert.strictEqual(util.compareVersionStrings('14.0', '!=', '14.0'), false); + assert.strictEqual(util.compareVersionStrings('14.0', '!==', '14.0'), false); + assert.strictEqual(util.compareVersionStrings('14.0', '==', '15.0'), false); + assert.strictEqual(util.compareVersionStrings('15.0', '==', '14.0'), false); + }); }); - it('should retry at least once', () => { - let tries = 0; - // first call + re-try = 2 - const expectedTries = 2; + describe('.deepCopy()', () => { + it('should copy object', () => { + const src = { schedule: { frequency: 'daily', time: { start: '04:20', end: '6:00' } } }; + assert.deepStrictEqual(util.deepCopy(src), src); + }); + }); - const promiseFunc = () => { - tries += 1; - return Promise.reject(new Error('expected error')); - }; + describe('assignDefaults', () => { + it('should assign defaults', () => { + assert.deepStrictEqual(util.assignDefaults({}, {}), {}); + assert.deepStrictEqual(util.assignDefaults(null, { a: 1 }), { a: 1 }); + assert.deepStrictEqual(util.assignDefaults(undefined, { a: 1 }), { a: 1 }); + assert.deepStrictEqual(util.assignDefaults({}, { a: 1 }), { a: 1 }); + assert.deepStrictEqual(util.assignDefaults({ a: 1 }, { a: 2 }), { a: 1 }, 'should not override existing property'); + assert.deepStrictEqual(util.assignDefaults({ a: undefined }, { a: 2 }), { a: undefined }, 'should preserve "undefined" as values'); + }); - return util.retryPromise(promiseFunc) - .catch((err) => { - // in total should be 2 tries - 1 call + 1 re-try - assert.strictEqual(tries, expectedTries); - assert.ok(/expected error/.test(err)); - }); + it('should return same object', () => { + const src = { a: 1 }; + const dst = util.assignDefaults(src, {}); + src.b = 2; + assert.deepStrictEqual(dst, src, 'should return same object and not copy of it'); + }); }); - it('should retry rejected promise', () => { - let tries = 0; - const maxTries = 3; - const expectedTries = maxTries + 1; + describe('.isObjectEmpty()', () => { + it('should pass empty object check', () => { + assert.strictEqual(util.isObjectEmpty({}), true, 'empty object'); + assert.strictEqual(util.isObjectEmpty(null), true, 'null'); + assert.strictEqual(util.isObjectEmpty(undefined), true, 'undefined'); + assert.strictEqual(util.isObjectEmpty([]), true, 'empty array'); + assert.strictEqual(util.isObjectEmpty(''), true, 'empty string'); + assert.strictEqual(util.isObjectEmpty(0), true, 'number'); + assert.strictEqual(util.isObjectEmpty({ 1: 1, 2: 2 }), false, 'object'); + assert.strictEqual(util.isObjectEmpty([1, 2, 3]), false, 'array'); + }); + }); - const promiseFunc = () => { - tries += 1; - return Promise.reject(new Error('expected error')); - }; + describe('.retryPromise()', () => { + it('should retry at least once', () => { + let tries = 0; + // first call + re-try = 2 + const expectedTries = 2; - return util.retryPromise(promiseFunc, { maxTries }) - .catch((err) => { - // in total should be 4 tries - 1 call + 3 re-try - assert.strictEqual(tries, expectedTries); - assert.ok(/expected error/.test(err)); - }); - }); + const promiseFunc = () => { + tries += 1; + return Promise.reject(new Error('expected error')); + }; - it('should call callback on retry', () => { - let callbackFlag = false; - let callbackErrFlag = false; - let tries = 0; - let cbTries = 0; - const maxTries = 3; - const expectedTries = maxTries + 1; - - const callback = (err) => { - cbTries += 1; - callbackErrFlag = /expected error/.test(err); - callbackFlag = true; - return true; - }; - const promiseFunc = () => { - tries += 1; - return Promise.reject(new Error('expected error')); - }; + return util.retryPromise(promiseFunc) + .catch((err) => { + // in total should be 2 tries - 1 call + 1 re-try + assert.strictEqual(tries, expectedTries); + assert.ok(/expected error/.test(err)); + }); + }); - return util.retryPromise(promiseFunc, { maxTries, callback }) - .catch((err) => { - // in total should be 4 tries - 1 call + 3 re-try - assert.strictEqual(tries, expectedTries); - assert.strictEqual(cbTries, maxTries); - assert.ok(/expected error/.test(err)); - assert.ok(callbackErrFlag); - assert.ok(callbackFlag); - }); - }); + it('should retry rejected promise', () => { + let tries = 0; + const maxTries = 3; + const expectedTries = maxTries + 1; - it('should stop retry on success', () => { - let tries = 0; - const maxTries = 3; - const expectedTries = 2; + const promiseFunc = () => { + tries += 1; + return Promise.reject(new Error('expected error')); + }; - const promiseFunc = () => { - tries += 1; - if (tries === expectedTries) { - return Promise.resolve('success'); - } - return Promise.reject(new Error('expected error')); - }; + return util.retryPromise(promiseFunc, { maxTries }) + .catch((err) => { + // in total should be 4 tries - 1 call + 3 re-try + assert.strictEqual(tries, expectedTries); + assert.ok(/expected error/.test(err)); + }); + }); - return util.retryPromise(promiseFunc, { maxTries }) - .then((data) => { - assert.strictEqual(tries, expectedTries); - assert.strictEqual(data, 'success'); - }); - }); + it('should call callback on retry', () => { + let callbackFlag = false; + let callbackErrFlag = false; + let tries = 0; + let cbTries = 0; + const maxTries = 3; + const expectedTries = maxTries + 1; + + const callback = (err) => { + cbTries += 1; + callbackErrFlag = /expected error/.test(err); + callbackFlag = true; + return true; + }; + const promiseFunc = () => { + tries += 1; + return Promise.reject(new Error('expected error')); + }; - it('should retry with delay', () => { - const timestamps = []; - const maxTries = 3; - const expectedTries = maxTries + 1; - const delay = 200; + return util.retryPromise(promiseFunc, { maxTries, callback }) + .catch((err) => { + // in total should be 4 tries - 1 call + 3 re-try + assert.strictEqual(tries, expectedTries); + assert.strictEqual(cbTries, maxTries); + assert.ok(/expected error/.test(err)); + assert.ok(callbackErrFlag); + assert.ok(callbackFlag); + }); + }); - const promiseFunc = () => { - timestamps.push(Date.now()); - return Promise.reject(new Error('expected error')); - }; + it('should stop retry on success', () => { + let tries = 0; + const maxTries = 3; + const expectedTries = 2; - return util.retryPromise(promiseFunc, { maxTries, delay }) - .catch((err) => { - assert.ok(/expected error/.test(err)); - assert.ok(timestamps.length === expectedTries, - `Expected ${expectedTries} timestamps, got ${timestamps.length}`); - - for (let i = 1; i < timestamps.length; i += 1) { - const actualDelay = timestamps[i] - timestamps[i - 1]; - // sometimes it is less than expected - assert.ok(actualDelay >= delay * 0.9, - `Actual delay (${actualDelay}) is less than expected (${delay})`); + const promiseFunc = () => { + tries += 1; + if (tries === expectedTries) { + return Promise.resolve('success'); } - }); - }).timeout(2000); - - it('should retry first time without backoff', () => { - const timestamps = []; - const maxTries = 3; - const expectedTries = maxTries + 1; - const delay = 200; - const backoff = 100; - - const promiseFunc = () => { - timestamps.push(Date.now()); - return Promise.reject(new Error('expected error')); - }; + return Promise.reject(new Error('expected error')); + }; - return util.retryPromise(promiseFunc, { maxTries, delay, backoff }) - .catch((err) => { - assert.ok(/expected error/.test(err)); - assert.ok(timestamps.length === expectedTries, - `Expected ${expectedTries} timestamps, got ${timestamps.length}`); - - for (let i = 1; i < timestamps.length; i += 1) { - const actualDelay = timestamps[i] - timestamps[i - 1]; - let expectedDelay = delay; - // first attempt should be without backoff factor - if (i > 1) { - /* eslint-disable no-restricted-properties */ - expectedDelay += backoff * Math.pow(2, i - 1); + return util.retryPromise(promiseFunc, { maxTries }) + .then((data) => { + assert.strictEqual(tries, expectedTries); + assert.strictEqual(data, 'success'); + }); + }); + + it('should retry with delay', () => { + const timestamps = []; + const maxTries = 3; + const expectedTries = maxTries + 1; + const delay = 200; + + const promiseFunc = () => { + timestamps.push(Date.now()); + return Promise.reject(new Error('expected error')); + }; + + return util.retryPromise(promiseFunc, { maxTries, delay }) + .catch((err) => { + assert.ok(/expected error/.test(err)); + assert.ok(timestamps.length === expectedTries, + `Expected ${expectedTries} timestamps, got ${timestamps.length}`); + + for (let i = 1; i < timestamps.length; i += 1) { + const actualDelay = timestamps[i] - timestamps[i - 1]; + // sometimes it is less than expected + assert.ok(actualDelay >= delay * 0.9, + `Actual delay (${actualDelay}) is less than expected (${delay})`); } - assert.ok(actualDelay >= expectedDelay * 0.9, - `Actual delay (${actualDelay}) is less than expected (${expectedDelay})`); - } - }); - }).timeout(10000); + }); + }).timeout(2000); + + it('should retry first time without backoff', () => { + const timestamps = []; + const maxTries = 3; + const expectedTries = maxTries + 1; + const delay = 200; + const backoff = 100; + + const promiseFunc = () => { + timestamps.push(Date.now()); + return Promise.reject(new Error('expected error')); + }; + + return util.retryPromise(promiseFunc, { maxTries, delay, backoff }) + .catch((err) => { + assert.ok(/expected error/.test(err)); + assert.ok(timestamps.length === expectedTries, + `Expected ${expectedTries} timestamps, got ${timestamps.length}`); + + for (let i = 1; i < timestamps.length; i += 1) { + const actualDelay = timestamps[i] - timestamps[i - 1]; + let expectedDelay = delay; + // first attempt should be without backoff factor + if (i > 1) { + /* eslint-disable no-restricted-properties */ + expectedDelay += backoff * Math.pow(2, i - 1); + } + assert.ok(actualDelay >= expectedDelay * 0.9, + `Actual delay (${actualDelay}) is less than expected (${expectedDelay})`); + } + }); + }).timeout(10000); + }); + + describe('.getConsumerClasses()', () => { + it('should fail when no declaration', () => { + assert.throws( + () => util.getConsumerClasses(), + /No declaration was provided for consumer counting/ + ); + }); - describe('.getConsumerClasses', () => { it('should return empty consumer object', () => { const result = util.getConsumerClasses({}); - assert.deepEqual(result, { consumers: {} }); + assert.deepStrictEqual(result, { consumers: {} }); }); it('should return object with count of consumer classes', () => { @@ -548,309 +790,283 @@ describe('Util', () => { } }; const result = util.getConsumerClasses(declaration); - assert.deepEqual(result, expected); + assert.deepStrictEqual(result, expected); }); }); - it('should return random number from range', () => { - const left = -5; - const right = 5; - - for (let i = 0; i < 100; i += 1) { - const randNumber = util.getRandomArbitrary(left, right); - assert.ok(left <= randNumber && randNumber <= right, `${randNumber} should be in range ${left}:${right}`); - } - }); -}); - -describe('validate renameKeys function', () => { - let util; + describe('.getRandomArbitrary()', () => { + it('should return random number from range', () => { + const left = -5; + const right = 5; - before(() => { - util = require('../../src/lib/util.js'); + for (let i = 0; i < 100; i += 1) { + const randNumber = util.getRandomArbitrary(left, right); + assert.ok(left <= randNumber && randNumber <= right, `${randNumber} should be in range ${left}:${right}`); + } + }); }); - it('should rename using regex literal', () => { - const target = { - brightredtomato: { - a: 1, - redcar: { - b: 2, - paintitred: { - c: 3 + describe('.renameKeys()', () => { + it('should rename using regex literal', () => { + const target = { + brightredtomato: { + a: 1, + redcar: { + b: 2, + paintitred: { + c: 3 + } } } - } - }; - util.renameKeys(target, /red/, 'green'); - - assert.strictEqual(Object.keys(target).length, 1); - assert.strictEqual(Object.keys(target.brightgreentomato).length, 2); - assert.strictEqual(target.brightgreentomato.a, 1); - assert.strictEqual(Object.keys(target.brightgreentomato.greencar).length, 2); - assert.strictEqual(target.brightgreentomato.greencar.b, 2); - assert.strictEqual(Object.keys(target.brightgreentomato.greencar.paintitgreen).length, 1); - assert.strictEqual(target.brightgreentomato.greencar.paintitgreen.c, 3); - }); - - it('regex flag - first instance only', () => { - const target = { - brightredredredtomato: { a: 1 } - }; - util.renameKeys(target, /red/, 'green'); - - assert.strictEqual(Object.keys(target).length, 1); - assert.strictEqual(target.brightgreenredredtomato.a, 1); - }); + }; + util.renameKeys(target, /red/, 'green'); + assert.strictEqual(Object.keys(target).length, 1); + assert.strictEqual(Object.keys(target.brightgreentomato).length, 2); + assert.strictEqual(target.brightgreentomato.a, 1); + assert.strictEqual(Object.keys(target.brightgreentomato.greencar).length, 2); + assert.strictEqual(target.brightgreentomato.greencar.b, 2); + assert.strictEqual(Object.keys(target.brightgreentomato.greencar.paintitgreen).length, 1); + assert.strictEqual(target.brightgreentomato.greencar.paintitgreen.c, 3); + }); - it('first instance only, case insensitive', () => { - const target = { - brightRedTomato: { a: 1 } - }; - util.renameKeys(target, /red/i, 'green'); + it('regex flag - first instance only', () => { + const target = { + brightredredredtomato: { a: 1 } + }; + util.renameKeys(target, /red/, 'green'); - assert.strictEqual(Object.keys(target).length, 1); - assert.strictEqual(target.brightgreenTomato.a, 1); - }); + assert.strictEqual(Object.keys(target).length, 1); + assert.strictEqual(target.brightgreenredredtomato.a, 1); + }); - it('globally', () => { - const target = { - brightredredtomato: { a: 1 } - }; - util.renameKeys(target, /red/g, 'green'); + it('first instance only, case insensitive', () => { + const target = { + brightRedTomato: { a: 1 } + }; + util.renameKeys(target, /red/i, 'green'); - assert.strictEqual(Object.keys(target).length, 1); - assert.strictEqual(target.brightgreengreentomato.a, 1); - }); + assert.strictEqual(Object.keys(target).length, 1); + assert.strictEqual(target.brightgreenTomato.a, 1); + }); - it('globally and case insensitive', () => { - const target = { - brightRedrEdreDtomato: { a: 1 } - }; - util.renameKeys(target, /red/ig, 'green'); + it('globally', () => { + const target = { + brightredredtomato: { a: 1 } + }; + util.renameKeys(target, /red/g, 'green'); - assert.strictEqual(Object.keys(target).length, 1); - assert.strictEqual(target.brightgreengreengreentomato.a, 1); - }); + assert.strictEqual(Object.keys(target).length, 1); + assert.strictEqual(target.brightgreengreentomato.a, 1); + }); - it('character group', () => { - const target = { - bearclaw: { a: 1 }, - teardrop: { b: 2 }, - dearjohn: { c: 3 } - }; - util.renameKeys(target, /[bt]ear/, 'jelly'); + it('globally and case insensitive', () => { + const target = { + brightRedrEdreDtomato: { a: 1 } + }; + util.renameKeys(target, /red/ig, 'green'); - assert.strictEqual(Object.keys(target).length, 3); - assert.strictEqual(target.jellyclaw.a, 1); - assert.strictEqual(target.jellydrop.b, 2); - assert.strictEqual(target.dearjohn.c, 3); - }); + assert.strictEqual(Object.keys(target).length, 1); + assert.strictEqual(target.brightgreengreengreentomato.a, 1); + }); - it('negated character group', () => { - const target = { - bearclaw: { a: 1 }, - teardrop: { b: 2 }, - dearjohn: { c: 3 } - }; - util.renameKeys(target, /[^bt]ear/, 'jelly'); + it('character group', () => { + const target = { + bearclaw: { a: 1 }, + teardrop: { b: 2 }, + dearjohn: { c: 3 } + }; + util.renameKeys(target, /[bt]ear/, 'jelly'); - assert.strictEqual(Object.keys(target).length, 3); - assert.strictEqual(target.bearclaw.a, 1); - assert.strictEqual(target.teardrop.b, 2); - assert.strictEqual(target.jellyjohn.c, 3); - }); -}); + assert.strictEqual(Object.keys(target).length, 3); + assert.strictEqual(target.jellyclaw.a, 1); + assert.strictEqual(target.jellydrop.b, 2); + assert.strictEqual(target.dearjohn.c, 3); + }); -// purpose: validate util (tracer) -describe('Util (Tracer)', () => { - let util; - let config; - const tracerFile = `${os.tmpdir()}/telemetry`; // os.tmpdir for windows + linux + it('negated character group', () => { + const target = { + bearclaw: { a: 1 }, + teardrop: { b: 2 }, + dearjohn: { c: 3 } + }; + util.renameKeys(target, /[^bt]ear/, 'jelly'); - before(() => { - util = require('../../src/lib/util.js'); - }); - beforeEach(() => { - config = { - trace: tracerFile - }; - if (fs.existsSync(tracerFile)) { - fs.unlinkSync(tracerFile); - } - }); - after(() => { - Object.keys(require.cache).forEach((key) => { - delete require.cache[key]; + assert.strictEqual(Object.keys(target).length, 3); + assert.strictEqual(target.bearclaw.a, 1); + assert.strictEqual(target.teardrop.b, 2); + assert.strictEqual(target.jellyjohn.c, 3); }); }); - it('should write to tracer', () => { - const msg = 'foobar'; - const tracer = util.tracer.createFromConfig('class', 'obj', config); - let error; - return tracer.write(msg) - .then(() => { - const contents = fs.readFileSync(tracerFile, 'utf8'); - assert.strictEqual(msg, contents); - }) - .catch((err) => { - error = err; - }) - .then(() => { - util.tracer.remove(tracer); // cleanup, otherwise will not exit - if (error) { - return Promise.reject(error); + describe('Tracer', () => { + const tracerDir = `${os.tmpdir()}/telemetry`; // os.tmpdir for windows + linux + const tracerFile = `${tracerDir}/tracerTest`; + let config; + let tracer; + + const emptyDir = (dirPath) => { + fs.readdirSync(dirPath).forEach((item) => { + item = path.join(dirPath, item); + if (fs.statSync(item).isDirectory()) { + emptyDir(item); + fs.rmdirSync(item); + } else { + fs.unlinkSync(item); } - return Promise.resolve(error); }); - }); + }; - it('should accept no data', () => { - const tracer = util.tracer.createFromConfig('class', 'obj', config); - return tracer.write(null); - }); + const removeDir = (dirPath) => { + if (fs.existsSync(dirPath)) { + emptyDir(dirPath); + fs.rmdirSync(dirPath); + } + }; - it('should get existing tracer by the name', () => { - const tracer = util.tracer.createFromConfig('class', 'obj', config); - let tracer2; - let error; + beforeEach(() => { + if (fs.existsSync(tracerDir)) { + emptyDir(tracerDir); + } - return tracer.write('somethings') - .then(() => { - tracer2 = util.tracer.createFromConfig('class', 'obj', config); - return tracer2.write('something3'); - }) - .then(() => { - assert.strictEqual(tracer2.inode, tracer.inode, 'inode should be the sane'); - assert.strictEqual(tracer2.stream.fd, tracer.stream.fd, 'fd should be the same'); - }) - .catch((err) => { - error = err; - }) - .then(() => { - util.tracer.remove(tracer); // cleanup, otherwise will not exit - if (tracer2) { - util.tracer.remove(tracer2); // cleanup, otherwise will not exit - } + config = { + trace: tracerFile + }; + tracer = util.tracer.createFromConfig('class', 'obj', config); + }); - if (error) { - return Promise.reject(error); - } - return Promise.resolve(); + afterEach(() => { + Object.keys(util.tracer.instances).forEach((tracerName) => { + util.tracer.remove(tracerName); }); - }); + sinon.restore(); + }); - it('should remove tracer by name', () => { - const tracer = util.tracer.createFromConfig('class', 'obj', config); - util.tracer.remove(tracer.name); - assert.strictEqual(util.tracer.instances[tracer.name], undefined); - }); + after(() => { + removeDir(tracerDir); + }); - it('should remove tracer by filter', () => { - const tracer = util.tracer.createFromConfig('class', 'obj', config); - util.tracer.remove(null, t => t.name === tracer.name); - assert.strictEqual(util.tracer.instances[tracer.name], undefined); - }); + it('should create tracer using default location', () => { + sinon.stub(constants, 'TRACER_DIR').value(tracerDir); + tracer = util.tracer.createFromConfig('class2', 'obj2', { trace: true }); + return assert.isFulfilled( + tracer.write('foobar') + .then(() => { + assert.strictEqual(fs.readFileSync(`${tracerDir}/class2.obj2`, 'utf8'), 'foobar'); + }) + ); + }); - it('should truncate file', () => { - const tracer = util.tracer.createFromConfig('class', 'obj', config); - const expectedData = 'expectedData'; - let error; - - return tracer.write(expectedData) - .then(() => { - const contents = fs.readFileSync(tracerFile, 'utf8'); - assert.strictEqual(contents, expectedData); - return tracer._truncate(); - }).then(() => { - const contents = fs.readFileSync(tracerFile, 'utf8'); - assert.strictEqual(contents, ''); - return tracer.write(expectedData); - }) - .then(() => { - const contents = fs.readFileSync(tracerFile, 'utf8'); - assert.strictEqual(contents, expectedData); - }) - .catch((err) => { - error = err; - }) - .then(() => { - util.tracer.remove(tracer); // cleanup, otherwise will not exit + it('should write data to same file (2 tracers)', () => { + // due to implementation of Tracer it is hard to verify output + // so, just verify it is not fails + const filePath = path.join(tracerDir, 'output'); + const tracer1 = util.tracer.createFromConfig('class2', 'obj2', { trace: filePath }); + const tracer2 = util.tracer.createFromConfig('class2', 'obj3', { trace: filePath }); + return assert.isFulfilled(Promise.all([ + tracer1.write('tracer1'), + tracer2.write('tracer2') + ]) + .then(() => { + assert.ok(/tracer[12]/.test(fs.readFileSync(filePath, 'utf8'))); + })); + }); - if (error) { - return Promise.reject(error); - } - return Promise.resolve(); - }); - }); + it('should write data to file', () => assert.isFulfilled( + tracer.write('foobar') + .then(() => { + assert.strictEqual(fs.readFileSync(tracerFile, 'utf8'), 'foobar'); + }) + )); - it('should recreate file and dir when deleted', () => { - const fileName = `${os.tmpdir()}/telemetryTmpDir/telemetry`; // os.tmpdir for windows + linux - const dirName = path.dirname(fileName); + it('should accept no data', () => assert.isFulfilled(tracer.write(null))); - const tracerConfig = { - trace: fileName - }; - const tracer = util.tracer.createFromConfig('class', 'obj', tracerConfig); - const expectedData = 'expectedData'; - let error; + it('should remove tracer', () => { + util.tracer.remove(tracer); + assert.strictEqual(util.tracer.instances[tracer.name], undefined); + }); - util.tracer.REOPEN_INTERVAL = 500; + it('should remove tracer by name', () => { + util.tracer.remove(tracer.name); + assert.strictEqual(util.tracer.instances[tracer.name], undefined); + }); - function removeTmpTestDirectory() { - if (fs.existsSync(dirName)) { - fs.readdirSync(dirName).forEach((item) => { - item = path.join(dirName, item); - fs.unlinkSync(item); - }); - fs.rmdirSync(dirName); + it('should remove tracer by filter', () => { + util.tracer.remove(t => t.name === tracer.name); + assert.strictEqual(util.tracer.instances[tracer.name], undefined); + }); + + it('should get existing tracer by the name', () => assert.isFulfilled( + tracer.write('somethings') + .then(() => { + const sameTracer = util.tracer.createFromConfig('class', 'obj', config); + return sameTracer.write('something3') + .then(() => Promise.resolve(sameTracer)); + }) + .then((sameTracer) => { + assert.strictEqual(sameTracer.inode, tracer.inode, 'inode should be the sane'); + assert.strictEqual(sameTracer.stream.fd, tracer.stream.fd, 'fd should be the same'); + }) + )); + + it('should truncate file', () => assert.isFulfilled( + tracer.write('expectedData') + .then(() => { + assert.strictEqual(fs.readFileSync(tracerFile, 'utf8'), 'expectedData'); + return tracer._truncate(); + }).then(() => { + assert.strictEqual(fs.readFileSync(tracerFile, 'utf8'), ''); + return tracer.write('expectedData'); + }) + .then(() => { + assert.strictEqual(fs.readFileSync(tracerFile, 'utf8'), 'expectedData'); + }) + )); + + it('should recreate file and dir when deleted', () => { + const fileName = `${tracerDir}/telemetryTmpDir/telemetry`; // os.tmpdir for windows + linux + const dirName = path.dirname(fileName); + const tracerConfig = { + trace: fileName + }; + const oldInode = tracer.inode; + + if (fs.existsSync(fileName)) { + fs.truncateSync(fileName); } - } - - return tracer.write(expectedData) - .then(() => { - const contents = fs.readFileSync(fileName, 'utf8'); - assert.strictEqual(contents, expectedData); - // remove file and directory - removeTmpTestDirectory(); - if (fs.existsSync(fileName)) { - assert.fail('Should remove file'); - } - if (fs.existsSync(dirName)) { - assert.fail('Should remove directory'); - } - }) - // re-open should be scheduled in next 1sec - .then(() => new Promise((resolve) => { - function check() { - fs.exists(fileName, (exists) => { - if (exists) { - resolve(exists); - } else { - setTimeout(check, 200); - } - }); - } - check(); - })) - .then(() => { - const contents = fs.readFileSync(fileName, 'utf8'); - assert.strictEqual(contents, ''); - assert.strictEqual(fs.existsSync(fileName), true, 'File should exists after recreation'); - }) - .catch((err) => { - error = err; - }) - .then(() => { - util.tracer.remove(tracer); // cleanup, otherwise will not exit - // remove file and directory - removeTmpTestDirectory(); - if (error) { - return Promise.reject(error); - } - return Promise.resolve(); - }); - }).timeout(10000); + tracer = util.tracer.createFromConfig('class2', 'obj2', tracerConfig); + util.tracer.REOPEN_INTERVAL = 500; + + return assert.isFulfilled(tracer.write('expectedData') + .then(() => { + assert.strictEqual(fs.readFileSync(fileName, 'utf8'), 'expectedData'); + // remove file and directory + removeDir(dirName); + if (fs.existsSync(fileName)) { + assert.fail('should remove file'); + } + if (fs.existsSync(dirName)) { + assert.fail('should remove directory'); + } + }) + // re-open should be scheduled in next 1sec + .then(() => new Promise((resolve) => { + function check() { + fs.exists(fileName, (exists) => { + if (exists) { + resolve(exists); + } else { + setTimeout(check, 200); + } + }); + } + check(); + })) + .then(() => { + assert.notStrictEqual(tracer.inode, oldInode, 'should have different inode'); + assert.strictEqual(fs.readFileSync(fileName, 'utf8'), ''); + assert.strictEqual(fs.existsSync(fileName), true, 'file should exists after re-creation'); + })); + }).timeout(10000); + }); });