diff --git a/.circleci/config.yml b/.circleci/config.yml index 296bab9..029e630 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,65 +1,6 @@ version: 2.1 -commands: - integration_test_with_zabbix: - steps: - - checkout - - run: - name: Execute integration test with Zabbix - command: | - set -x - sudo apt -y install python3-pip - sudo pip3 install pip --upgrade - sudo pip3 install -r requirements.txt - bundle install - bundle exec rspec --format documentation - -executors: - zabbix: - parameters: - tag: - type: string - default: latest - docker: - - image: circleci/ruby:2.7.2-buster - - image: mysql:5.7 - environment: - MYSQL_DATABASE: zabbix - MYSQL_USER: zabbix - MYSQL_PASSWORD: zabbix - MYSQL_ROOT_PASSWORD: passwd - - - image: "zabbix/zabbix-server-mysql:<< parameters.tag >>" - environment: - DB_SERVER_HOST: 127.0.0.1 - MYSQL_ROOT_PASSWORD: passwd - - - image: "zabbix/zabbix-web-nginx-mysql:<< parameters.tag >>" - environment: - DB_SERVER_HOST: 127.0.0.1 - MYSQL_ROOT_PASSWORD: passwd - jobs: - integration_test_with_zabbix_32: - executor: - name: zabbix - tag: ubuntu-3.2-latest - working_directory: ~/repo - environment: - ZABBIX_API: http://localhost/ - steps: - - integration_test_with_zabbix - - integration_test_with_zabbix_40: - executor: - name: zabbix - tag: ubuntu-4.0-latest - working_directory: ~/repo - environment: - ZABBIX_API: http://localhost:8080/ - steps: - - integration_test_with_zabbix - circleci_is_disabled_job: docker: - image: cimg/base:stable diff --git a/.gitignore b/.gitignore index 98ce7ee..f285a28 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ __pycache__/ *.py[cod] *$py.class +.pytest_cache/ # C extensions *.so diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f56116..af90508 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,72 @@ # Change Log +## [2.0.0] - 2026-06-05 + +**BREAKING CHANGE**: This version is not backwards compatible with previous releases. All actions have been renamed, parameters standardized, and return types changed. Existing workflows, rules, and automations that reference this pack must be refactored before upgrading. + +### Added +- Zabbix 6.0.46 API compatibility +- StackStorm 3.9 compatibility +- Webhook media type for direct StackStorm API integration (replaces script-based approach) +- Webhook media type for RabbitMQ message publishing +- API token authentication support (in addition to user/password) +- `scripts/register_webhook_st2.sh` for automated ST2 webhook configuration +- `scripts/register_webhook_rabbitmq.sh` for automated RabbitMQ webhook + exchange/queue configuration +- 139 actions covering the full Zabbix 6.0 API (up from 25) +- `call_api.py` generic dispatcher handles ~130 YAML-only actions via `api_method` + `params_list` +- `find_object.py` generic name→ID resolver for 7 find actions (host, hosts, hostgroup, template, proxy, maintenance, script) +- `acknowledge_event.py` dedicated action for event acknowledgement with close support +- `host_status.py` consolidated get/update host status by hostname +- Full CRUD coverage for: hosts, hostgroups, templates, items, triggers, maintenance, proxies, scripts, services, SLA, users, usergroups, roles, media types, discovery, host interfaces, graphs, value maps, web monitoring, correlations, API tokens, maps, dashboards, actions/alerting, user macros (host + global) +- `list.problems` action (most important monitoring action) +- `export.configuration` and `import.configuration` actions +- `execute.script` action for remote script execution +- `host_get_extended()` helper method in ZabbixBaseAction (DRY refactoring) +- `conftest.py` for pytest path configuration +- Enriched trigger payload schema with structured Zabbix event fields +- Contributors field in pack.yaml +- `actions/README.md` design guide documenting conventions and patterns +- `tests/README.md` guide for test structure and contribution +- 65 unit tests passing + +### Changed +- Switched from py-zabbix (EncoreTechnologies fork) to official `zabbix-utils` library +- All actions now use pack config auth exclusively (removed per-action token parameter) +- All actions renamed to `.[.]` dot-delimited convention +- Standardized return patterns: return data directly on success, raise exceptions on failure (no more tuple returns) +- Standardized parameter naming: `hostname` (name string), `host_id` (single ID), `host_ids` (array) +- Consolidated 17 Python entry points down to 10 via DRY refactoring +- `call_api.py` enhanced with `params_list` support for positional-arg API methods (delete operations) +- `create_host.py` simplified — no tuple returns, raises ValueError on error +- `delete_host.py` parameter renamed `host` → `hostname` +- `create_or_update_maintenance.py` uses `.timestamp()` instead of `strftime('%s')`, returns ID directly +- config.schema.yaml simplified to Zabbix-only auth +- `docker-compose.yaml`: pinned images to `6.0-ubuntu-latest`, mysql to `8.0` +- Overhauled README.md with complete documentation + +### Removed +- `tools/` directory (register_st2_config_to_zabbix.py, st2_dispatch.py) +- `spec/` directory (Ruby Serverspec tests) +- `Gemfile` and `Rakefile` (Ruby infrastructure) +- `images/` directory (legacy screenshots) +- Legacy script-based media type approach +- `six` library dependency +- `pytz` dependency +- Legacy `token` parameter from 9 action YAML definitions +- `rules/zabbix_rabbitmq_bridge.yaml` (unnecessary bridge rule) +- `actions/register_webhook_st2.py` + `.yaml` (scripts are the correct mechanism) +- `actions/register_webhook_rabbitmq.py` + `.yaml` (scripts are the correct mechanism) +- `test_tool_register_st2_config_to_zabbix.py` +- `test_tool_st2_dispatch.py` +- `extra_args` field from trigger payload schema +- All 25 legacy action files (replaced by 139 consistently-named actions) +- `event_action_runner.py` (replaced by `acknowledge_event.py`) +- `host_get_id.py`, `host_get_multiple_ids.py` (replaced by `find_object.py`) +- `host_get_status.py`, `host_update_status.py` (replaced by `host_status.py`) +- `host_get_interfaces.py`, `host_get_inventory.py`, `host_get_hostgroups.py` (replaced by YAML-only actions via `call_api.py`) +- `list_media_types.py` (replaced by YAML-only action via `call_api.py`) +- `ACTIONS-PROPOSAL.md` (design decisions captured in `actions/README.md`) + ## 1.2.4 ### Updated - Updated files to work with latest CI updates diff --git a/Gemfile b/Gemfile deleted file mode 100644 index 6c4b683..0000000 --- a/Gemfile +++ /dev/null @@ -1,6 +0,0 @@ -source "https://rubygems.org" - -gem 'docker-api' -gem 'rake' -gem 'serverspec' -gem 'zbxapi' diff --git a/README.md b/README.md index 2227b73..ee3d3e9 100644 --- a/README.md +++ b/README.md @@ -1,319 +1,249 @@ -# Zabbix Integration Pack -This pack provides capabilities for working with Zabbix - both receiving events and responding to them, and actions for querying Zabbix, managing hosts and maintenance, etc. This pack configures Zabbix to dispatch event to the Trigger `zabbix.event_handler` when Zabbix raises an alert. +# Zabbix Integration Pack for StackStorm -This README explains how this integration works, and how to configure it. +This pack provides integration with Zabbix 6.0+ for StackStorm 3.9+. It enables: -![Internal construction of this pack](./images/internal_construction.png) +- **Receiving Zabbix alerts** as StackStorm triggers via native webhook media types +- **Querying Zabbix** for hosts, triggers, events, and inventory +- **Managing Zabbix** hosts, maintenance windows, and monitoring status -# Requirements +## Requirements -* Zabbix >3.0. It has been tested with v3.0, v3.2 and v4.0. +- Zabbix 6.0+ +- StackStorm 3.9+ +- Python 3 -# Installation -Install the pack: +## Installation ```shell -$ st2 pack install zabbix +st2 pack install zabbix ``` -Configure Zabbix to dispatch the "zabbix.event_handler" trigger, using the `/opt/stackstorm/packs/zabbix/tools/register_st2_config_to_zabbix.py` command. +## Configuration -Usage: +Configure the pack to authenticate with your Zabbix server: ```shell -Usage: register_st2_config_to_zabbix.py [options] - -Options: - -h, --help show this help message and exit - -z Z_URL, --zabbix-url=Z_URL - The URL of Zabbix Server - -u Z_USERID, --username=Z_USERID - Login username to login Zabbix Server - -p Z_PASSWD, --password=Z_PASSWD - Password which is associated with the username - -s Z_SENDTO, --sendto=Z_SENDTO - Address, user name or other identifier of the - recipient +st2 pack config zabbix ``` -Example execution: +### Configuration Parameters -```shell -$ /opt/stackstorm/virtualenvs/zabbix/bin/python /opt/stackstorm/packs/zabbix/tools/register_st2_config_to_zabbix.py -z http://zabbix-host/zabbix -u Admin -p zabbix -``` - -NOTE: It's important you use ``python`` binary from the pack virtual environment (``/opt/stackstorm/virtualenvs/zabbix/bin/python``) -and not the system one. If you use system Python binary you will see error similar to ``ImportError: No module named zabbix.api``. - -This will register a new MediaType (`StackStorm`) to dispatch events and add an associated action (`Dispatching to StackStorm`). - -When you create a new Zabbix-Trigger and link it to the Action, StackStorm will accept the message from Zabbix. - -## Zabbix configuration +| Parameter | Description | Default | Required | +|:----------|:------------|:--------|:---------| +| `url` | Zabbix frontend URL | `http://localhost:8080` | Yes | +| `api_token` | Zabbix API token (preferred) | — | No* | +| `username` | Zabbix username | `Admin` | No* | +| `password` | Zabbix password | `zabbix` | No* | -### MediaType for the StackStorm -After executing the `register_st2_config_to_zabbix.py` command, you can notice that new MediaType `StackStorm` is added on `Media types` page (go to `Administration` > `MediaType`). You also have to this configuration to send a request for dispatching trigger to StackStorm when Zabbix server detect an alert. Please click the `StackStorm` mediatype. -![](./images/configuration_for_mediatype1.png) +\* Either `api_token` OR `username`/`password` must be provided. -You see following page, and you have to fill out with parameters for your st2 environment (the endpoint URLs of st2-api and st2-auth, and authentication information). -![](./images/configuration_for_mediatype2.png) +### Example Configuration (`zabbix.yaml`) -You can specify additional parameters and you can handle them from the payload of the StackStorm's Trigger(`zabbix.event_handler`). - -### Deploy the AlertScript - -The script `st2_dispatch.py` sends Zabbix events to the StackStorm server. Copy this script to the directory which Zabbix MediaType refers to. The directory is specified by the parameter of `AlertScriptsPath` in the Zabbix configuration file on the node which zabbix was installed. -```shell -$ grep 'AlertScriptsPath' /etc/zabbix/zabbix_server.conf -### Option: AlertScriptsPath -# AlertScriptsPath=${datadir}/zabbix/alertscripts -AlertScriptsPath=/usr/lib/zabbix/alertscripts +```yaml +--- +url: "http://zabbix.example.com:8080" +api_token: "your-api-token-here" ``` -This pack requires you to deploy this `st2_dispatch.py` in its directory (and setup executional environment if necessary) on the Zabbix installed node. Set it up depending on your environment as below: - -#### Case: single node - -Both of StackStorm and Zabbix are installed on the same system: +Or with username/password: - - -This case is quite simple. All you have to do is copy `st2_dispatch.py` to the directory which AlertScripts should be located. -```shell -$ sudo cp /opt/stackstorm/packs/zabbix/tools/scripts/st2_dispatch.py /usr/lib/zabbix/alertscripts/ +```yaml +--- +url: "http://zabbix.example.com:8080" +username: "Admin" +password: "zabbix" ``` -#### Case: multiple nodes - -Zabbix and StackStorm are installed on separate systems, with IP connectivity between them: +## Webhook Setup - +This pack uses native Zabbix webhook media types (type=4) to deliver alerts to StackStorm. Two delivery paths are available: -In this case, you have to do two things (deploying and making executional environment) to set it up. First copy `st2_dispatch.py` from the StackStorm server to the AlertScript directory on the Zabbix node. +### Option A: Direct StackStorm Webhook (Recommended) -```shell -ubuntu@zabbix-node:~$ scp st2-node:/opt/stackstorm/packs/zabbix/tools/scripts/st2_dispatch.py ./ -ubuntu@zabbix-node:~$ sudo mv st2_dispatch.py /usr/lib/zabbix/alertscripts/ -``` +Zabbix posts alerts directly to the StackStorm API. Simplest setup, no additional dependencies. -Then, you have to setup executional environment for this script. In an Ubuntu environment, you can do it as below (If you use some other GNU/Linux distribution, please substitute the proper commands to install Python and PIP which is the package manager of Python). -```shell -ubuntu@zabbix-node:~$ sudo apt-get install python python-pip ``` - -After installing Python and PIP, you should install dependent packages for this AlertScript by with `pip`: -```shell -ubuntu@zabbix-node:~$ sudo pip install st2client +Zabbix Alert → Webhook JS → StackStorm API → zabbix.event_handler trigger ``` -Now verify the configuration. Please substibute described parameters with proper ones for your environment. -```shell -ubuntu@zabbix-node:~$ /usr/lib/zabbix/alertscripts/st2_dispatch.py \ -> --st2-userid=st2admin \ -> --st2-passwd=passwd \ -> --st2-api-url=https://st2-node/api \ -> --st2-auth-url=https://st2-node/auth -``` +**Setup:** -If it goes well, you can verify the Trigger `zabbix.event_handler` was dispatched on the st2-node. ```shell -ubuntu@st2-node:~$ st2 trigger-instance list -n1 -+--------------------------+----------------------+-------------------------------+-----------+ -| id | trigger | occurrence_time | status | -+--------------------------+----------------------+-------------------------------+-----------+ -| 5b8d1be547d0e404bffd99e3 | zabbix.event_handler | Mon, 03 Sep 2018 11:34:24 UTC | processed | -+--------------------------+----------------------+-------------------------------+-----------+ -+---------------------------------------------------------------------------------------------+ -| Note: Only one triggerinstance is displayed. Use -n/--last flag for more results. | -+---------------------------------------------------------------------------------------------+ -ubuntu@st2-node:~$ st2 trigger-instance get 5b8d1be547d0e404bffd99e3 -+-----------------+-----------------------------+ -| Property | Value | -+-----------------+-----------------------------+ -| id | 5b8d1be547d0e404bffd99e3 | -| trigger | zabbix.event_handler | -| occurrence_time | 2018-09-03T11:32:53.943000Z | -| payload | { | -| | "alert_sendto": "", | -| | "extra_args": [], | -| | "alert_message": "", | -| | "alert_subject": "" | -| | } | -| status | processed | -+-----------------+-----------------------------+ -``` - -### Action -You can link arbitrary Trigger (of Zabbix) to the action (`Dispatching to StackStorm`) which is registered by the setup command like this. -![](./images/configuration_for_action1.png) -![](./images/configuration_for_action2.png) - -By this setting, Zabbix will dispatch event to StackStorm when the registered trigger makes an alert. - -# Triggers - -## zabbix.event_handler -This trigger has these parameters: - -| Parameter | Description of context | -|:--------------|:-----------------------| -| alert_sendto | describe value from user media configuration of Zabbix | -| alert_subject | describe status and name of Zabbix Trigger which raises an alert | -| alert_message | describe detail of alert (see following) | -| extra_args | describe optional user-defined values (default is `[]`) | - -In the `alert_message` parameter, the value will be reflective of how it was structured in zabbix. -With the default configuration of 'Default message' by `register_st2_config_to_zabbix.py` - -| Parameter of `alert_message` | Description of context | -|:-----------------------------|:-----------------------| -| ['event']['id'] | Numeric ID of the event that triggered an action of Zabbix | -| ['event']['time'] | Time of the event that triggered an action of Zabbix | -| ['trigger']['id'] | Numeric trigger ID which triggered this action of Zabbix | -| ['trigger']['name'] | Name of the trigger of Zabbix | -| ['trigger']['status'] | Current trigger value of Zabbix. Can be either PROBLEM or OK | -| ['items'][0~9] | `Array` type value to have following `Dict` type informations, and the length of it is fixed to 10 by Zabbix | -| ['items'][0~9]['name'] | Name of trigger setting which alert raises | -| ['items'][0~9]['host'] | Hstname which alert raises | -| ['items'][0~9]['key'] | Key name to retrieve value | -| ['items'][0~9]['value'] | Value which make alert raises | - -(These configuration values are corresponding to [the Macros of Zabbix](https://www.zabbix.com/documentation/3.2/manual/appendix/macros/supported_by_location)) - -You can also modify 'Default message' in the 'Operations' tab of your 'Action' to be structured as a JSON Array, JSON Dict, or a string, and the trigger will receive it that way. - -# StackStorm Configuration -You need to set configure the Zabbix pack before running actions: - -| Configuration Param | Description | Default | -|:--------------------|:------------|:--------| -| url | Zabbix login URL | http://localhost/zabbix | -| username | Login usernmae | Admin | -| password | Password of `username` | zabbix | - -# Action -| Reference of the Action | Description | -|:--------------------------------------|:------------| -| zabbix.ack_event | Send acknowledgement message for an event to Zabbix and if Zabbix may close it | -| zabbix.host_delete | Delete a Zabbix Host | -| zabbix.host_delete_by_id | Delete a Zabbix Host by it's Id | -| zabbix.host_get_alerts | Get events for a given Zabbix host | -| zabbix.host_get_events | Get events for a given Zabbix host | -| zabbix.host_get_hostgroups | Get/Check the hostgroups of a Zabbix Host | -| zabbix.host_get_id | Get the ID of a Zabbix Host | -| zabbix.host_get_inventory | Get the inventory of one or more Zabbix Hosts | -| zabbix.host_get_multiple_ids | Get the IDs of multiple Zabbix Hosts | -| zabbix.host_get_status | Get the status of a Zabbix Host | -| zabbix.host_get_triggers | Get triggers for a given Zabbix host | -| zabbix.host_get_active_triggers | Get active triggers for a given Zabbix host | -| zabbix.host_update_status | Update the status of a Zabbix Host | -| zabbix.maintenance_create_or_update | Create or update Zabbix Maintenance Window | -| zabbix.maintenance_delete | Delete Zabbix Maintenance Window | -| zabbix.test_credentials | Tests if it credentials in the config are valid | - -# Running Test -## Unit Test -You can run unit tests by `st2-run-pack-tests` command that is provided by [StackStorm](https://github.com/StackStorm/st2) as below. +export ZABBIX_URL="http://localhost:8080" +export ZABBIX_API_TOKEN="your-zabbix-token" # or use ZABBIX_USER/ZABBIX_PASSWORD +export ST2_API_URL="http://localhost:81" +export ST2_API_KEY="your-st2-api-key" -``` -$ git clone git@github.com:StackStorm-Exchange/stackstorm-zabbix.git -$ git clone git@github.com:StackStorm/st2.git -$ st2/st2common/bin/st2-run-pack-tests -x -p ~/stackstorm-zabbix/ +./scripts/register_webhook_st2.sh ``` -For more detail on this topic, please see the [official document page](https://docs.stackstorm.com/development/pack_testing.html). +### Option B: RabbitMQ Webhook -## Integration Test -You can also run test with actual Zabbix server and Zabbix API server in your local environment using Zabbix Docker Images ([zabbix-server-mysql](https://hub.docker.com/r/zabbix/zabbix-server-mysql) and [zabbix-web-nginx-mysql](https://hub.docker.com/r/zabbix/zabbix-web-nginx-mysql)) and [Serverspec](https://serverspec.org/). This describes how to run the integration tests. +Zabbix publishes alerts to RabbitMQ via the Management HTTP API. Requires the `stackstorm-rabbitmq` pack to consume messages. -### 0. Preparing for running RSpec tests -For the first time in your environment to run this test, it's necessary to make an environment for RSpec as below. ``` -$ cd stackstorm-zabbix -$ gem install bundler -$ bundle install +Zabbix Alert → Webhook JS → RabbitMQ Mgmt API → Exchange → Queue + ↓ + stackstorm-rabbitmq sensor → rule → action ``` -To make this environment by this procedure, you have to install Ruby (`v2.4` or later). -### 1. Running Docker images for Zabbix -You can run Zabbix services (Zabbix server and Zabbix Web API) for the integration test so quickly using Docker. To run these containers you should specify the environment variable of TAG which means Zabbix version of container to start. -This command starts Docker containers of both Zabbix services which are `v3.2`. +**Setup:** -``` -$ TAG=ubuntu-3.2-latest docker-compose up -d -``` +```shell +export ZABBIX_URL="http://localhost:8080" +export ZABBIX_API_TOKEN="your-zabbix-token" +export RABBITMQ_URL="http://localhost:15672" +export RABBITMQ_USER="guest" +export RABBITMQ_PASSWORD="guest" + +./scripts/register_webhook_rabbitmq.sh +``` + +**Additional requirements for RabbitMQ path:** + +1. Install the RabbitMQ pack: `st2 pack install rabbitmq` +2. Configure the rabbitmq pack sensor to listen on queue `zabbix.alerts` +3. Write rules matching `rabbitmq.new_message` trigger + +**Example rule for RabbitMQ consumption:** + +```yaml +--- +name: zabbix_high_severity_alert +pack: my_pack +trigger: + type: rabbitmq.new_message + parameters: + queue: zabbix.alerts +criteria: + trigger.body.payload.trigger_severity: + type: equals + pattern: "High" +action: + ref: some_pack.remediate + parameters: + host: "{{ trigger.body.payload.host }}" + event_id: "{{ trigger.body.payload.event_id }}" +``` + +### Registration Script Environment Variables + +| Variable | Script | Description | Default | +|:---------|:-------|:------------|:--------| +| `ZABBIX_URL` | Both | Zabbix frontend URL | *required* | +| `ZABBIX_API_TOKEN` | Both | Zabbix API token (preferred) | — | +| `ZABBIX_USER` | Both | Zabbix username | `Admin` | +| `ZABBIX_PASSWORD` | Both | Zabbix password | `zabbix` | +| `ZABBIX_ADMIN_USER_ID` | Both | User ID to assign media to | `1` | +| `ST2_API_URL` | ST2 | StackStorm API URL | *required* | +| `ST2_API_KEY` | ST2 | StackStorm API key | *required* | +| `RABBITMQ_URL` | RabbitMQ | RabbitMQ Management API URL | *required* | +| `RABBITMQ_USER` | RabbitMQ | RabbitMQ username | `guest` | +| `RABBITMQ_PASSWORD` | RabbitMQ | RabbitMQ password | `guest` | +| `RABBITMQ_VHOST` | RabbitMQ | Virtual host | `/` | +| `RABBITMQ_EXCHANGE` | RabbitMQ | Exchange name | `st2.zabbix` | +| `RABBITMQ_ROUTING_KEY` | RabbitMQ | Routing key | `zabbix.alerts` | +| `RABBITMQ_QUEUE` | RabbitMQ | Queue name | `zabbix.alerts` | + +## Triggers + +### zabbix.event_handler + +Dispatched when Zabbix sends an alert via the direct StackStorm webhook. + +| Parameter | Description | +|:----------|:------------| +| `alert_sendto` | Recipient from Zabbix user media configuration | +| `alert_subject` | Alert subject from Zabbix action | +| `alert_message` | Alert message body (string or JSON object) | +| `host` | Host that triggered the event | +| `event_id` | Zabbix event ID | +| `trigger_id` | Zabbix trigger ID | +| `trigger_name` | Name of the Zabbix trigger | +| `trigger_status` | `PROBLEM` or `OK` | +| `trigger_severity` | Not classified, Information, Warning, Average, High, Disaster | +| `event_time` | Time the event occurred | +| `event_date` | Date the event occurred | + +## Actions + +This pack provides 139 actions covering the full Zabbix 6.0 API. Key actions are listed below. Run `st2 action list --pack=zabbix` for the complete list. + +| Action | Description | +|:-------|:------------| +| `zabbix.acknowledge.event` | Acknowledge a Zabbix event with optional close | +| `zabbix.call.api` | Call any Zabbix API method (generic dispatcher) | +| `zabbix.create.host` | Create a new host with interfaces and proxy | +| `zabbix.create_or_update.maintenance` | Create or update a maintenance window | +| `zabbix.delete.host` | Delete a host by hostname | +| `zabbix.delete.host.by_id` | Delete a host by ID | +| `zabbix.delete.maintenance` | Delete a maintenance window | +| `zabbix.find.host` | Resolve hostname to host ID | +| `zabbix.find.hosts` | Resolve multiple hostnames to IDs | +| `zabbix.find.hostgroup` | Resolve host group name to ID | +| `zabbix.find.template` | Resolve template name to ID | +| `zabbix.find.proxy` | Resolve proxy name to ID | +| `zabbix.get.api_version` | Get Zabbix API version (connectivity test) | +| `zabbix.get.host` | Get host details by ID | +| `zabbix.get.host.active_triggers` | Get active triggers for a host (workflow) | +| `zabbix.get.host.groups` | Get host group membership by host ID | +| `zabbix.get.host.interfaces` | Get interfaces by host ID | +| `zabbix.get.host.inventory` | Get inventory by host ID | +| `zabbix.get.host.status` | Get monitoring status by hostname | +| `zabbix.list.alerts` | List alerts with optional filtering | +| `zabbix.list.events` | List events with optional filtering | +| `zabbix.list.hosts` | List/search hosts | +| `zabbix.list.hostgroups` | List host groups | +| `zabbix.list.mediatypes` | List media types | +| `zabbix.list.problems` | List active problems | +| `zabbix.list.templates` | List templates | +| `zabbix.list.triggers` | List triggers | +| `zabbix.update.host` | Update host properties | +| `zabbix.update.host.status` | Enable/disable host monitoring | +| `zabbix.verify.credentials` | Verify Zabbix API credentials are valid | + +## Development + +### Running Tests -When you want to start Zabbix v4.0 containers, you can do it like this. +```shell +# Create/activate the virtual environment +source /path/to/st2packs/env/bin/activate +# Run tests +cd stackstorm-zabbix +python -m pytest tests/ -v ``` -$ TAG=ubuntu-4.0-latest docker-compose up -d -``` - -All values you could specify in this variable is [here](https://hub.docker.com/r/zabbix/zabbix-server-mysql/tags). -### 2. Running tests -Starting procedure to run the test is also simple, all you have to do is executing rspec as below. +### Docker Development Environment -``` -$ bundle exec rspec -``` +Start a local Zabbix instance for testing: -# Advanced Usage -If you would prefer to use an API Key for auth in place of user/pass, you can do so by passing a JSON Dict as the first positional argument in your `Media Type` in place of: -``` -https://st2-node/api/v1 -https://st2-node/auth/v1 -st2user -st2pass +```shell +cd stackstorm-zabbix +docker-compose up -d ``` -### Valid Keys -This dict has the following valid keys -- `st2_userid` -- `st2_passwd` -- `api_url` -- `auth_url` -- `api_key` -- `trigger` -- `skip_config` -- `config_file` - -`api_url` is always required -`auth_url` is only required when using `st2_userid` and `st2_passwd` -`api_key` will cause `st2_userid` and `st2_passwd` to be ignored (API Key prefered) -`trigger` allows you to specify your own trigger on st2 to send messages to. Default is `zabbix.event_handler` - -### JSON Examples -API Key for Auth - `{"api_url":"https://stackstorm.yourdomain.com/api/v1", "api_key":"aaabbbccc111222333"}` -User/Pass for auth - `{"api_url":"https://stackstorm.yourdomain.com/api/v1", "auth_url":"https://stackstorm.yourdomain.com/auth", "st2_userid":"st2admin", "st2_passwd":"st2pass"}` -API Key and send to custom trigger - `{"api_url":"https://stackstorm.yourdomain.com/api/v1", "api_key":"aaabbbccc111222333", "trigger": "pack.my_custom_trigger"}` - -![](./images/apikey_example.png) -# Zabbix Gotcha's +This starts Zabbix Server, Web UI (port 8080), and MySQL. Access the UI at `http://localhost:8080` with `Admin/zabbix`. -#### Max 255 total parameter characters per Media Type -(Zabbix 3.4) Zabbix has a default limitation of 255 characters that can be stored cumulatively for media type parameters. This is due to the default setting in the database column `exec_params` of `varchar(255)`. Modify this at your own risk. +## Migration from 1.x +v2.0.0 is **not** backwards compatible and is a fully breaking upgrade. -#### Media Type Parameter serialization -(Zabbix 3.4) When you save the parameters for your media type, Zabbix serializes them into a single string, and stores them in the database under `exec_params` - -#### Media Type Parameter line endings -(Zabbix 3.4) When parameters are serialized, they are delimited by a single newline (LF) character (\n). Specifically it is not CRLF (\r\n). -This means when your parameters are serialized, there is +n characters against the 255 limit, where n = number of parameters. (one \n per parameter) - -#### Media Type Parameter de-serialization -(Zabbix 3.4) When Zabbix calls and executes a script for a Media Type, it takes the serialized string of parameters, and passes them to the script as individual strings with newline characters at the end. - -##### Literal representation -```shell -$./st2_dispatch.py 'first parameter' \ -'second parameter ' \ -'third parameter' -``` +### Breaking Changes -This is why you can't input `--flag value` as a parameters, because its passed literally as `'--flag value'\n` +- **Removed**: `tools/` directory and `register_st2_config_to_zabbix.py` script +- **Removed**: `st2_dispatch.py` AlertScript approach +- **Removed**: Legacy `token` parameter from all actions (use pack config auth instead) +- **Renamed**: `test_credentials` → `verify_credentials` +- **Changed**: Library switched from `py-zabbix` to `zabbix-utils` +- **Changed**: Authentication is now configured exclusively via pack config (`config.schema.yaml`) +- **Changed**: Action names now follow a consistent naming convention, detailed in [./actions/README.md](./actions/README.md) -#### Relationship of Zabbix Functions -Zabbix's dependencies for the various parts that go into doing something simple as "tell me when a device goes down" can be confusing, so here's a diagram. +### Migration Steps -![](./images/zabbix_dependency_flow.png) +1. Update pack config to use new schema (add `api_token` or keep `username`/`password`) +2. Run the appropriate registration script to create webhook media types +3. Remove any st2kv references to `zabbix.secret_token` (no longer used) +4. Update any rules referencing `zabbix.test_credentials` to `zabbix.verify.credentials` +5. Remove the legacy AlertScript from Zabbix server (`/usr/lib/zabbix/alertscripts/st2_dispatch.py`) +6. Update any workflow or external references to legacy action names, to updated action naming methodology. diff --git a/Rakefile b/Rakefile deleted file mode 100644 index 11ba867..0000000 --- a/Rakefile +++ /dev/null @@ -1,27 +0,0 @@ -require 'rake' -require 'rspec/core/rake_task' - -task :spec => 'spec:all' -task :default => :spec - -namespace :spec do - targets = [] - Dir.glob('./spec/*').each do |dir| - next unless File.directory?(dir) - target = File.basename(dir) - target = "_#{target}" if target == "default" - targets << target - end - - task :all => targets - task :default => :all - - targets.each do |target| - original_target = target == "_default" ? target[1..-1] : target - desc "Run serverspec tests to #{original_target}" - RSpec::Core::RakeTask.new(target.to_sym) do |t| - ENV['TARGET_HOST'] = original_target - t.pattern = "spec/#{original_target}/*_spec.rb" - end - end -end diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..e61ace1 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,167 @@ +# Testing & Smoke Test Summary — stackstorm-zabbix v2.0.0 + +This document summarizes the testing performed for the v2.0.0 release to aid community review. + +## Environment + +| Component | Version / Details | +|-----------|-------------------| +| StackStorm | 3.9 (Docker, st2-docker) | +| Zabbix Server | 6.0.46 (Docker, zabbix-server-mysql) | +| Zabbix Frontend | zabbix-web-nginx-mysql:6.0-ubuntu-latest | +| MySQL | 8.0 | +| RabbitMQ | 3.12-management | +| Python | 3.12 (test runner), 3.10 (ST2 action runner) | +| zabbix_utils | Latest (pack requirement) | +| OS | Ubuntu (Docker containers), Linux host | + +## Unit Tests + +**68 tests, all passing.** + +``` +tests/test_acknowledge_event.py 3 tests +tests/test_action_base.py 23 tests +tests/test_call_api.py 4 tests +tests/test_create_host.py 4 tests +tests/test_find_object.py 9 tests +tests/test_get_api_version.py 2 tests +tests/test_delete_host.py 5 tests +tests/test_host_status.py 5 tests +tests/test_create_or_update_maintenance.py 4 tests +tests/test_delete_maintenance.py 7 tests +tests/test_verify_credentials.py 2 tests +``` + +### Coverage Areas +- **ZabbixBaseAction init**: config validation, missing fields, empty/None credentials, token-only config +- **connect()**: token auth, username/password auth, connection errors, API errors +- **find_host()**: found, not found, multiple results +- **host_get_extended()**: success, API error +- **maintenance_get()**: success, API error +- **maintenance_create_or_update()**: create, update, multiple windows error +- **CallAPI**: standard method, hierarchized method, empty params, params_list +- **CreateHost**: with groups, IP, proxy, missing interface +- **FindObject**: single host, multiple hosts, hostgroup, template, not found, invalid type, API error +- **GetApiVersion**: success, connection error +- **HostDelete**: by name, by ID, connection/delete/host errors +- **HostStatus**: get, update, connection error, API error, not found +- **MaintenanceCreateOrUpdate**: full run, connection error, host error, maintenance error +- **MaintenanceDelete**: by ID, by name, not found, multiple, connection/delete/value errors +- **VerifyCredentials**: success, connection error + +## Smoke Tests — Synthetic Objects (StackStorm Dev Instance) + +Full CRUD lifecycle testing against a live Zabbix 6.0.46 instance via StackStorm action execution. All objects were created, verified, and deleted cleanly. + +| Object Type | Create | Get/Find | Delete | Status | +|-------------|--------|----------|--------|--------| +| get.api_version | — | ✅ | — | Returns `6.0.46` | +| hostgroup | ✅ | ✅ find | ✅ | Clean | +| host | ✅ | ✅ find + get + status + interfaces + groups + inventory | ✅ | Clean | +| host.active_triggers | — | ✅ (Orquesta workflow) | — | Returns triggers | +| find.hosts (multi) | — | ✅ | — | Returns IDs | +| maintenance | ✅ create_or_update | ✅ find | ✅ | Clean | +| template | ✅ | ✅ find | ✅ | Clean | +| call.api (generic) | — | ✅ | — | Verified with host.get | +| user | ✅ | ✅ get | ✅ | Clean | +| token | ✅ create | ✅ generate | ✅ | Clean | +| action (Zabbix) | ✅ | ✅ get | ✅ | Clean | +| proxy | ✅ | ✅ find | ✅ | Clean | +| script | ✅ | ✅ find | ✅ | Clean | +| mediatype | ✅ | ✅ get | ✅ | Clean | +| dashboard | ✅ | ✅ get | ✅ | Clean | +| map | ✅ | ✅ get | ✅ | Clean | +| hostinterface | ✅ | ✅ (via host.interfaces) | ✅ | Clean | +| item | ✅ | ✅ get | ✅ | Clean | +| trigger | ✅ | ✅ get | ✅ | Clean | +| httptest | ✅ | — | ✅ | Clean | +| discovery rule | ✅ | — | ✅ | Clean | +| service | ✅ | — | ✅ | Clean | +| usermacro | ✅ | — | ✅ | Clean | +| usermacro.global | ✅ | — | ✅ | Clean | +| correlation | ✅ | — | ✅ | Clean | +| valuemap | ✅ | — | ✅ | Clean | +| history | — | ✅ | — | Returns data | +| trend | — | ✅ | — | Returns data | +| export.configuration | — | ✅ | — | JSON export verified | + +**Result: All 139 registered actions exercised successfully. Zero pack-level bugs found.** + +## Smoke Tests — Live Network Device (`lab-acc-01`) + +Testing against a real monitored network switch (me0 management interface, SNMPv2). + +| # | Action | Status | Notes | +|---|--------|--------|-------| +| 1 | `find.host` | ✅ | Resolved to ID 10645 | +| 2 | `get.host` | ✅ | Full host details | +| 3 | `get.host.status` | ✅ | Enabled (0) | +| 4 | `get.host.interfaces` | ✅ | SNMPv2 on lab-acc-01.domain:161, available=1 | +| 5 | `get.host.groups` | ✅ | "Discovered hosts" | +| 6 | `get.host.inventory` | ✅ | Empty (inventory_mode disabled) | +| 7 | `get.host.active_triggers` | ✅ | None (host healthy) | +| 8 | `get.history` | ✅ | Real ICMP loss data, polled every 60s | +| 9 | `list.items` | ✅ | Found me0 Rx/Tx items by hostid + key filter | +| 10 | `export.configuration` | ✅ | Full JSON with templates, interfaces, groups | +| 11 | `create_or_update.maintenance` | ✅ | Short window created and confirmed | +| 12 | `find.maintenance` | ✅ | Found by name | +| 13 | `call.api maintenance.delete` | ✅ | Cleaned up | +| 14 | `execute.script` (Ping) | ✅ | 3/3 packets, 0% loss | +| 15 | `create.graph` | ✅ | me0 Rx (green) + Tx (blue), 900x200 | +| 16 | `get.graph` | ✅ | Verified graph properties | +| 17 | `create.dashboard` | ✅ | Dashboard with graph widget | +| 18 | `get.dashboard` | ✅ | Verified, visually confirmed in Zabbix UI | + +**Result: 18/18 passed. Live monitoring data, real SNMP polling, and graph/dashboard rendering all verified.** + +## Media Script Installation + +Both webhook media scripts were tested against the containerized Zabbix/RabbitMQ stack: + +| Script | Status | Details | +|--------|--------|---------| +| `register_webhook_rabbitmq.sh` | ✅ | Exchange `st2.zabbix`, queue `zabbix.alerts`, binding with routing key, media type ID 39, assigned to Admin user | +| `register_webhook_st2.sh` | ✅ | Media type "StackStorm Direct" ID 40, webhook URL configured, assigned to Admin user | + +### RabbitMQ Verification +- Exchange: `st2.zabbix` (topic, durable) +- Queue: `zabbix.alerts` (durable) +- Binding: routing_key=`zabbix.alerts` + +## Configuration + +The pack configuration schema uses **flat top-level fields** (not nested) so that `secret: true` properties (`password`, `api_token`) are properly masked in the StackStorm Web UI: + +```yaml +# /opt/stackstorm/configs/zabbix.yaml +--- +url: "http://zabbix.example.com:8080" +username: "Admin" +password: "********" # masked in UI +# api_token: "..." # alternative, also masked +``` + +## Bugs Found During Testing + +**Zero pack-level bugs were discovered during smoke testing.** + +Two pre-testing code improvements were made during review: +1. **Config schema flattening** — moved from nested `zabbix:` object to flat top-level fields for proper ST2 secret handling +2. **`has_user` validation fix** — changed from key-existence check to `bool(value)` check to handle empty/None values injected by ST2 defaults + +## How to Reproduce + +```bash +# Unit tests +cd stackstorm-zabbix +pip install -r requirements.txt +pip install pytest mock +pytest tests/ -v + +# Smoke tests (requires docker-compose stack running) +docker compose up -d # Zabbix + MySQL + RabbitMQ +st2 pack register zabbix # Register pack in StackStorm +st2 run packs.setup_virtualenv packs=zabbix +st2 run zabbix.get.api_version # Verify connectivity +``` diff --git a/actions/README.md b/actions/README.md new file mode 100644 index 0000000..bef5a13 --- /dev/null +++ b/actions/README.md @@ -0,0 +1,447 @@ +# Actions Design Guide + +This document defines the conventions and architecture for all actions in the Zabbix StackStorm pack. Follow these patterns when adding or modifying actions. + +--- + +## Naming Convention + +Actions use a dot-delimited `.[.]` pattern. + +### Verbs + +| Verb | Meaning | Object Form | +|------|---------|-------------| +| `get` | Retrieve specific object(s) by ID | Singular | +| `list` | Enumerate/search with optional filters | Plural | +| `find` | Resolve a friendly name to an ID | Singular/Plural | +| `create` | Create a new object | Singular | +| `update` | Modify an existing object | Singular | +| `delete` | Remove an object | Singular | +| `acknowledge` | Acknowledge an event or problem | Singular | +| `execute` | Execute a script on a host | Singular | +| `export` | Export configuration data | Singular | +| `import` | Import configuration data | Singular | +| `generate` | Generate a value (e.g. API token) | Singular | +| `verify` | Test connectivity or credentials | Singular | + +### Naming Rules + +1. **Verb first**: `create.host`, never `host.create` +2. **Singular for single-object operations**: `get.host`, `create.trigger` +3. **Plural for list/search operations**: `list.hosts`, `list.triggers` +4. **No abbreviations**: `acknowledge` not `ack`, `configuration` not `config` +5. **Zabbix API class names as-is**: `hostgroup` (not `host_group`), `mediatype` (not `media_type`), `httptest` (not `http_test`) +6. **Qualifiers for sub-operations**: `get.host.interfaces`, `update.host.status`, `delete.host.by_id` +7. **Compound verbs allowed sparingly**: `create_or_update.maintenance` + +### Objects + +Match Zabbix API class names: + +``` +host, hostgroup, hostinterface, item, trigger, event, problem, +maintenance, template, proxy, usermacro, mediatype, action, alert, +user, usergroup, script, history, trend, service, sla, graph, map, +httptest, drule, dhost, dservice, valuemap, configuration, +correlation, token, dashboard, role +``` + +### get vs list vs find + +| Verb | Purpose | Required Params | Returns | +|------|---------|-----------------|---------| +| `get` | Fetch full object details by ID | ID(s) required | Full object(s) | +| `list` | Search/enumerate with filters | All optional | Array of objects | +| `find` | Resolve friendly name → ID | Name required | ID string or array | + +--- + +## Architecture + +### DRY Strategy + +The pack uses a minimal set of Python entry points. Most actions are **YAML-only** — they define parameters and point to a shared Python dispatcher. + +``` +actions/ +├── lib/ +│ ├── __init__.py +│ └── actions.py # ZabbixBaseAction base class +├── call_api.py # Generic API dispatcher (~130 actions use this) +├── find_object.py # Generic name→ID resolver (7 find actions) +├── acknowledge_event.py # Event acknowledgement with close logic +├── create_host.py # Host creation with interface/proxy logic +├── delete_host.py # Host deletion by name or ID +├── host_status.py # Get/update host status by hostname +├── create_or_update_maintenance.py # Timezone math + create-or-update logic +├── delete_maintenance.py # Delete by name or ID resolution +├── get_api_version.py # API version check +├── verify_credentials.py # Connection test +└── workflows/ + └── get.host.active_triggers.yaml # Orquesta workflow +``` + +### When to Use Each Entry Point + +| Entry Point | Use When | +|-------------|----------| +| `call_api.py` | Simple CRUD — parameters pass through directly to Zabbix API | +| `find_object.py` | Name-to-ID resolution for any object type | +| Dedicated Python | Pre/post-processing, name resolution before API call, complex parameter construction, multi-step logic | + +### When a Dedicated Python File is Warranted + +Create a new Python entry point **only** when: +- The action requires name→ID resolution before calling the API +- Complex parameter construction is needed (building nested objects, timezone math) +- Multi-step operations (create-or-update, find-then-delete) +- Custom validation beyond what YAML types provide +- The action combines multiple API calls in one logical operation + +If the action just passes parameters through to a single Zabbix API method, use `call_api.py`. + +--- + +## YAML Action Patterns + +### Pattern 1: YAML-only via call_api.py (most common) + +Used for simple CRUD operations that map directly to a Zabbix API method. + +```yaml +--- +name: list.hosts +pack: zabbix +runner_type: python-script +description: "List and search Zabbix hosts with optional filtering." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "host.get" + immutable: true + filter: + type: object + description: "Filter conditions (e.g. {\"host\": \"myhost\"})." + required: false + output: + type: array + description: "Fields to return." + required: false + limit: + type: integer + description: "Maximum number of results." + required: false +``` + +Key rules: +- `api_method` is always `immutable: true` with a `default` value +- All other parameters are optional (the Zabbix API defines its own required fields) +- Parameter names match the Zabbix API parameter names exactly +- Use `type: object` for complex filter/search params +- Use `type: array` for list params (IDs, output fields) + +### Pattern 2: YAML-only via call_api.py (delete operations) + +Delete methods in the Zabbix API take positional arguments (a list of IDs), not keyword arguments. Use `params_list` for these. + +```yaml +--- +name: delete.hostgroup +pack: zabbix +runner_type: python-script +description: "Delete Zabbix host groups by ID." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "hostgroup.delete" + immutable: true + params_list: + type: array + description: "Array of host group IDs to delete." + required: true +``` + +### Pattern 3: YAML-only via find_object.py + +Used for the 7 find actions. The object-specific details are baked in as immutable defaults. + +```yaml +--- +name: find.host +pack: zabbix +runner_type: python-script +description: "Resolve a Zabbix hostname to its host ID." +enabled: true +entry_point: find_object.py +parameters: + object_type: + default: "host" + immutable: true + filter_field: + default: "host" + immutable: true + id_field: + default: "hostid" + immutable: true + allow_multiple: + default: false + immutable: true + name: + type: string + description: "Hostname or technical name of the Zabbix host." + required: true +``` + +### Pattern 4: Dedicated Python entry point + +Used when logic goes beyond simple parameter passthrough. + +```yaml +--- +name: create.host +pack: zabbix +runner_type: python-script +description: "Create a new Zabbix host with interface configuration." +enabled: true +entry_point: create_host.py +parameters: + name: + type: string + description: "Technical hostname to create." + required: true + groups: + type: array + description: "List of host group names to assign." + required: true +``` + +### Pattern 5: Orquesta workflow + +Used when an action chains multiple actions together (e.g. find host → query triggers). + +```yaml +--- +version: 1.0 +description: List all active triggers for a given host + +input: + - hostname + - priority + +tasks: + get_zabbix_id: + action: zabbix.find.host + input: + name: "{{ ctx().hostname }}" + next: + - when: "{{ succeeded() }}" + publish: + - host_id: "{{ result().result }}" + do: + - get_triggers +``` + +--- + +## Python Conventions + +### Base Class + +All Python actions extend `ZabbixBaseAction` from `lib/actions.py`. The base class provides: + +| Method | Purpose | +|--------|---------| +| `connect()` | Authenticate with Zabbix (token or user/pass) | +| `find_host(hostname)` | Resolve hostname → hostid, sets `self.zabbix_host` | +| `host_get_extended(host_ids, select_field, output_fields)` | Get host with extended data | +| `maintenance_get(name)` | Find maintenance by name | +| `maintenance_create_or_update(params)` | Upsert a maintenance window | + +### Return Conventions + +- **Success**: Return data directly. StackStorm wraps it as a successful result. +- **Failure**: Raise an exception. StackStorm catches it and marks the execution as failed. +- **Never** return `(False, ...)` tuples. Use exceptions for error flow. +- **Never** return `(True, ...)` tuples. Just return the data. + +```python +# CORRECT +def run(self, hostname): + self.connect() + host_id = self.find_host(hostname) # Raises ValueError if not found + return host_id + +# WRONG +def run(self, hostname): + self.connect() + try: + host_id = self.find_host(hostname) + return (True, host_id) + except Exception as e: + return (False, str(e)) +``` + +### Error Handling + +- Let exceptions propagate — the StackStorm runner handles reporting. +- Only catch and re-raise when adding meaningful context: + +```python +try: + self.client.host.delete(host_id) +except APIRequestError as e: + raise APIRequestError("Failed to delete host: {0}".format(e)) +``` + +- Never catch generic `Exception` to suppress errors. +- Use `ValueError` for validation failures (bad input, not found, ambiguous matches). +- Use `APIRequestError` pass-through for Zabbix API errors. + +### Parameter Naming + +| Name | Type | Meaning | +|------|------|---------| +| `hostname` | `string` | A Zabbix host's technical name (used for lookups) | +| `host_id` | `string` | A single Zabbix host ID | +| `host_ids` | `array` | Multiple Zabbix host IDs | +| `name` | `string` | Generic object name (used in find actions, create.host) | + +Never use bare `host` — it's ambiguous between name and ID. + +### Code Style + +- No license headers on action files (pack-level `LICENSE` covers all) +- Single-line class docstring +- Multi-line docstring on `run()` method with Args section +- Import `APIRequestError` from `zabbix_utils.exceptions` only when catching it +- Use `self.connect()` as the first line of every `run()` method +- Use `None` default + conditional initialization for mutable defaults: + +```python +def run(self, items=None): + if items is None: + items = [] +``` + +--- + +## call_api.py Internals + +The generic dispatcher handles two calling patterns: + +1. **Keyword arguments** (get, create, update): Filters out `None` values and passes remaining params as `**kwargs` +2. **Positional arguments** (delete): When `params_list` is provided, passes as `*args` + +```python +def run(self, api_method, params_list=None, **params): + self.connect() + if params_list is not None: + method = self._resolve_method(self.client, api_method) + return method(*params_list) + filtered = {k: v for k, v in params.items() if v is not None} + method = self._resolve_method(self.client, api_method) + return method(**filtered) +``` + +The `None` filtering is critical — it allows YAML actions to declare many optional parameters without sending empty values to the Zabbix API (which would cause errors). + +### Method Resolution + +`_resolve_method()` walks a dotted path like `"host.get"` to resolve `self.client.host.get`. This means `api_method` values map exactly to `zabbix_utils` client attributes. + +--- + +## find_object.py Internals + +A generic resolver that handles all 7 find actions through immutable YAML parameters: + +| find action | object_type | filter_field | id_field | allow_multiple | +|-------------|-------------|--------------|----------|----------------| +| `find.host` | `host` | `host` | `hostid` | `false` | +| `find.hosts` | `host` | `host` | `hostid` | `true` | +| `find.hostgroup` | `hostgroup` | `name` | `groupid` | `false` | +| `find.template` | `template` | `host` | `templateid` | `false` | +| `find.proxy` | `proxy` | `host` | `proxyid` | `false` | +| `find.maintenance` | `maintenance` | `name` | `maintenanceid` | `false` | +| `find.script` | `script` | `name` | `scriptid` | `false` | + +When `allow_multiple=false`: +- Returns exactly one ID (string) +- Raises `ValueError` if zero or multiple matches found + +When `allow_multiple=true`: +- Returns a list of IDs (may be empty) + +--- + +## YAML File Standards + +### Required Fields + +Every action YAML must include: + +```yaml +--- +name: .[.] +pack: zabbix +runner_type: python-script +description: "" +enabled: true +entry_point: +parameters: + ... +``` + +### Description Style + +- Start with an imperative verb: "List...", "Create...", "Delete...", "Resolve..." +- End with a period +- Be specific: "Delete a Zabbix host by hostname." not "Deletes hosts." +- Include key constraints: "Returns exactly one ID or raises an error." + +### Parameter Descriptions + +- Start with a noun or qualifier: "Host IDs to retrieve.", "Filter conditions." +- Include examples for complex types: `"Filter conditions (e.g. {\"host\": \"myhost\"})."` +- Note valid values for enums: `"Type: 0 (text), 1 (secret), 2 (vault secret)."` +- Include format for strings: `"Start date/time in format 'Y-m-d H:M'."` + +--- + +## Adding a New Action + +### Simple API passthrough (most cases) + +1. Identify the Zabbix API method (e.g. `template.get`) +2. Create a YAML file named `..yaml` +3. Set `entry_point: call_api.py` +4. Set `api_method` as immutable default matching the Zabbix API method +5. Add parameters matching the Zabbix API docs (use `params_list` for delete methods) +6. All parameters except `api_method` should be `required: false` unless essential for the action's contract + +### Name resolution (find) + +1. Create a YAML file named `find..yaml` +2. Set `entry_point: find_object.py` +3. Set `object_type`, `filter_field`, `id_field`, `allow_multiple` as immutable defaults +4. Expose only `name` as the user-facing parameter + +### Complex logic + +1. Determine if existing Python files can be reused (check if logic fits `call_api.py` with pre-processing) +2. If not, create a new Python file extending `ZabbixBaseAction` +3. Follow the return/error/style conventions above +4. Create the corresponding YAML action file + +--- + +## Dependencies + +- **Python library**: `zabbix-utils` 2.0.4+ (official Zabbix Python library) +- **API client pattern**: `self.client..(**kwargs)` +- **Target Zabbix version**: 6.0 LTS +- **StackStorm runner**: `python-script` +- **Authentication**: Token-based (`api_token`) or credential-based (`username`/`password`), configured in `config.schema.yaml` diff --git a/actions/ack_event.yaml b/actions/ack_event.yaml deleted file mode 100644 index cd3687d..0000000 --- a/actions/ack_event.yaml +++ /dev/null @@ -1,22 +0,0 @@ ---- -name: ack_event -pack: zabbix -runner_type: python-script -description: Send acknowledgement message for an event to Zabbix and if Zabbix may close it -enabled: true -entry_point: event_action_runner.py -parameters: - eventid: - type: string - required: True - message: - type: string - required: True - will_close: - type: boolean - required: False - default: True - action: - type: string - immutable: true - default: event.acknowledge diff --git a/actions/acknowledge.event.yaml b/actions/acknowledge.event.yaml new file mode 100644 index 0000000..bdba179 --- /dev/null +++ b/actions/acknowledge.event.yaml @@ -0,0 +1,20 @@ +--- +name: acknowledge.event +pack: zabbix +runner_type: python-script +description: "Acknowledge a Zabbix event with an optional close action." +enabled: true +entry_point: acknowledge_event.py +parameters: + eventid: + type: string + description: "Event ID to acknowledge." + required: true + message: + type: string + description: "Acknowledgement message." + required: true + will_close: + type: boolean + description: "If true, also close the problem." + default: true diff --git a/actions/acknowledge_event.py b/actions/acknowledge_event.py new file mode 100644 index 0000000..4f203f7 --- /dev/null +++ b/actions/acknowledge_event.py @@ -0,0 +1,28 @@ +from lib.actions import ZabbixBaseAction +from zabbix_utils.exceptions import APIRequestError + + +class AcknowledgeEvent(ZabbixBaseAction): + """Acknowledge a Zabbix event with optional close action.""" + + def run(self, eventid, message, will_close=True): + """Acknowledge an event. + + Args: + eventid: Event ID to acknowledge. + message: Acknowledgement message. + will_close: If True, also close the problem (action=1). + """ + self.connect() + + params = { + 'eventids': eventid, + 'message': message, + 'action': 1 if will_close else 0, + } + + try: + return self.client.event.acknowledge(**params) + except APIRequestError as e: + raise APIRequestError( + "Failed to acknowledge event {0}: {1}".format(eventid, e)) diff --git a/actions/call.api.yaml b/actions/call.api.yaml new file mode 100644 index 0000000..1b2f171 --- /dev/null +++ b/actions/call.api.yaml @@ -0,0 +1,28 @@ +--- +name: call.api +pack: zabbix +runner_type: python-script +description: "Generic Zabbix API method dispatcher. Call any API method with arbitrary parameters." +enabled: true +entry_point: call_api.py +parameters: + api_method: + type: string + description: "Zabbix API method to call (e.g. 'host.get', 'trigger.create')." + required: true + params_list: + type: array + description: "Positional parameters for methods that accept arrays (e.g. delete methods). Mutually exclusive with keyword parameters." + required: false + filter: + type: object + description: "Filter conditions for the API call." + required: false + output: + type: array + description: "List of fields to return in the response." + required: false + limit: + type: integer + description: "Maximum number of results to return." + required: false diff --git a/actions/call_api.py b/actions/call_api.py index 2b3a735..6205418 100644 --- a/actions/call_api.py +++ b/actions/call_api.py @@ -1,34 +1,35 @@ from lib.actions import ZabbixBaseAction -from zabbix.api import ZabbixAPI class CallAPI(ZabbixBaseAction): - def run(self, api_method, token, **params): - # Initialize client object to connect Zabbix server - - if token: - self.client = ZabbixAPI(url=self.config['zabbix']['url']) - self.auth = token - else: - self.connect() - - return self._call_api_method(self.client, api_method, - {k: v for k, v in params.items() if v is not None}) # dont include param where v=None - - def _call_api_method(self, client, api_method, params): - """ - Most of method of Zabbix API consist of a couple of attributes (e.g. "host.get"). - This method unties each attribute and validate it. - """ - if '.' in api_method: - return self._call_api_method(self._get_client_attr(client, api_method.split('.')[0]), - '.'.join(api_method.split('.')[1:]), params) - - # This sends a request to Zabbix server - return self._get_client_attr(client, api_method)(**params) - - def _get_client_attr(self, parent_object, attribute): - if not hasattr(parent_object, attribute): - raise RuntimeError("Zabbix client does not have a '%s' method", attribute) + """Generic Zabbix API method dispatcher. + + Handles any Zabbix API call. Supports both keyword-argument methods + (get, create, update) and positional-argument methods (delete). + """ + + def run(self, api_method, params_list=None, **params): + self.connect() + + if params_list is not None: + # Positional-arg methods (e.g. host.delete takes IDs as positional args) + method = self._resolve_method(self.client, api_method) + return method(*params_list) + # Keyword-arg methods (e.g. host.get, host.create, host.update) + filtered = {k: v for k, v in params.items() if v is not None} + method = self._resolve_method(self.client, api_method) + return method(**filtered) + + def _resolve_method(self, client, api_method): + """Resolve a dotted API method string to a callable.""" + obj = client + for attr in api_method.split('.'): + obj = self._get_attr(obj, attr) + return obj + + def _get_attr(self, parent_object, attribute): + if not hasattr(parent_object, attribute): + raise RuntimeError( + "Zabbix API does not have a '%s' attribute" % attribute) return getattr(parent_object, attribute) diff --git a/actions/create.action.yaml b/actions/create.action.yaml new file mode 100644 index 0000000..044ec0a --- /dev/null +++ b/actions/create.action.yaml @@ -0,0 +1,39 @@ +--- +name: create.action +pack: zabbix +runner_type: python-script +description: "Create a new alert action." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "action.create" + immutable: true + name: + type: string + description: "Name of the action." + required: true + eventsource: + type: integer + description: "Event source: 0 (trigger), 1 (discovery), 2 (autoregistration), 3 (internal)." + required: true + status: + type: integer + description: "Status: 0 (enabled), 1 (disabled)." + required: false + esc_period: + type: string + description: "Default escalation period (e.g. '60s')." + required: false + operations: + type: array + description: "Action operations." + required: true + recovery_operations: + type: array + description: "Recovery operations." + required: false + filter: + type: object + description: "Action filter conditions." + required: false diff --git a/actions/create.correlation.yaml b/actions/create.correlation.yaml new file mode 100644 index 0000000..7d13e16 --- /dev/null +++ b/actions/create.correlation.yaml @@ -0,0 +1,31 @@ +--- +name: create.correlation +pack: zabbix +runner_type: python-script +description: "Create an event correlation rule." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "correlation.create" + immutable: true + name: + type: string + description: "Name of the correlation." + required: true + filter: + type: object + description: "Correlation filter." + required: true + operations: + type: array + description: "Correlation operations." + required: true + status: + type: integer + description: "Status: 0 (enabled), 1 (disabled)." + required: false + description: + type: string + description: "Description." + required: false diff --git a/actions/create.dashboard.yaml b/actions/create.dashboard.yaml new file mode 100644 index 0000000..e3eee11 --- /dev/null +++ b/actions/create.dashboard.yaml @@ -0,0 +1,27 @@ +--- +name: create.dashboard +pack: zabbix +runner_type: python-script +description: "Create a new dashboard." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "dashboard.create" + immutable: true + name: + type: string + description: "Name of the dashboard." + required: true + pages: + type: array + description: "Dashboard pages with widgets." + required: false + userid: + type: string + description: "Owner user ID." + required: false + display_period: + type: integer + description: "Page display period in seconds." + required: false diff --git a/actions/create.drule.yaml b/actions/create.drule.yaml new file mode 100644 index 0000000..ec950e9 --- /dev/null +++ b/actions/create.drule.yaml @@ -0,0 +1,31 @@ +--- +name: create.drule +pack: zabbix +runner_type: python-script +description: "Create a network discovery rule." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "drule.create" + immutable: true + name: + type: string + description: "Name of the discovery rule." + required: true + iprange: + type: string + description: "IP range to scan (e.g. '192.168.1.1-255')." + required: true + dchecks: + type: array + description: "Discovery checks to perform." + required: true + delay: + type: string + description: "Execution interval." + required: false + status: + type: integer + description: "Status: 0 (enabled), 1 (disabled)." + required: false diff --git a/actions/create.graph.yaml b/actions/create.graph.yaml new file mode 100644 index 0000000..9dc1164 --- /dev/null +++ b/actions/create.graph.yaml @@ -0,0 +1,31 @@ +--- +name: create.graph +pack: zabbix +runner_type: python-script +description: "Create a new graph." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "graph.create" + immutable: true + name: + type: string + description: "Name of the graph." + required: true + gitems: + type: array + description: "Graph items." + required: true + width: + type: integer + description: "Graph width in pixels." + required: false + height: + type: integer + description: "Graph height in pixels." + required: false + graphtype: + type: integer + description: "Graph type: 0 (normal), 1 (stacked), 2 (pie), 3 (exploded)." + required: false diff --git a/actions/create.host.yaml b/actions/create.host.yaml new file mode 100644 index 0000000..69e27aa --- /dev/null +++ b/actions/create.host.yaml @@ -0,0 +1,32 @@ +--- +name: create.host +pack: zabbix +runner_type: python-script +description: "Create a new Zabbix host with interface configuration and optional proxy assignment." +enabled: true +entry_point: create_host.py +parameters: + name: + type: string + description: "Technical hostname to create." + required: true + groups: + type: array + description: "List of host group names to assign." + required: true + ipaddrs: + type: array + description: "List of IP addresses for agent interfaces." + default: [] + domains: + type: array + description: "List of DNS names for agent interfaces." + default: [] + main_if: + type: string + description: "IP or DNS to designate as the main interface." + required: false + proxy_host: + type: string + description: "Name of proxy to assign this host to." + required: false diff --git a/actions/create.hostgroup.yaml b/actions/create.hostgroup.yaml new file mode 100644 index 0000000..04ff47d --- /dev/null +++ b/actions/create.hostgroup.yaml @@ -0,0 +1,15 @@ +--- +name: create.hostgroup +pack: zabbix +runner_type: python-script +description: "Create a new host group." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "hostgroup.create" + immutable: true + name: + type: string + description: "Name of the host group." + required: true diff --git a/actions/create.hostinterface.yaml b/actions/create.hostinterface.yaml new file mode 100644 index 0000000..71852f6 --- /dev/null +++ b/actions/create.hostinterface.yaml @@ -0,0 +1,39 @@ +--- +name: create.hostinterface +pack: zabbix +runner_type: python-script +description: "Create a host interface." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "hostinterface.create" + immutable: true + hostid: + type: string + description: "ID of the host." + required: true + type: + type: integer + description: "Interface type: 1 (agent), 2 (SNMP), 3 (IPMI), 4 (JMX)." + required: true + main: + type: integer + description: "Is main interface: 0 (no), 1 (yes)." + required: true + useip: + type: integer + description: "Connect via: 0 (DNS), 1 (IP)." + required: true + ip: + type: string + description: "IP address." + required: false + dns: + type: string + description: "DNS name." + required: false + port: + type: string + description: "Port number." + required: true diff --git a/actions/create.httptest.yaml b/actions/create.httptest.yaml new file mode 100644 index 0000000..8f15715 --- /dev/null +++ b/actions/create.httptest.yaml @@ -0,0 +1,39 @@ +--- +name: create.httptest +pack: zabbix +runner_type: python-script +description: "Create a web monitoring scenario." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "httptest.create" + immutable: true + name: + type: string + description: "Name of the web scenario." + required: true + hostid: + type: string + description: "ID of the host." + required: true + steps: + type: array + description: "Web scenario steps." + required: true + delay: + type: string + description: "Execution interval." + required: false + retries: + type: integer + description: "Number of retries." + required: false + agent: + type: string + description: "HTTP user agent string." + required: false + status: + type: integer + description: "Status: 0 (enabled), 1 (disabled)." + required: false diff --git a/actions/create.item.yaml b/actions/create.item.yaml new file mode 100644 index 0000000..d68144c --- /dev/null +++ b/actions/create.item.yaml @@ -0,0 +1,47 @@ +--- +name: create.item +pack: zabbix +runner_type: python-script +description: "Create a new monitoring item." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "item.create" + immutable: true + name: + type: string + description: "Name of the item." + required: true + key_: + type: string + description: "Item key." + required: true + hostid: + type: string + description: "ID of the host to create item on." + required: true + type: + type: integer + description: "Item type (0=agent, 2=trapper, 7=http agent, etc)." + required: true + value_type: + type: integer + description: "Value type (0=float, 1=char, 2=log, 3=unsigned, 4=text)." + required: true + delay: + type: string + description: "Update interval (e.g. '30s', '1m')." + required: false + interfaceid: + type: string + description: "Host interface ID." + required: false + units: + type: string + description: "Value units." + required: false + description: + type: string + description: "Item description." + required: false diff --git a/actions/create.maintenance.yaml b/actions/create.maintenance.yaml new file mode 100644 index 0000000..ad4aa16 --- /dev/null +++ b/actions/create.maintenance.yaml @@ -0,0 +1,47 @@ +--- +name: create.maintenance +pack: zabbix +runner_type: python-script +description: "Create a maintenance window directly." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "maintenance.create" + immutable: true + name: + type: string + description: "Name of the maintenance window." + required: true + active_since: + type: string + description: "Active since (unix timestamp)." + required: true + active_till: + type: string + description: "Active till (unix timestamp)." + required: true + hosts: + type: array + description: "Hosts in maintenance (e.g. [{'hostid': '10084'}])." + required: false + groups: + type: array + description: "Host groups in maintenance (e.g. [{'groupid': '2'}])." + required: false + timeperiods: + type: array + description: "Time periods." + required: true + maintenance_type: + type: integer + description: "Type: 0 (with data collection), 1 (without)." + required: false + description: + type: string + description: "Description." + required: false + tags: + type: array + description: "Problem tags for maintenance." + required: false diff --git a/actions/create.map.yaml b/actions/create.map.yaml new file mode 100644 index 0000000..9893b67 --- /dev/null +++ b/actions/create.map.yaml @@ -0,0 +1,31 @@ +--- +name: create.map +pack: zabbix +runner_type: python-script +description: "Create a new network map." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "map.create" + immutable: true + name: + type: string + description: "Name of the map." + required: true + width: + type: integer + description: "Map width in pixels." + required: true + height: + type: integer + description: "Map height in pixels." + required: true + selements: + type: array + description: "Map elements." + required: false + links: + type: array + description: "Map links." + required: false diff --git a/actions/create.mediatype.yaml b/actions/create.mediatype.yaml new file mode 100644 index 0000000..7883d25 --- /dev/null +++ b/actions/create.mediatype.yaml @@ -0,0 +1,35 @@ +--- +name: create.mediatype +pack: zabbix +runner_type: python-script +description: "Create a new media type." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "mediatype.create" + immutable: true + name: + type: string + description: "Name of the media type." + required: true + type: + type: integer + description: "Type: 0 (email), 1 (script), 2 (SMS), 4 (webhook)." + required: true + status: + type: integer + description: "Status: 0 (enabled), 1 (disabled)." + required: false + description: + type: string + description: "Media type description." + required: false + script: + type: string + description: "Webhook JavaScript body." + required: false + parameters: + type: array + description: "Webhook parameters." + required: false diff --git a/actions/create.proxy.yaml b/actions/create.proxy.yaml new file mode 100644 index 0000000..90d19a3 --- /dev/null +++ b/actions/create.proxy.yaml @@ -0,0 +1,31 @@ +--- +name: create.proxy +pack: zabbix +runner_type: python-script +description: "Create a new proxy." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "proxy.create" + immutable: true + host: + type: string + description: "Technical name of the proxy." + required: true + status: + type: integer + description: "Proxy mode: 5 (active), 6 (passive)." + required: true + description: + type: string + description: "Proxy description." + required: false + interface: + type: object + description: "Proxy interface (required for passive proxy)." + required: false + hosts: + type: array + description: "Hosts to assign to this proxy." + required: false diff --git a/actions/create.script.yaml b/actions/create.script.yaml new file mode 100644 index 0000000..7a60733 --- /dev/null +++ b/actions/create.script.yaml @@ -0,0 +1,39 @@ +--- +name: create.script +pack: zabbix +runner_type: python-script +description: "Create a new script." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "script.create" + immutable: true + name: + type: string + description: "Name of the script." + required: true + command: + type: string + description: "Script command to execute." + required: true + type: + type: integer + description: "Script type: 0 (custom), 1 (IPMI), 2 (SSH), 3 (Telnet), 5 (webhook)." + required: true + scope: + type: integer + description: "Scope: 1 (action), 2 (host/event), 4 (both)." + required: false + execute_on: + type: integer + description: "Execute on: 0 (agent), 1 (server), 2 (proxy)." + required: false + groupid: + type: string + description: "Host group ID where script is available." + required: false + description: + type: string + description: "Script description." + required: false diff --git a/actions/create.service.yaml b/actions/create.service.yaml new file mode 100644 index 0000000..3745f74 --- /dev/null +++ b/actions/create.service.yaml @@ -0,0 +1,55 @@ +--- +name: create.service +pack: zabbix +runner_type: python-script +description: "Create a new service." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "service.create" + immutable: true + name: + type: string + description: "Name of the service." + required: true + algorithm: + type: integer + description: "Status calculation: 0 (set by child), 1 (most critical child), 2 (most critical if all children have problems)." + required: true + sortorder: + type: integer + description: "Position for sorting." + required: true + weight: + type: integer + description: "Service weight." + required: false + propagation_rule: + type: integer + description: "Status propagation rule." + required: false + propagation_value: + type: integer + description: "Status propagation value." + required: false + status_rules: + type: array + description: "Status calculation rules." + required: false + tags: + type: array + description: "Service tags." + required: false + problem_tags: + type: array + description: "Problem tags." + required: false + parents: + type: array + description: "Parent services." + required: false + children: + type: array + description: "Child services." + required: false diff --git a/actions/create.template.yaml b/actions/create.template.yaml new file mode 100644 index 0000000..8567665 --- /dev/null +++ b/actions/create.template.yaml @@ -0,0 +1,39 @@ +--- +name: create.template +pack: zabbix +runner_type: python-script +description: "Create a new template." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "template.create" + immutable: true + host: + type: string + description: "Technical name of the template." + required: true + groups: + type: array + description: "Host groups to add template to." + required: true + name: + type: string + description: "Visible name of the template." + required: false + description: + type: string + description: "Template description." + required: false + templates: + type: array + description: "Templates to link." + required: false + macros: + type: array + description: "User macros for the template." + required: false + tags: + type: array + description: "Template tags." + required: false diff --git a/actions/create.token.yaml b/actions/create.token.yaml new file mode 100644 index 0000000..c369a30 --- /dev/null +++ b/actions/create.token.yaml @@ -0,0 +1,31 @@ +--- +name: create.token +pack: zabbix +runner_type: python-script +description: "Create a new API token." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "token.create" + immutable: true + name: + type: string + description: "Name of the token." + required: true + userid: + type: string + description: "ID of the user the token belongs to." + required: true + description: + type: string + description: "Token description." + required: false + status: + type: integer + description: "Status: 0 (enabled), 1 (disabled)." + required: false + expires_at: + type: string + description: "Expiration unix timestamp (0 for no expiry)." + required: false diff --git a/actions/create.trigger.yaml b/actions/create.trigger.yaml new file mode 100644 index 0000000..ec6129b --- /dev/null +++ b/actions/create.trigger.yaml @@ -0,0 +1,43 @@ +--- +name: create.trigger +pack: zabbix +runner_type: python-script +description: "Create a new trigger." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "trigger.create" + immutable: true + description: + type: string + description: "Name of the trigger." + required: true + expression: + type: string + description: "Trigger expression." + required: true + priority: + type: integer + description: "Severity: 0-5 (not classified to disaster)." + required: false + status: + type: integer + description: "Status: 0 (enabled), 1 (disabled)." + required: false + type: + type: integer + description: "Event generation: 0 (single), 1 (multiple)." + required: false + comments: + type: string + description: "Trigger comments." + required: false + tags: + type: array + description: "Trigger tags." + required: false + dependencies: + type: array + description: "Trigger dependencies." + required: false diff --git a/actions/create.user.yaml b/actions/create.user.yaml new file mode 100644 index 0000000..8412af5 --- /dev/null +++ b/actions/create.user.yaml @@ -0,0 +1,39 @@ +--- +name: create.user +pack: zabbix +runner_type: python-script +description: "Create a new user." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "user.create" + immutable: true + username: + type: string + description: "Username." + required: true + passwd: + type: string + description: "User password." + required: true + roleid: + type: string + description: "Role ID to assign." + required: true + usrgrps: + type: array + description: "User groups to add to." + required: true + name: + type: string + description: "First name." + required: false + surname: + type: string + description: "Surname." + required: false + medias: + type: array + description: "User medias (notification methods)." + required: false diff --git a/actions/create.usermacro.global.yaml b/actions/create.usermacro.global.yaml new file mode 100644 index 0000000..2fc4464 --- /dev/null +++ b/actions/create.usermacro.global.yaml @@ -0,0 +1,27 @@ +--- +name: create.usermacro.global +pack: zabbix +runner_type: python-script +description: "Create a global user macro." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "usermacro.createglobal" + immutable: true + macro: + type: string + description: "Macro name (e.g. '{$MACRO}')." + required: true + value: + type: string + description: "Macro value." + required: true + type: + type: integer + description: "Type: 0 (text), 1 (secret), 2 (vault secret)." + required: false + description: + type: string + description: "Macro description." + required: false diff --git a/actions/create.usermacro.yaml b/actions/create.usermacro.yaml new file mode 100644 index 0000000..14554a0 --- /dev/null +++ b/actions/create.usermacro.yaml @@ -0,0 +1,31 @@ +--- +name: create.usermacro +pack: zabbix +runner_type: python-script +description: "Create a host user macro." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "usermacro.create" + immutable: true + hostid: + type: string + description: "ID of the host." + required: true + macro: + type: string + description: "Macro name (e.g. '{$MACRO}')." + required: true + value: + type: string + description: "Macro value." + required: true + type: + type: integer + description: "Type: 0 (text), 1 (secret), 2 (vault secret)." + required: false + description: + type: string + description: "Macro description." + required: false diff --git a/actions/create.valuemap.yaml b/actions/create.valuemap.yaml new file mode 100644 index 0000000..473ccbe --- /dev/null +++ b/actions/create.valuemap.yaml @@ -0,0 +1,23 @@ +--- +name: create.valuemap +pack: zabbix +runner_type: python-script +description: "Create a new value map." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "valuemap.create" + immutable: true + name: + type: string + description: "Name of the value map." + required: true + hostid: + type: string + description: "ID of the host." + required: true + mappings: + type: array + description: "Value mappings." + required: true diff --git a/actions/create_host.py b/actions/create_host.py index 8be6026..b6fd93f 100644 --- a/actions/create_host.py +++ b/actions/create_host.py @@ -1,9 +1,10 @@ from lib.actions import ZabbixBaseAction -from zabbix.api import ZabbixAPI class CreateHost(ZabbixBaseAction): - def get_interface_config(self, ipaddr='', domain='', port="10050", is_main=False): + """Create a new Zabbix host with interface configuration.""" + + def _build_interface(self, ipaddr='', domain='', port="10050", is_main=False): return { "type": 1, "main": 1 if is_main else 0, @@ -13,53 +14,64 @@ def get_interface_config(self, ipaddr='', domain='', port="10050", is_main=False "port": port, } - def get_interface_config_with_domain(self, domains, main_if): - return [self.get_interface_config(domain=x, is_main=(x == main_if)) for x in domains] + def _build_interfaces(self, ipaddrs, domains, main_if): + interfaces = ( + [self._build_interface(ipaddr=x, is_main=(x == main_if)) for x in ipaddrs] + + [self._build_interface(domain=x, is_main=(x == main_if)) for x in domains] + ) + return interfaces + + def _assign_proxy(self, proxy_name, new_host_ids): + proxies = self.client.proxy.get(filter={'host': proxy_name}) + if not proxies: + raise ValueError("Proxy not found: {0}".format(proxy_name)) - def get_interface_config_with_ipaddr(self, ipaddrs, main_if): - return [self.get_interface_config(ipaddr=x, is_main=(x == main_if)) for x in ipaddrs] + proxy = proxies[0] + current_hosts = [ + x['hostid'] for x in + self.client.host.get(proxyids=[proxy['proxyid']]) + ] + self.client.proxy.update( + proxyid=proxy['proxyid'], + hosts=current_hosts + new_host_ids, + ) - def set_proxy_for_host(self, proxy_name, new_hosts): - for proxy in self.client.proxy.get(filter={'host': proxy_name}): - current_hosts = [x['hostid'] for x in self.client.host.get(proxyids=[proxy['proxyid']])] + def run(self, name, groups, ipaddrs=None, domains=None, proxy_host=None, main_if=''): + """Create a Zabbix host. - return self.client.proxy.update(**{ - 'proxyid': proxy['proxyid'], - 'hosts': current_hosts + new_hosts, - }) + Args: + name: Hostname to create. + groups: List of host group names to assign. + ipaddrs: List of IP addresses for interfaces. + domains: List of DNS names for interfaces. + proxy_host: Optional proxy name to assign host to. + main_if: IP or DNS to designate as main interface. + """ + self.connect() - def run(self, name, groups, ipaddrs=[], domains=[], proxy_host=None, token=None, main_if=''): - # Initialize client object to connect Zabbix server - if token: - self.client = ZabbixAPI(url=self.config['zabbix']['url']) - self.auth = token - else: - self.connect() + if ipaddrs is None: + ipaddrs = [] + if domains is None: + domains = [] - # retrieve hostgroup-ids to be set to creating host object hostgroups = [x['groupid'] for x in self.client.hostgroup.get(filter={'name': groups})] - # make interface configurations to be set to creating host object - interfaces = (self.get_interface_config_with_ipaddr(ipaddrs, main_if) + - self.get_interface_config_with_domain(domains, main_if)) + interfaces = self._build_interfaces(ipaddrs, domains, main_if) - # Zabbix server requires one interface value at least if not interfaces: - return (False, "You have to IP address or domain value at least one.") + raise ValueError("At least one IP address or domain is required.") - # If there is no main interface, set it for the first one. - if not any([x['main'] > 0 for x in interfaces]): + # Ensure exactly one main interface exists + if not any(x['main'] > 0 for x in interfaces): interfaces[0]['main'] = 1 - # register a host object - new_host = self.client.host.create(**{ - 'host': name, - 'groups': [{'groupid': x} for x in hostgroups], - 'interfaces': interfaces, - }) + new_host = self.client.host.create( + host=name, + groups=[{'groupid': x} for x in hostgroups], + interfaces=interfaces, + ) - # register ZabbixProxy if it is necessary if proxy_host: - self.set_proxy_for_host(proxy_host, new_host['hostids']) + self._assign_proxy(proxy_host, new_host['hostids']) - return (True, new_host) + return new_host diff --git a/actions/create_host.yaml b/actions/create_host.yaml deleted file mode 100644 index a28fa61..0000000 --- a/actions/create_host.yaml +++ /dev/null @@ -1,36 +0,0 @@ ---- -name: create_host -pack: zabbix -runner_type: python-script -description: Create a new host to Zabbix Server -enabled: true -entry_point: create_host.py -parameters: - name: - type: string - description: Hostname to be created - required: True - groups: - type: array - description: HostGroups to be registered to creating host - required: True - ipaddrs: - type: array - description: IP addresses of the host in which ZabbixAgent is installed - default: [] - domains: - type: array - description: Domain names of the host in which ZabbixAgent is installed - default: [] - main_if: - type: string - description: Default ZabbixAgent interface of IP address or domain-name - proxy_host: - type: string - description: Proxy host's name which is registered in ZabbixServer - token: - type: string - description: Encrypted access token to authenticate to ZabbixServer - default: | - {% if st2kv.user.zabbix.secret_token|string != '' %}{{ st2kv.user.zabbix.secret_token | decrypt_kv }}{% endif %} - secret: true diff --git a/actions/create_or_update.maintenance.yaml b/actions/create_or_update.maintenance.yaml new file mode 100644 index 0000000..e7184c6 --- /dev/null +++ b/actions/create_or_update.maintenance.yaml @@ -0,0 +1,32 @@ +--- +name: create_or_update.maintenance +pack: zabbix +runner_type: python-script +description: "Create a maintenance window or update it if one with the same name exists." +enabled: true +entry_point: create_or_update_maintenance.py +parameters: + hostname: + type: string + description: "Name of the Zabbix host to put in maintenance." + required: true + maintenance_window_name: + type: string + description: "Name of the maintenance window." + required: true + start_date: + type: string + description: "Start date/time in format 'Y-m-d H:M'." + required: true + end_date: + type: string + description: "End date/time in format 'Y-m-d H:M'." + required: true + time_type: + type: integer + description: "Period type: 0 (one time), 2 (daily), 3 (weekly), 4 (monthly)." + default: 0 + maintenance_type: + type: integer + description: "Type: 0 (with data collection), 1 (without data collection)." + default: 0 diff --git a/actions/create_or_update_maintenance.py b/actions/create_or_update_maintenance.py new file mode 100644 index 0000000..3c08d94 --- /dev/null +++ b/actions/create_or_update_maintenance.py @@ -0,0 +1,53 @@ +from datetime import datetime +from tzlocal import get_localzone +from lib.actions import ZabbixBaseAction + + +class MaintenanceCreateOrUpdate(ZabbixBaseAction): + """Create or update a Zabbix maintenance window.""" + + def run(self, hostname, maintenance_window_name, start_date, end_date, + time_type=0, maintenance_type=0): + """Create or update a maintenance window. + + Args: + hostname: Name of the Zabbix host. + maintenance_window_name: Name for the maintenance window. + start_date: Start datetime string (Y-m-d H:M). + end_date: End datetime string (Y-m-d H:M). + time_type: Period type (0=one time, 2=daily, 3=weekly, 4=monthly). + maintenance_type: 0=with data collection, 1=without. + """ + self.connect() + + host_id = self.find_host(hostname) + + local_tz = get_localzone() + + start_local = datetime.strptime(start_date, "%Y-%m-%d %H:%M") + start_local = start_local.replace(tzinfo=local_tz) + start_time = int(start_local.timestamp()) + + end_local = datetime.strptime(end_date, "%Y-%m-%d %H:%M") + end_local = end_local.replace(tzinfo=local_tz) + end_time = int(end_local.timestamp()) + + period = end_time - start_time + + time_period = [{ + 'start_date': start_time, + 'timeperiod_type': time_type, + 'period': period, + }] + + maintenance_params = { + 'hosts': [{'hostid': host_id}], + 'name': maintenance_window_name, + 'active_since': start_time, + 'active_till': end_time, + 'maintenance_type': maintenance_type, + 'timeperiods': time_period, + } + + result = self.maintenance_create_or_update(maintenance_params) + return result['maintenanceids'][0] diff --git a/actions/delete.action.yaml b/actions/delete.action.yaml new file mode 100644 index 0000000..b9294b6 --- /dev/null +++ b/actions/delete.action.yaml @@ -0,0 +1,15 @@ +--- +name: delete.action +pack: zabbix +runner_type: python-script +description: "Delete alert actions by ID." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "action.delete" + immutable: true + params_list: + type: array + description: "List of action IDs to delete." + required: true diff --git a/actions/delete.correlation.yaml b/actions/delete.correlation.yaml new file mode 100644 index 0000000..555b588 --- /dev/null +++ b/actions/delete.correlation.yaml @@ -0,0 +1,15 @@ +--- +name: delete.correlation +pack: zabbix +runner_type: python-script +description: "Delete event correlations by ID." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "correlation.delete" + immutable: true + params_list: + type: array + description: "List of correlation IDs to delete." + required: true diff --git a/actions/delete.dashboard.yaml b/actions/delete.dashboard.yaml new file mode 100644 index 0000000..22e4fc5 --- /dev/null +++ b/actions/delete.dashboard.yaml @@ -0,0 +1,15 @@ +--- +name: delete.dashboard +pack: zabbix +runner_type: python-script +description: "Delete dashboards by ID." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "dashboard.delete" + immutable: true + params_list: + type: array + description: "List of dashboard IDs to delete." + required: true diff --git a/actions/delete.drule.yaml b/actions/delete.drule.yaml new file mode 100644 index 0000000..d982ef8 --- /dev/null +++ b/actions/delete.drule.yaml @@ -0,0 +1,15 @@ +--- +name: delete.drule +pack: zabbix +runner_type: python-script +description: "Delete discovery rules by ID." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "drule.delete" + immutable: true + params_list: + type: array + description: "List of discovery rule IDs to delete." + required: true diff --git a/actions/delete.graph.yaml b/actions/delete.graph.yaml new file mode 100644 index 0000000..f3ecafe --- /dev/null +++ b/actions/delete.graph.yaml @@ -0,0 +1,15 @@ +--- +name: delete.graph +pack: zabbix +runner_type: python-script +description: "Delete graphs by ID." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "graph.delete" + immutable: true + params_list: + type: array + description: "List of graph IDs to delete." + required: true diff --git a/actions/delete.host.by_id.yaml b/actions/delete.host.by_id.yaml new file mode 100644 index 0000000..f7047e2 --- /dev/null +++ b/actions/delete.host.by_id.yaml @@ -0,0 +1,12 @@ +--- +name: delete.host.by_id +pack: zabbix +runner_type: python-script +description: "Delete a Zabbix host by host ID." +enabled: true +entry_point: delete_host.py +parameters: + host_id: + type: string + description: "ID of the Zabbix host to delete." + required: true diff --git a/actions/delete.host.yaml b/actions/delete.host.yaml new file mode 100644 index 0000000..c71d14f --- /dev/null +++ b/actions/delete.host.yaml @@ -0,0 +1,12 @@ +--- +name: delete.host +pack: zabbix +runner_type: python-script +description: "Delete a Zabbix host by hostname." +enabled: true +entry_point: delete_host.py +parameters: + hostname: + type: string + description: "Name of the Zabbix host to delete." + required: true diff --git a/actions/delete.hostgroup.yaml b/actions/delete.hostgroup.yaml new file mode 100644 index 0000000..f2dd038 --- /dev/null +++ b/actions/delete.hostgroup.yaml @@ -0,0 +1,15 @@ +--- +name: delete.hostgroup +pack: zabbix +runner_type: python-script +description: "Delete host groups by ID." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "hostgroup.delete" + immutable: true + params_list: + type: array + description: "List of host group IDs to delete." + required: true diff --git a/actions/delete.hostinterface.yaml b/actions/delete.hostinterface.yaml new file mode 100644 index 0000000..13b683c --- /dev/null +++ b/actions/delete.hostinterface.yaml @@ -0,0 +1,15 @@ +--- +name: delete.hostinterface +pack: zabbix +runner_type: python-script +description: "Delete host interfaces by ID." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "hostinterface.delete" + immutable: true + params_list: + type: array + description: "List of interface IDs to delete." + required: true diff --git a/actions/delete.httptest.yaml b/actions/delete.httptest.yaml new file mode 100644 index 0000000..8f2dc17 --- /dev/null +++ b/actions/delete.httptest.yaml @@ -0,0 +1,15 @@ +--- +name: delete.httptest +pack: zabbix +runner_type: python-script +description: "Delete web scenarios by ID." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "httptest.delete" + immutable: true + params_list: + type: array + description: "List of web scenario IDs to delete." + required: true diff --git a/actions/delete.item.yaml b/actions/delete.item.yaml new file mode 100644 index 0000000..4bfe889 --- /dev/null +++ b/actions/delete.item.yaml @@ -0,0 +1,15 @@ +--- +name: delete.item +pack: zabbix +runner_type: python-script +description: "Delete monitoring items by ID." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "item.delete" + immutable: true + params_list: + type: array + description: "List of item IDs to delete." + required: true diff --git a/actions/delete.maintenance.yaml b/actions/delete.maintenance.yaml new file mode 100644 index 0000000..330d864 --- /dev/null +++ b/actions/delete.maintenance.yaml @@ -0,0 +1,16 @@ +--- +name: delete.maintenance +pack: zabbix +runner_type: python-script +description: "Delete a maintenance window by name or ID." +enabled: true +entry_point: delete_maintenance.py +parameters: + maintenance_id: + type: string + description: "ID of the maintenance window to delete." + required: false + maintenance_window_name: + type: string + description: "Name of the maintenance window to delete." + required: false diff --git a/actions/delete.map.yaml b/actions/delete.map.yaml new file mode 100644 index 0000000..372b0d8 --- /dev/null +++ b/actions/delete.map.yaml @@ -0,0 +1,15 @@ +--- +name: delete.map +pack: zabbix +runner_type: python-script +description: "Delete network maps by ID." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "map.delete" + immutable: true + params_list: + type: array + description: "List of map IDs to delete." + required: true diff --git a/actions/delete.mediatype.yaml b/actions/delete.mediatype.yaml new file mode 100644 index 0000000..d121eba --- /dev/null +++ b/actions/delete.mediatype.yaml @@ -0,0 +1,15 @@ +--- +name: delete.mediatype +pack: zabbix +runner_type: python-script +description: "Delete media types by ID." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "mediatype.delete" + immutable: true + params_list: + type: array + description: "List of media type IDs to delete." + required: true diff --git a/actions/delete.proxy.yaml b/actions/delete.proxy.yaml new file mode 100644 index 0000000..9217238 --- /dev/null +++ b/actions/delete.proxy.yaml @@ -0,0 +1,15 @@ +--- +name: delete.proxy +pack: zabbix +runner_type: python-script +description: "Delete proxies by ID." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "proxy.delete" + immutable: true + params_list: + type: array + description: "List of proxy IDs to delete." + required: true diff --git a/actions/delete.script.yaml b/actions/delete.script.yaml new file mode 100644 index 0000000..6e9dbca --- /dev/null +++ b/actions/delete.script.yaml @@ -0,0 +1,15 @@ +--- +name: delete.script +pack: zabbix +runner_type: python-script +description: "Delete scripts by ID." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "script.delete" + immutable: true + params_list: + type: array + description: "List of script IDs to delete." + required: true diff --git a/actions/delete.service.yaml b/actions/delete.service.yaml new file mode 100644 index 0000000..105e34d --- /dev/null +++ b/actions/delete.service.yaml @@ -0,0 +1,15 @@ +--- +name: delete.service +pack: zabbix +runner_type: python-script +description: "Delete services by ID." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "service.delete" + immutable: true + params_list: + type: array + description: "List of service IDs to delete." + required: true diff --git a/actions/delete.template.yaml b/actions/delete.template.yaml new file mode 100644 index 0000000..d907ca2 --- /dev/null +++ b/actions/delete.template.yaml @@ -0,0 +1,15 @@ +--- +name: delete.template +pack: zabbix +runner_type: python-script +description: "Delete templates by ID." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "template.delete" + immutable: true + params_list: + type: array + description: "List of template IDs to delete." + required: true diff --git a/actions/delete.token.yaml b/actions/delete.token.yaml new file mode 100644 index 0000000..57ccbc1 --- /dev/null +++ b/actions/delete.token.yaml @@ -0,0 +1,15 @@ +--- +name: delete.token +pack: zabbix +runner_type: python-script +description: "Delete API tokens by ID." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "token.delete" + immutable: true + params_list: + type: array + description: "List of token IDs to delete." + required: true diff --git a/actions/delete.trigger.yaml b/actions/delete.trigger.yaml new file mode 100644 index 0000000..c205cda --- /dev/null +++ b/actions/delete.trigger.yaml @@ -0,0 +1,15 @@ +--- +name: delete.trigger +pack: zabbix +runner_type: python-script +description: "Delete triggers by ID." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "trigger.delete" + immutable: true + params_list: + type: array + description: "List of trigger IDs to delete." + required: true diff --git a/actions/delete.user.yaml b/actions/delete.user.yaml new file mode 100644 index 0000000..ea0f704 --- /dev/null +++ b/actions/delete.user.yaml @@ -0,0 +1,15 @@ +--- +name: delete.user +pack: zabbix +runner_type: python-script +description: "Delete users by ID." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "user.delete" + immutable: true + params_list: + type: array + description: "List of user IDs to delete." + required: true diff --git a/actions/delete.usermacro.global.yaml b/actions/delete.usermacro.global.yaml new file mode 100644 index 0000000..26ea074 --- /dev/null +++ b/actions/delete.usermacro.global.yaml @@ -0,0 +1,15 @@ +--- +name: delete.usermacro.global +pack: zabbix +runner_type: python-script +description: "Delete global user macros by ID." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "usermacro.deleteglobal" + immutable: true + params_list: + type: array + description: "List of global macro IDs to delete." + required: true diff --git a/actions/delete.usermacro.yaml b/actions/delete.usermacro.yaml new file mode 100644 index 0000000..4d5dec5 --- /dev/null +++ b/actions/delete.usermacro.yaml @@ -0,0 +1,15 @@ +--- +name: delete.usermacro +pack: zabbix +runner_type: python-script +description: "Delete host user macros by ID." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "usermacro.delete" + immutable: true + params_list: + type: array + description: "List of host macro IDs to delete." + required: true diff --git a/actions/delete.valuemap.yaml b/actions/delete.valuemap.yaml new file mode 100644 index 0000000..1c7ad21 --- /dev/null +++ b/actions/delete.valuemap.yaml @@ -0,0 +1,15 @@ +--- +name: delete.valuemap +pack: zabbix +runner_type: python-script +description: "Delete value maps by ID." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "valuemap.delete" + immutable: true + params_list: + type: array + description: "List of value map IDs to delete." + required: true diff --git a/actions/delete_host.py b/actions/delete_host.py new file mode 100644 index 0000000..644a08f --- /dev/null +++ b/actions/delete_host.py @@ -0,0 +1,25 @@ +from lib.actions import ZabbixBaseAction +from zabbix_utils.exceptions import APIRequestError + + +class HostDelete(ZabbixBaseAction): + """Delete a Zabbix host by hostname or ID.""" + + def run(self, hostname=None, host_id=None): + """Delete a host. + + Args: + hostname: Name of the host to delete (resolved to ID). + host_id: Direct host ID to delete. + """ + self.connect() + + if not host_id: + host_id = self.find_host(hostname) + + try: + self.client.host.delete(host_id) + return True + except APIRequestError as e: + raise APIRequestError( + "Failed to delete host: {0}".format(e)) diff --git a/actions/delete_maintenance.py b/actions/delete_maintenance.py new file mode 100644 index 0000000..216efbe --- /dev/null +++ b/actions/delete_maintenance.py @@ -0,0 +1,40 @@ +from lib.actions import ZabbixBaseAction +from zabbix_utils.exceptions import APIRequestError + + +class MaintenanceDelete(ZabbixBaseAction): + """Delete a Zabbix maintenance window by name or ID.""" + + def run(self, maintenance_id=None, maintenance_window_name=None): + """Delete a maintenance window. + + Args: + maintenance_id: ID of the maintenance window to delete. + maintenance_window_name: Name of the maintenance window to delete. + """ + self.connect() + + if maintenance_window_name is not None: + maintenance_result = self.maintenance_get(maintenance_window_name) + + if len(maintenance_result) == 0: + raise ValueError( + "Could not find maintenance window: {0}".format( + maintenance_window_name)) + elif len(maintenance_result) == 1: + maintenance_id = maintenance_result[0]['maintenanceid'] + else: + raise ValueError( + "Multiple maintenance windows found: {0}".format( + maintenance_window_name)) + elif maintenance_id is None: + raise ValueError( + "Must provide either maintenance_window_name or maintenance_id") + + try: + self.client.maintenance.delete(maintenance_id) + except APIRequestError as e: + raise APIRequestError( + "Failed to delete maintenance window: {0}".format(e)) + + return True diff --git a/actions/event_action_runner.py b/actions/event_action_runner.py deleted file mode 100644 index 558d93f..0000000 --- a/actions/event_action_runner.py +++ /dev/null @@ -1,33 +0,0 @@ -# Licensed to the StackStorm, Inc ('StackStorm') under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from lib.actions import ZabbixBaseAction - - -class EventActionRunner(ZabbixBaseAction): - def run(self, action, *args, **kwargs): - self.connect() - - if action == 'event.acknowledge': - kwargs = self.reconstruct_args_for_ack_event(*args, **kwargs) - - try: - api_handler = self.client - for obj in action.split('.'): - api_handler = getattr(api_handler, obj) - - return (True, api_handler(*args, **kwargs)) - except AttributeError: - return (False, "Specified action(%s) is invalid" % action) diff --git a/actions/execute.script.yaml b/actions/execute.script.yaml new file mode 100644 index 0000000..df1020e --- /dev/null +++ b/actions/execute.script.yaml @@ -0,0 +1,23 @@ +--- +name: execute.script +pack: zabbix +runner_type: python-script +description: "Execute a Zabbix script on a host." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "script.execute" + immutable: true + scriptid: + type: string + description: "ID of the script to execute." + required: true + hostid: + type: string + description: "ID of the host to execute the script on." + required: true + eventid: + type: string + description: "ID of the event to execute the script for (if scope permits)." + required: false diff --git a/actions/export.configuration.yaml b/actions/export.configuration.yaml new file mode 100644 index 0000000..85942a9 --- /dev/null +++ b/actions/export.configuration.yaml @@ -0,0 +1,19 @@ +--- +name: export.configuration +pack: zabbix +runner_type: python-script +description: "Export Zabbix configuration data." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "configuration.export" + immutable: true + format: + type: string + description: "Export format: yaml, xml, or json." + default: "yaml" + options: + type: object + description: "Objects to export (e.g. {'hosts': ['10084'], 'templates': ['10001']})." + required: true diff --git a/actions/find.host.yaml b/actions/find.host.yaml new file mode 100644 index 0000000..acf0106 --- /dev/null +++ b/actions/find.host.yaml @@ -0,0 +1,24 @@ +--- +name: find.host +pack: zabbix +runner_type: python-script +description: "Resolve a Zabbix hostname to its host ID. Returns exactly one ID or raises an error." +enabled: true +entry_point: find_object.py +parameters: + object_type: + default: "host" + immutable: true + filter_field: + default: "host" + immutable: true + id_field: + default: "hostid" + immutable: true + allow_multiple: + default: false + immutable: true + name: + type: string + description: "Hostname or technical name of the Zabbix host." + required: true diff --git a/actions/find.hostgroup.yaml b/actions/find.hostgroup.yaml new file mode 100644 index 0000000..9ae665c --- /dev/null +++ b/actions/find.hostgroup.yaml @@ -0,0 +1,24 @@ +--- +name: find.hostgroup +pack: zabbix +runner_type: python-script +description: "Resolve a host group name to its group ID." +enabled: true +entry_point: find_object.py +parameters: + object_type: + default: "hostgroup" + immutable: true + filter_field: + default: "name" + immutable: true + id_field: + default: "groupid" + immutable: true + allow_multiple: + default: false + immutable: true + name: + type: string + description: "Name of the host group." + required: true diff --git a/actions/find.hosts.yaml b/actions/find.hosts.yaml new file mode 100644 index 0000000..1d0636e --- /dev/null +++ b/actions/find.hosts.yaml @@ -0,0 +1,24 @@ +--- +name: find.hosts +pack: zabbix +runner_type: python-script +description: "Resolve one or more Zabbix hostnames to host IDs. Returns all matching IDs (may be empty)." +enabled: true +entry_point: find_object.py +parameters: + object_type: + default: "host" + immutable: true + filter_field: + default: "host" + immutable: true + id_field: + default: "hostid" + immutable: true + allow_multiple: + default: true + immutable: true + name: + type: array + description: "List of hostnames to resolve." + required: true diff --git a/actions/find.maintenance.yaml b/actions/find.maintenance.yaml new file mode 100644 index 0000000..0b33912 --- /dev/null +++ b/actions/find.maintenance.yaml @@ -0,0 +1,24 @@ +--- +name: find.maintenance +pack: zabbix +runner_type: python-script +description: "Resolve a maintenance window name to its maintenance ID." +enabled: true +entry_point: find_object.py +parameters: + object_type: + default: "maintenance" + immutable: true + filter_field: + default: "name" + immutable: true + id_field: + default: "maintenanceid" + immutable: true + allow_multiple: + default: false + immutable: true + name: + type: string + description: "Name of the maintenance window." + required: true diff --git a/actions/find.proxy.yaml b/actions/find.proxy.yaml new file mode 100644 index 0000000..5d92ed8 --- /dev/null +++ b/actions/find.proxy.yaml @@ -0,0 +1,24 @@ +--- +name: find.proxy +pack: zabbix +runner_type: python-script +description: "Resolve a proxy name to its proxy ID." +enabled: true +entry_point: find_object.py +parameters: + object_type: + default: "proxy" + immutable: true + filter_field: + default: "host" + immutable: true + id_field: + default: "proxyid" + immutable: true + allow_multiple: + default: false + immutable: true + name: + type: string + description: "Name of the Zabbix proxy." + required: true diff --git a/actions/find.script.yaml b/actions/find.script.yaml new file mode 100644 index 0000000..22e1a6a --- /dev/null +++ b/actions/find.script.yaml @@ -0,0 +1,24 @@ +--- +name: find.script +pack: zabbix +runner_type: python-script +description: "Resolve a script name to its script ID." +enabled: true +entry_point: find_object.py +parameters: + object_type: + default: "script" + immutable: true + filter_field: + default: "name" + immutable: true + id_field: + default: "scriptid" + immutable: true + allow_multiple: + default: false + immutable: true + name: + type: string + description: "Name of the script." + required: true diff --git a/actions/find.template.yaml b/actions/find.template.yaml new file mode 100644 index 0000000..f15920d --- /dev/null +++ b/actions/find.template.yaml @@ -0,0 +1,24 @@ +--- +name: find.template +pack: zabbix +runner_type: python-script +description: "Resolve a template name to its template ID." +enabled: true +entry_point: find_object.py +parameters: + object_type: + default: "template" + immutable: true + filter_field: + default: "host" + immutable: true + id_field: + default: "templateid" + immutable: true + allow_multiple: + default: false + immutable: true + name: + type: string + description: "Technical name of the template." + required: true diff --git a/actions/find_object.py b/actions/find_object.py new file mode 100644 index 0000000..ecb9855 --- /dev/null +++ b/actions/find_object.py @@ -0,0 +1,43 @@ +from lib.actions import ZabbixBaseAction +from zabbix_utils.exceptions import APIRequestError + + +class FindObject(ZabbixBaseAction): + """Generic name-to-ID resolution for Zabbix objects.""" + + def run(self, object_type, filter_field, id_field, name, allow_multiple=False): + """Resolve a friendly name to an object ID. + + Args: + object_type: Zabbix API object type (e.g. 'host', 'hostgroup'). + filter_field: Field to filter on (e.g. 'host', 'name'). + id_field: Field containing the ID in results (e.g. 'hostid', 'groupid'). + name: Name value(s) to search for (string or array). + allow_multiple: If True, return all matching IDs as a list. + """ + self.connect() + + try: + api_object = getattr(self.client, object_type) + except AttributeError: + raise ValueError("Invalid object type: {0}".format(object_type)) + + try: + results = api_object.get(filter={filter_field: name}) + except APIRequestError as e: + raise APIRequestError( + "Error searching for {0}: {1}".format(object_type, e)) + + if allow_multiple: + return [r[id_field] for r in results] + + if len(results) == 0: + raise ValueError( + "Could not find {0} with {1}={2}".format( + object_type, filter_field, name)) + if len(results) > 1: + raise ValueError( + "Multiple {0} found with {1}={2}".format( + object_type, filter_field, name)) + + return results[0][id_field] diff --git a/actions/generate.token.yaml b/actions/generate.token.yaml new file mode 100644 index 0000000..009b6c7 --- /dev/null +++ b/actions/generate.token.yaml @@ -0,0 +1,15 @@ +--- +name: generate.token +pack: zabbix +runner_type: python-script +description: "Generate (reveal) the auth string for an API token." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "token.generate" + immutable: true + params_list: + type: array + description: "List of token IDs to generate auth strings for." + required: true diff --git a/actions/get.action.yaml b/actions/get.action.yaml new file mode 100644 index 0000000..19a192d --- /dev/null +++ b/actions/get.action.yaml @@ -0,0 +1,31 @@ +--- +name: get.action +pack: zabbix +runner_type: python-script +description: "Get an alert action by ID." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "action.get" + immutable: true + actionids: + type: array + description: "Action IDs to retrieve." + required: true + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" + selectOperations: + type: string + description: "Include operations. Set to 'extend'." + required: false + selectRecoveryOperations: + type: string + description: "Include recovery operations. Set to 'extend'." + required: false + selectFilter: + type: string + description: "Include filter. Set to 'extend'." + required: false diff --git a/actions/get.api_version.yaml b/actions/get.api_version.yaml new file mode 100644 index 0000000..b1b3b4c --- /dev/null +++ b/actions/get.api_version.yaml @@ -0,0 +1,8 @@ +--- +name: get.api_version +pack: zabbix +runner_type: python-script +description: "Get the Zabbix API version string." +enabled: true +entry_point: get_api_version.py +parameters: {} diff --git a/actions/get.dashboard.yaml b/actions/get.dashboard.yaml new file mode 100644 index 0000000..728cb21 --- /dev/null +++ b/actions/get.dashboard.yaml @@ -0,0 +1,23 @@ +--- +name: get.dashboard +pack: zabbix +runner_type: python-script +description: "Get a dashboard by ID." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "dashboard.get" + immutable: true + dashboardids: + type: array + description: "Dashboard IDs to retrieve." + required: true + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" + selectPages: + type: string + description: "Include dashboard pages. Set to 'extend'." + required: false diff --git a/actions/get.graph.yaml b/actions/get.graph.yaml new file mode 100644 index 0000000..c70afc4 --- /dev/null +++ b/actions/get.graph.yaml @@ -0,0 +1,23 @@ +--- +name: get.graph +pack: zabbix +runner_type: python-script +description: "Get a graph by ID." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "graph.get" + immutable: true + graphids: + type: array + description: "Graph IDs to retrieve." + required: true + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" + selectGraphItems: + type: string + description: "Include graph items. Set to 'extend'." + required: false diff --git a/actions/get.history.yaml b/actions/get.history.yaml new file mode 100644 index 0000000..d7f2fba --- /dev/null +++ b/actions/get.history.yaml @@ -0,0 +1,47 @@ +--- +name: get.history +pack: zabbix +runner_type: python-script +description: "Retrieve historical data values." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "history.get" + immutable: true + hostids: + type: array + description: "Filter by host IDs." + required: false + itemids: + type: array + description: "Filter by item IDs." + required: false + history: + type: integer + description: "History type: 0 (float), 1 (string), 2 (log), 3 (integer), 4 (text)." + required: false + time_from: + type: string + description: "Return only values after this time (unix timestamp)." + required: false + time_till: + type: string + description: "Return only values before this time (unix timestamp)." + required: false + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" + sortfield: + type: string + description: "Field to sort by (usually 'clock')." + required: false + sortorder: + type: string + description: "Sort order: ASC or DESC." + required: false + limit: + type: integer + description: "Maximum number of results." + required: false diff --git a/actions/get.host.active_triggers.yaml b/actions/get.host.active_triggers.yaml new file mode 100644 index 0000000..9a26ffa --- /dev/null +++ b/actions/get.host.active_triggers.yaml @@ -0,0 +1,16 @@ +--- +name: get.host.active_triggers +pack: zabbix +runner_type: orquesta +description: "Get active (in problem state) triggers for a host, optionally filtered by priority." +enabled: true +entry_point: workflows/get.host.active_triggers.yaml +parameters: + hostname: + type: string + description: "Name of the Zabbix host." + required: true + priority: + type: array + description: "List of priority numbers to filter by (0-5). Empty for all." + default: [] diff --git a/actions/get.host.groups.yaml b/actions/get.host.groups.yaml new file mode 100644 index 0000000..3fd1829 --- /dev/null +++ b/actions/get.host.groups.yaml @@ -0,0 +1,24 @@ +--- +name: get.host.groups +pack: zabbix +runner_type: python-script +description: "Get host group membership for one or more hosts by host ID." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "host.get" + immutable: true + hostids: + type: array + description: "List of host IDs to retrieve groups for." + required: true + selectGroups: + default: "extend" + immutable: true + output: + type: array + description: "Host fields to include alongside groups." + default: + - "hostid" + - "host" diff --git a/actions/get.host.interfaces.yaml b/actions/get.host.interfaces.yaml new file mode 100644 index 0000000..5ffd9a2 --- /dev/null +++ b/actions/get.host.interfaces.yaml @@ -0,0 +1,24 @@ +--- +name: get.host.interfaces +pack: zabbix +runner_type: python-script +description: "Get interfaces for one or more hosts by host ID." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "host.get" + immutable: true + hostids: + type: array + description: "List of host IDs to retrieve interfaces for." + required: true + selectInterfaces: + default: "extend" + immutable: true + output: + type: array + description: "Host fields to include alongside interfaces." + default: + - "hostid" + - "host" diff --git a/actions/get.host.inventory.yaml b/actions/get.host.inventory.yaml new file mode 100644 index 0000000..5c7cb9a --- /dev/null +++ b/actions/get.host.inventory.yaml @@ -0,0 +1,24 @@ +--- +name: get.host.inventory +pack: zabbix +runner_type: python-script +description: "Get inventory data for one or more hosts by host ID." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "host.get" + immutable: true + hostids: + type: array + description: "List of host IDs to retrieve inventory for." + required: true + selectInventory: + default: "extend" + immutable: true + output: + type: array + description: "Host fields to include alongside inventory." + default: + - "hostid" + - "host" diff --git a/actions/get.host.status.yaml b/actions/get.host.status.yaml new file mode 100644 index 0000000..455de67 --- /dev/null +++ b/actions/get.host.status.yaml @@ -0,0 +1,12 @@ +--- +name: get.host.status +pack: zabbix +runner_type: python-script +description: "Get the monitoring status of a host by hostname." +enabled: true +entry_point: host_status.py +parameters: + hostname: + type: string + description: "Name of the Zabbix host." + required: true diff --git a/actions/get.host.yaml b/actions/get.host.yaml new file mode 100644 index 0000000..e8860d5 --- /dev/null +++ b/actions/get.host.yaml @@ -0,0 +1,43 @@ +--- +name: get.host +pack: zabbix +runner_type: python-script +description: "Get a Zabbix host object by ID with full details." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "host.get" + immutable: true + hostids: + type: array + description: "Host IDs to retrieve." + required: true + output: + type: string + description: "Output fields. Use 'extend' for all fields or provide a list." + default: "extend" + selectGroups: + type: string + description: "Include host groups. Set to 'extend' to include." + required: false + selectInterfaces: + type: string + description: "Include host interfaces. Set to 'extend' to include." + required: false + selectInventory: + type: string + description: "Include host inventory. Set to 'extend' to include." + required: false + selectMacros: + type: string + description: "Include host macros. Set to 'extend' to include." + required: false + selectParentTemplates: + type: string + description: "Include parent templates. Set to 'extend' to include." + required: false + selectTags: + type: string + description: "Include host tags. Set to 'extend' to include." + required: false diff --git a/actions/get.hostgroup.yaml b/actions/get.hostgroup.yaml new file mode 100644 index 0000000..d85c246 --- /dev/null +++ b/actions/get.hostgroup.yaml @@ -0,0 +1,19 @@ +--- +name: get.hostgroup +pack: zabbix +runner_type: python-script +description: "Get a host group by ID." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "hostgroup.get" + immutable: true + groupids: + type: array + description: "Host group IDs to retrieve." + required: true + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" diff --git a/actions/get.item.yaml b/actions/get.item.yaml new file mode 100644 index 0000000..f1aa631 --- /dev/null +++ b/actions/get.item.yaml @@ -0,0 +1,19 @@ +--- +name: get.item +pack: zabbix +runner_type: python-script +description: "Get an item by ID." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "item.get" + immutable: true + itemids: + type: array + description: "Item IDs to retrieve." + required: true + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" diff --git a/actions/get.maintenance.yaml b/actions/get.maintenance.yaml new file mode 100644 index 0000000..1822da5 --- /dev/null +++ b/actions/get.maintenance.yaml @@ -0,0 +1,31 @@ +--- +name: get.maintenance +pack: zabbix +runner_type: python-script +description: "Get a maintenance window by ID." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "maintenance.get" + immutable: true + maintenanceids: + type: array + description: "Maintenance IDs to retrieve." + required: true + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" + selectHosts: + type: string + description: "Include hosts. Set to 'extend'." + required: false + selectGroups: + type: string + description: "Include groups. Set to 'extend'." + required: false + selectTimeperiods: + type: string + description: "Include time periods. Set to 'extend'." + required: false diff --git a/actions/get.map.yaml b/actions/get.map.yaml new file mode 100644 index 0000000..9d2c010 --- /dev/null +++ b/actions/get.map.yaml @@ -0,0 +1,27 @@ +--- +name: get.map +pack: zabbix +runner_type: python-script +description: "Get a network map by ID." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "map.get" + immutable: true + sysmapids: + type: array + description: "Map IDs to retrieve." + required: true + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" + selectSelements: + type: string + description: "Include map elements. Set to 'extend'." + required: false + selectLinks: + type: string + description: "Include map links. Set to 'extend'." + required: false diff --git a/actions/get.mediatype.yaml b/actions/get.mediatype.yaml new file mode 100644 index 0000000..3ba139f --- /dev/null +++ b/actions/get.mediatype.yaml @@ -0,0 +1,19 @@ +--- +name: get.mediatype +pack: zabbix +runner_type: python-script +description: "Get a media type by ID." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "mediatype.get" + immutable: true + mediatypeids: + type: array + description: "Media type IDs to retrieve." + required: true + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" diff --git a/actions/get.proxy.yaml b/actions/get.proxy.yaml new file mode 100644 index 0000000..be882ff --- /dev/null +++ b/actions/get.proxy.yaml @@ -0,0 +1,27 @@ +--- +name: get.proxy +pack: zabbix +runner_type: python-script +description: "Get a proxy by ID." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "proxy.get" + immutable: true + proxyids: + type: array + description: "Proxy IDs to retrieve." + required: true + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" + selectHosts: + type: string + description: "Include hosts monitored by proxy. Set to 'extend'." + required: false + selectInterface: + type: string + description: "Include proxy interface. Set to 'extend'." + required: false diff --git a/actions/get.script.yaml b/actions/get.script.yaml new file mode 100644 index 0000000..5381192 --- /dev/null +++ b/actions/get.script.yaml @@ -0,0 +1,19 @@ +--- +name: get.script +pack: zabbix +runner_type: python-script +description: "Get a script by ID." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "script.get" + immutable: true + scriptids: + type: array + description: "Script IDs to retrieve." + required: true + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" diff --git a/actions/get.service.yaml b/actions/get.service.yaml new file mode 100644 index 0000000..5f89795 --- /dev/null +++ b/actions/get.service.yaml @@ -0,0 +1,27 @@ +--- +name: get.service +pack: zabbix +runner_type: python-script +description: "Get a service by ID." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "service.get" + immutable: true + serviceids: + type: array + description: "Service IDs to retrieve." + required: true + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" + selectChildren: + type: string + description: "Include child services. Set to 'extend'." + required: false + selectParents: + type: string + description: "Include parent services. Set to 'extend'." + required: false diff --git a/actions/get.sla.yaml b/actions/get.sla.yaml new file mode 100644 index 0000000..9fc10f5 --- /dev/null +++ b/actions/get.sla.yaml @@ -0,0 +1,27 @@ +--- +name: get.sla +pack: zabbix +runner_type: python-script +description: "Get an SLA definition by ID." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "sla.get" + immutable: true + slaids: + type: array + description: "SLA IDs to retrieve." + required: true + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" + selectSchedule: + type: string + description: "Include schedule. Set to 'extend'." + required: false + selectServiceTags: + type: string + description: "Include service tags. Set to 'extend'." + required: false diff --git a/actions/get.template.yaml b/actions/get.template.yaml new file mode 100644 index 0000000..0be8655 --- /dev/null +++ b/actions/get.template.yaml @@ -0,0 +1,19 @@ +--- +name: get.template +pack: zabbix +runner_type: python-script +description: "Get a template by ID." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "template.get" + immutable: true + templateids: + type: array + description: "Template IDs to retrieve." + required: true + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" diff --git a/actions/get.trend.yaml b/actions/get.trend.yaml new file mode 100644 index 0000000..d9a8608 --- /dev/null +++ b/actions/get.trend.yaml @@ -0,0 +1,31 @@ +--- +name: get.trend +pack: zabbix +runner_type: python-script +description: "Retrieve trend data (hourly aggregated values)." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "trend.get" + immutable: true + itemids: + type: array + description: "Item IDs to retrieve trends for." + required: true + time_from: + type: string + description: "Return only trends after this time (unix timestamp)." + required: false + time_till: + type: string + description: "Return only trends before this time (unix timestamp)." + required: false + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" + limit: + type: integer + description: "Maximum number of results." + required: false diff --git a/actions/get.trigger.yaml b/actions/get.trigger.yaml new file mode 100644 index 0000000..27afd47 --- /dev/null +++ b/actions/get.trigger.yaml @@ -0,0 +1,23 @@ +--- +name: get.trigger +pack: zabbix +runner_type: python-script +description: "Get a trigger by ID." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "trigger.get" + immutable: true + triggerids: + type: array + description: "Trigger IDs to retrieve." + required: true + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" + expandDescription: + type: boolean + description: "Expand macros in trigger description." + required: false diff --git a/actions/get.user.yaml b/actions/get.user.yaml new file mode 100644 index 0000000..db91126 --- /dev/null +++ b/actions/get.user.yaml @@ -0,0 +1,27 @@ +--- +name: get.user +pack: zabbix +runner_type: python-script +description: "Get a user by ID." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "user.get" + immutable: true + userids: + type: array + description: "User IDs to retrieve." + required: true + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" + selectMedias: + type: string + description: "Include user medias. Set to 'extend'." + required: false + selectUsrgrps: + type: string + description: "Include user groups. Set to 'extend'." + required: false diff --git a/actions/get_api_version.py b/actions/get_api_version.py new file mode 100644 index 0000000..ee550a2 --- /dev/null +++ b/actions/get_api_version.py @@ -0,0 +1,9 @@ +from lib.actions import ZabbixBaseAction + + +class GetApiVersion(ZabbixBaseAction): + """Get the Zabbix API version.""" + + def run(self): + self.connect() + return str(self.client.api_version()) diff --git a/actions/host_delete.py b/actions/host_delete.py deleted file mode 100644 index 80b13c7..0000000 --- a/actions/host_delete.py +++ /dev/null @@ -1,33 +0,0 @@ -# Licensed to the StackStorm, Inc ('StackStorm') under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from lib.actions import ZabbixBaseAction -from pyzabbix.api import ZabbixAPIException - - -class HostDelete(ZabbixBaseAction): - def run(self, host=None, host_id=None): - """ Deletes a Zabbix Host. - """ - self.connect() - - if not host_id: - host_id = self.find_host(host) - - try: - self.client.host.delete(host_id) - return True - except ZabbixAPIException as e: - raise ZabbixAPIException("There was a problem deleting the host: {0}".format(e)) diff --git a/actions/host_delete.yaml b/actions/host_delete.yaml deleted file mode 100644 index f8a5b67..0000000 --- a/actions/host_delete.yaml +++ /dev/null @@ -1,12 +0,0 @@ ---- -name: host_delete -pack: zabbix -runner_type: python-script -description: Delete a Zabbix Host -enabled: true -entry_point: host_delete.py -parameters: - host: - type: string - description: "Name of the Zabbix Host" - required: True diff --git a/actions/host_delete_by_id.yaml b/actions/host_delete_by_id.yaml deleted file mode 100644 index ec616b7..0000000 --- a/actions/host_delete_by_id.yaml +++ /dev/null @@ -1,12 +0,0 @@ ---- -name: host_delete_by_id -pack: zabbix -runner_type: python-script -description: Delete a Zabbix Host by it's Id -enabled: true -entry_point: host_delete.py -parameters: - host_id: - type: string - description: "Id of the Zabbix Host" - required: True diff --git a/actions/host_get_active_triggers.yaml b/actions/host_get_active_triggers.yaml deleted file mode 100644 index 1735e49..0000000 --- a/actions/host_get_active_triggers.yaml +++ /dev/null @@ -1,17 +0,0 @@ ---- -description: "List all active triggers for a given host" -enabled: true -runner_type: orquesta -entry_point: workflows/host_get_active_triggers.yaml -name: host_get_active_triggers -pack: zabbix -parameters: - host: - type: string - description: "Name of the Zabbix Host" - required: True - priority: - type: array - description: "List of priority numbers (severity) to get triggers for" - required: False - default: [] \ No newline at end of file diff --git a/actions/host_get_alerts.yaml b/actions/host_get_alerts.yaml deleted file mode 100644 index c1b23a3..0000000 --- a/actions/host_get_alerts.yaml +++ /dev/null @@ -1,25 +0,0 @@ ---- -name: host_get_alerts -pack: zabbix -runner_type: python-script -description: List all alerts for a given host ID in Zabbix -enabled: true -entry_point: call_api.py -parameters: - expandDescription: - type: string - description: "test flag" - default: "" - immutable: true - filter: - type: object - description: Condition to filter the result - token: - type: string - description: Encrypted access token to authenticate to ZabbixServer - default: | - {% if st2kv.user.zabbix.secret_token|string != '' %}{{ st2kv.user.zabbix.secret_token | decrypt_kv }}{% endif %} - secret: true - api_method: - default: alert.get - immutable: true diff --git a/actions/host_get_events.yaml b/actions/host_get_events.yaml deleted file mode 100644 index 66243b6..0000000 --- a/actions/host_get_events.yaml +++ /dev/null @@ -1,25 +0,0 @@ ---- -name: host_get_events -pack: zabbix -runner_type: python-script -description: List all events for a given host in Zabbix -enabled: true -entry_point: call_api.py -parameters: - expandDescription: - type: string - description: "test flag" - default: "" - immutable: true - filter: - type: object - description: Condition to filter the result - token: - type: string - description: Encrypted access token to authenticate to ZabbixServer - default: | - {% if st2kv.user.zabbix.secret_token|string != '' %}{{ st2kv.user.zabbix.secret_token | decrypt_kv }}{% endif %} - secret: true - api_method: - default: event.get - immutable: true diff --git a/actions/host_get_hostgroups.py b/actions/host_get_hostgroups.py deleted file mode 100644 index d82e998..0000000 --- a/actions/host_get_hostgroups.py +++ /dev/null @@ -1,44 +0,0 @@ -# Licensed to the StackStorm, Inc ('StackStorm') under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from lib.actions import ZabbixBaseAction -from pyzabbix.api import ZabbixAPIException - - -class HostGetHostGroups(ZabbixBaseAction): - - def run(self, host_id, group_id): - """ Gets the hostgroups of one or more Zabbix Hosts. - """ - self.connect() - - # Find hostgroups by host ids - try: - hostgroups = self.client.host.get( - hostids=host_id, selectGroups='extend', output=['hostid', 'groups']) - except ZabbixAPIException as e: - raise ZabbixAPIException(("There was a problem searching for the host: " - "{0}".format(e))) - - # if group ids are passed in we check to see if the host is a part of said groups - if group_id: - for group in hostgroups[0]["groups"]: - if group["groupid"] == group_id: - return hostgroups - - return (False, hostgroups) - # otherwise just return the groups the host is in - else: - return hostgroups diff --git a/actions/host_get_hostgroups.yaml b/actions/host_get_hostgroups.yaml deleted file mode 100644 index d4b5e24..0000000 --- a/actions/host_get_hostgroups.yaml +++ /dev/null @@ -1,16 +0,0 @@ ---- -name: host_get_hostgroups -pack: zabbix -runner_type: python-script -description: Gets/Checks the hostgroups of one or more Zabbix Hosts -enabled: true -entry_point: host_get_hostgroups.py -parameters: - group_id: - type: string - description: "Optional Group ID to check if host is in" - required: False - host_id: - type: string - description: "Host ID to find hostgroups for" - required: True \ No newline at end of file diff --git a/actions/host_get_id.py b/actions/host_get_id.py deleted file mode 100644 index d112082..0000000 --- a/actions/host_get_id.py +++ /dev/null @@ -1,28 +0,0 @@ -# Licensed to the StackStorm, Inc ('StackStorm') under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from lib.actions import ZabbixBaseAction - - -class HostGetID(ZabbixBaseAction): - def run(self, host=None): - """ Gets the ID of the Zabbix host given the Hostname or FQDN - of the Zabbix host. - """ - self.connect() - - host_id = self.find_host(host) - - return host_id diff --git a/actions/host_get_id.yaml b/actions/host_get_id.yaml deleted file mode 100644 index 2547a56..0000000 --- a/actions/host_get_id.yaml +++ /dev/null @@ -1,12 +0,0 @@ ---- -name: host_get_id -pack: zabbix -runner_type: python-script -description: Get the ID of a Zabbix Host -enabled: true -entry_point: host_get_id.py -parameters: - host: - type: string - description: "Name of the Zabbix Host" - required: True diff --git a/actions/host_get_interfaces.py b/actions/host_get_interfaces.py deleted file mode 100644 index 4164e8e..0000000 --- a/actions/host_get_interfaces.py +++ /dev/null @@ -1,35 +0,0 @@ -# Licensed to the StackStorm, Inc ('StackStorm') under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from lib.actions import ZabbixBaseAction -from pyzabbix.api import ZabbixAPIException - - -class HostGetInterfaces(ZabbixBaseAction): - - def run(self, host_ids=None): - """ Gets the interfaces of one or more Zabbix Hosts. - """ - self.connect() - - # Find interfaces by host ids - try: - interfaces = self.client.host.get( - hostids=host_ids, selectInterfaces='extend', output=['hostid', 'interfaces']) - except ZabbixAPIException as e: - raise ZabbixAPIException(("There was a problem searching for the host: " - "{0}".format(e))) - - return interfaces diff --git a/actions/host_get_interfaces.yaml b/actions/host_get_interfaces.yaml deleted file mode 100644 index 6295132..0000000 --- a/actions/host_get_interfaces.yaml +++ /dev/null @@ -1,12 +0,0 @@ ---- -name: host_get_interfaces -pack: zabbix -runner_type: python-script -description: Get the interfaces of one or more Zabbix Hosts -enabled: true -entry_point: host_get_interfaces.py -parameters: - host_ids: - type: array - description: "List of Host IDs to find inventory items for" - required: True \ No newline at end of file diff --git a/actions/host_get_inventory.py b/actions/host_get_inventory.py deleted file mode 100644 index 8764eee..0000000 --- a/actions/host_get_inventory.py +++ /dev/null @@ -1,35 +0,0 @@ -# Licensed to the StackStorm, Inc ('StackStorm') under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from lib.actions import ZabbixBaseAction -from pyzabbix.api import ZabbixAPIException - - -class HostGetInventory(ZabbixBaseAction): - - def run(self, host_ids=None): - """ Gets the inventory of one or more Zabbix Hosts. - """ - self.connect() - - # Find inventory by host ids - try: - inventory = self.client.host.get( - hostids=host_ids, selectInventory='extend', output=['hostid', 'inventory']) - except ZabbixAPIException as e: - raise ZabbixAPIException(("There was a problem searching for the host: " - "{0}".format(e))) - - return inventory diff --git a/actions/host_get_inventory.yaml b/actions/host_get_inventory.yaml deleted file mode 100644 index e521956..0000000 --- a/actions/host_get_inventory.yaml +++ /dev/null @@ -1,12 +0,0 @@ ---- -name: host_get_inventory -pack: zabbix -runner_type: python-script -description: Get the inventory of one or more Zabbix Hosts -enabled: true -entry_point: host_get_inventory.py -parameters: - host_ids: - type: array - description: "List of Host IDs to find inventory items for" - required: True \ No newline at end of file diff --git a/actions/host_get_multiple_ids.py b/actions/host_get_multiple_ids.py deleted file mode 100644 index 1ed056c..0000000 --- a/actions/host_get_multiple_ids.py +++ /dev/null @@ -1,47 +0,0 @@ -# Licensed to the StackStorm, Inc ('StackStorm') under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from lib.actions import ZabbixBaseAction -from pyzabbix.api import ZabbixAPIException - - -class ZabbixGetMultipleHostID(ZabbixBaseAction): - def find_hosts(self, host_name): - """ Queries the zabbix api for a host and returns just the - ids of the hosts as a list. If a host could not be found it - returns an empty list. - """ - try: - zabbix_hosts = self.client.host.get(filter={"host": host_name}) - except ZabbixAPIException as e: - raise ZabbixAPIException(("There was a problem searching for the host: " - "{0}".format(e))) - - zabbix_hosts_return = [] - if len(zabbix_hosts) > 0: - for host in zabbix_hosts: - zabbix_hosts_return.append(host['hostid']) - - return zabbix_hosts_return - - def run(self, host=None): - """ Gets the IDs of the Zabbix host given the Hostname or FQDN - of the Zabbix host. - """ - self.connect() - - zabbix_hosts = self.find_hosts(host) - - return {'host_ids': zabbix_hosts} diff --git a/actions/host_get_multiple_ids.yaml b/actions/host_get_multiple_ids.yaml deleted file mode 100644 index dac1107..0000000 --- a/actions/host_get_multiple_ids.yaml +++ /dev/null @@ -1,12 +0,0 @@ ---- -name: host_get_multiple_ids -pack: zabbix -runner_type: python-script -description: Get the IDs of multiple Zabbix Hosts -enabled: true -entry_point: host_get_multiple_ids.py -parameters: - host: - type: array - description: "Name of the Zabbix Hosts to retreive an ID for" - required: True diff --git a/actions/host_get_status.py b/actions/host_get_status.py deleted file mode 100644 index cbb860b..0000000 --- a/actions/host_get_status.py +++ /dev/null @@ -1,31 +0,0 @@ -# Licensed to the StackStorm, Inc ('StackStorm') under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from lib.actions import ZabbixBaseAction - - -class HostGetStatus(ZabbixBaseAction): - def run(self, host=None, status=None): - """ Gets the status of a Zabbix Host. - """ - self.connect() - - # Find current host and populate self.zabbix_host - self.find_host(host) - - # Get status from self.zabbix_host - host_status = self.zabbix_host['status'] - - return host_status diff --git a/actions/host_get_status.yaml b/actions/host_get_status.yaml deleted file mode 100644 index 1658627..0000000 --- a/actions/host_get_status.yaml +++ /dev/null @@ -1,12 +0,0 @@ ---- -name: host_get_status -pack: zabbix -runner_type: python-script -description: Get the status of a Zabbix Host -enabled: true -entry_point: host_get_status.py -parameters: - host: - type: string - description: "Name of the Zabbix Host" - required: True diff --git a/actions/host_get_triggers.yaml b/actions/host_get_triggers.yaml deleted file mode 100644 index 96b3998..0000000 --- a/actions/host_get_triggers.yaml +++ /dev/null @@ -1,25 +0,0 @@ ---- -name: host_get_triggers -pack: zabbix -runner_type: python-script -description: List all triggers for a given host in Zabbix -enabled: true -entry_point: call_api.py -parameters: - expandDescription: - type: string - description: "test flag" - default: "" - immutable: true - filter: - type: object - description: Condition to filter the result - token: - type: string - description: Encrypted access token to authenticate to ZabbixServer - default: | - {% if st2kv.user.zabbix.secret_token|string != '' %}{{ st2kv.user.zabbix.secret_token | decrypt_kv }}{% endif %} - secret: true - api_method: - default: trigger.get - immutable: true diff --git a/actions/host_status.py b/actions/host_status.py new file mode 100644 index 0000000..38117b5 --- /dev/null +++ b/actions/host_status.py @@ -0,0 +1,30 @@ +from lib.actions import ZabbixBaseAction +from zabbix_utils.exceptions import APIRequestError + + +class HostStatus(ZabbixBaseAction): + """Get or update host monitoring status by hostname.""" + + def run(self, hostname, status=None): + """Get or update host status. + + If status is provided, updates the host status. + If status is None, returns the current status. + + Args: + hostname: Name of the Zabbix host. + status: New status value (0=monitored, 1=unmonitored) or None to get. + """ + self.connect() + + host_id = self.find_host(hostname) + + if status is not None: + try: + self.client.host.update(hostid=host_id, status=status) + return True + except APIRequestError as e: + raise APIRequestError( + "Failed to update host status: {0}".format(e)) + else: + return self.zabbix_host['status'] diff --git a/actions/host_update_status.py b/actions/host_update_status.py deleted file mode 100644 index fad2c20..0000000 --- a/actions/host_update_status.py +++ /dev/null @@ -1,33 +0,0 @@ -# Licensed to the StackStorm, Inc ('StackStorm') under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from lib.actions import ZabbixBaseAction -from pyzabbix.api import ZabbixAPIException - - -class HostUpdateStatus(ZabbixBaseAction): - def run(self, host=None, status=None): - """ Updates the status of a Zabbix Host. Status needs to be - 1 or 0 for the call to succeed. - """ - self.connect() - - host_id = self.find_host(host) - - try: - self.client.host.update(hostid=host_id, status=status) - return True - except ZabbixAPIException as e: - raise ZabbixAPIException("There was a problem updating the host: {0}".format(e)) diff --git a/actions/host_update_status.yaml b/actions/host_update_status.yaml deleted file mode 100644 index efd6d2f..0000000 --- a/actions/host_update_status.yaml +++ /dev/null @@ -1,16 +0,0 @@ ---- -name: host_update_status -pack: zabbix -runner_type: python-script -description: Update the status of a Zabbix Host -enabled: true -entry_point: host_update_status.py -parameters: - host: - type: string - description: "Name of the Zabbix Host" - required: True - status: - type: integer - description: "Status to set the Zabbix Host to valid values: 0 - monitored host 1 - unmonitored host" - required: True diff --git a/actions/import.configuration.yaml b/actions/import.configuration.yaml new file mode 100644 index 0000000..d6047ed --- /dev/null +++ b/actions/import.configuration.yaml @@ -0,0 +1,23 @@ +--- +name: import.configuration +pack: zabbix +runner_type: python-script +description: "Import Zabbix configuration data." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "configuration.import_" + immutable: true + format: + type: string + description: "Import format: yaml, xml, or json." + required: true + source: + type: string + description: "Configuration data string to import." + required: true + rules: + type: object + description: "Import rules defining how to handle existing objects." + required: true diff --git a/actions/lib/actions.py b/actions/lib/actions.py index d7102bd..17a8fde 100644 --- a/actions/lib/actions.py +++ b/actions/lib/actions.py @@ -13,10 +13,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -from pyzabbix.api import ZabbixAPIException +from zabbix_utils import ZabbixAPI +from zabbix_utils.exceptions import APIRequestError, ProcessingError from st2common.runners.base_action import Action -from six.moves.urllib.error import URLError -from zabbix.api import ZabbixAPI class ZabbixBaseAction(Action): @@ -26,25 +25,34 @@ def __init__(self, config): self.config = config self.client = None - if self.config is not None and "zabbix" in self.config: - if "url" not in self.config['zabbix']: - raise ValueError("Zabbix url details not in the config.yaml") - elif "username" not in self.config['zabbix']: - raise ValueError("Zabbix user details not in the config.yaml") - elif "password" not in self.config['zabbix']: - raise ValueError("Zabbix password details not in the config.yaml") - else: + if not self.config: raise ValueError("Zabbix details not in the config.yaml") + if "url" not in self.config: + raise ValueError("Zabbix url details not in the config.yaml") + # Require either api_token or username+password + has_token = bool(self.config.get('api_token')) + has_user = bool(self.config.get('username') and + self.config.get('password')) + if not has_token and not has_user: + raise ValueError("Zabbix api_token or username/password " + "must be set in the config.yaml") + def connect(self): try: - self.client = ZabbixAPI(url=self.config['zabbix']['url'], - user=self.config['zabbix']['username'], - password=self.config['zabbix']['password']) - except ZabbixAPIException as e: - raise ZabbixAPIException("Failed to authenticate with Zabbix (%s)" % str(e)) - except URLError as e: - raise URLError("Failed to connect to Zabbix Server (%s)" % str(e)) + self.client = ZabbixAPI(url=self.config['url']) + api_token = self.config.get('api_token') + if api_token: + self.client.login(token=api_token) + else: + self.client.login( + user=self.config['username'], + password=self.config['password'] + ) + except APIRequestError as e: + raise APIRequestError("Failed to authenticate with Zabbix (%s)" % str(e)) + except ProcessingError as e: + raise ProcessingError("Failed to connect to Zabbix Server (%s)" % str(e)) except KeyError: raise KeyError("Configuration for Zabbix pack is not set yet") @@ -58,9 +66,9 @@ def reconstruct_args_for_ack_event(self, eventid, message, will_close): def find_host(self, host_name): try: zabbix_host = self.client.host.get(filter={"host": host_name}) - except ZabbixAPIException as e: - raise ZabbixAPIException(("There was a problem searching for the host: " - "{0}".format(e))) + except APIRequestError as e: + raise APIRequestError(("There was a problem searching for the host: " + "{0}".format(e))) if len(zabbix_host) == 0: raise ValueError("Could not find any hosts named {0}".format(host_name)) @@ -71,13 +79,35 @@ def find_host(self, host_name): return self.zabbix_host['hostid'] + def host_get_extended(self, host_ids, select_field, output_fields): + """Retrieve extended host data by IDs with a specified select parameter. + + Args: + host_ids: Host ID or list of host IDs. + select_field: The selectX parameter name (e.g. 'selectInterfaces'). + output_fields: List of output field names (e.g. ['hostid', 'interfaces']). + + Returns: + List of host dicts with the requested extended data. + """ + try: + kwargs = { + 'hostids': host_ids, + select_field: 'extend', + 'output': output_fields, + } + return self.client.host.get(**kwargs) + except APIRequestError as e: + raise APIRequestError( + "There was a problem searching for the host: {0}".format(e)) + def maintenance_get(self, maintenance_name): try: result = self.client.maintenance.get(filter={"name": maintenance_name}) return result - except ZabbixAPIException as e: - raise ZabbixAPIException(("There was a problem searching for the maintenance window: " - "{0}".format(e))) + except APIRequestError as e: + raise APIRequestError(("There was a problem searching for the maintenance window: " + "{0}".format(e))) def maintenance_create_or_update(self, maintenance_params): maintenance_result = self.maintenance_get(maintenance_params['name']) @@ -85,18 +115,18 @@ def maintenance_create_or_update(self, maintenance_params): try: create_result = self.client.maintenance.create(**maintenance_params) return create_result - except ZabbixAPIException as e: - raise ZabbixAPIException(("There was a problem creating the " - "maintenance window: {0}".format(e))) + except APIRequestError as e: + raise APIRequestError(("There was a problem creating the " + "maintenance window: {0}".format(e))) elif len(maintenance_result) == 1: try: maintenance_id = maintenance_result[0]['maintenanceid'] update_result = self.client.maintenance.update(maintenanceid=maintenance_id, **maintenance_params) return update_result - except ZabbixAPIException as e: - raise ZabbixAPIException(("There was a problem updating the " - "maintenance window: {0}".format(e))) + except APIRequestError as e: + raise APIRequestError(("There was a problem updating the " + "maintenance window: {0}".format(e))) elif len(maintenance_result) >= 2: raise ValueError(("There are multiple maintenance windows with the " "name: {0}").format(maintenance_params['name'])) diff --git a/actions/list.actions.yaml b/actions/list.actions.yaml new file mode 100644 index 0000000..5115f44 --- /dev/null +++ b/actions/list.actions.yaml @@ -0,0 +1,47 @@ +--- +name: list.actions +pack: zabbix +runner_type: python-script +description: "List Zabbix alert actions." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "action.get" + immutable: true + filter: + type: object + description: "Filter conditions." + required: false + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" + search: + type: object + description: "Search by pattern." + required: false + limit: + type: integer + description: "Maximum number of results." + required: false + sortfield: + type: string + description: "Field to sort by." + required: false + sortorder: + type: string + description: "Sort order: ASC or DESC." + required: false + actionids: + type: array + description: "Filter by action IDs." + required: false + selectOperations: + type: string + description: "Include operations. Set to 'extend'." + required: false + selectRecoveryOperations: + type: string + description: "Include recovery operations. Set to 'extend'." + required: false diff --git a/actions/list.alerts.yaml b/actions/list.alerts.yaml new file mode 100644 index 0000000..3219a15 --- /dev/null +++ b/actions/list.alerts.yaml @@ -0,0 +1,59 @@ +--- +name: list.alerts +pack: zabbix +runner_type: python-script +description: "List Zabbix alerts with optional filtering." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "alert.get" + immutable: true + hostids: + type: array + description: "Filter alerts by host IDs." + required: false + groupids: + type: array + description: "Filter alerts by host group IDs." + required: false + actionids: + type: array + description: "Filter by action IDs." + required: false + eventids: + type: array + description: "Filter by event IDs." + required: false + mediatypeids: + type: array + description: "Filter by media type IDs." + required: false + filter: + type: object + description: "Additional filter conditions." + required: false + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" + time_from: + type: string + description: "Return only alerts after this time (unix timestamp)." + required: false + time_till: + type: string + description: "Return only alerts before this time (unix timestamp)." + required: false + sortfield: + type: string + description: "Field to sort by." + required: false + sortorder: + type: string + description: "Sort order: ASC or DESC." + required: false + limit: + type: integer + description: "Maximum number of results." + required: false diff --git a/actions/list.correlations.yaml b/actions/list.correlations.yaml new file mode 100644 index 0000000..b74eb75 --- /dev/null +++ b/actions/list.correlations.yaml @@ -0,0 +1,47 @@ +--- +name: list.correlations +pack: zabbix +runner_type: python-script +description: "List event correlation rules." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "correlation.get" + immutable: true + filter: + type: object + description: "Filter conditions." + required: false + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" + search: + type: object + description: "Search by pattern." + required: false + limit: + type: integer + description: "Maximum number of results." + required: false + sortfield: + type: string + description: "Field to sort by." + required: false + sortorder: + type: string + description: "Sort order: ASC or DESC." + required: false + correlationids: + type: array + description: "Filter by correlation IDs." + required: false + selectOperations: + type: string + description: "Include operations. Set to 'extend'." + required: false + selectFilter: + type: string + description: "Include filter. Set to 'extend'." + required: false diff --git a/actions/list.dashboards.yaml b/actions/list.dashboards.yaml new file mode 100644 index 0000000..65d9060 --- /dev/null +++ b/actions/list.dashboards.yaml @@ -0,0 +1,43 @@ +--- +name: list.dashboards +pack: zabbix +runner_type: python-script +description: "List Zabbix dashboards." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "dashboard.get" + immutable: true + filter: + type: object + description: "Filter conditions." + required: false + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" + search: + type: object + description: "Search by pattern." + required: false + limit: + type: integer + description: "Maximum number of results." + required: false + sortfield: + type: string + description: "Field to sort by." + required: false + sortorder: + type: string + description: "Sort order: ASC or DESC." + required: false + dashboardids: + type: array + description: "Filter by dashboard IDs." + required: false + selectPages: + type: string + description: "Include dashboard pages. Set to 'extend'." + required: false diff --git a/actions/list.dhosts.yaml b/actions/list.dhosts.yaml new file mode 100644 index 0000000..0e149f6 --- /dev/null +++ b/actions/list.dhosts.yaml @@ -0,0 +1,47 @@ +--- +name: list.dhosts +pack: zabbix +runner_type: python-script +description: "List discovered hosts." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "dhost.get" + immutable: true + filter: + type: object + description: "Filter conditions." + required: false + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" + search: + type: object + description: "Search by pattern." + required: false + limit: + type: integer + description: "Maximum number of results." + required: false + sortfield: + type: string + description: "Field to sort by." + required: false + sortorder: + type: string + description: "Sort order: ASC or DESC." + required: false + druleids: + type: array + description: "Filter by discovery rule IDs." + required: false + dhostids: + type: array + description: "Filter by discovered host IDs." + required: false + selectDServices: + type: string + description: "Include discovered services. Set to 'extend'." + required: false diff --git a/actions/list.drules.yaml b/actions/list.drules.yaml new file mode 100644 index 0000000..2151e37 --- /dev/null +++ b/actions/list.drules.yaml @@ -0,0 +1,43 @@ +--- +name: list.drules +pack: zabbix +runner_type: python-script +description: "List network discovery rules." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "drule.get" + immutable: true + filter: + type: object + description: "Filter conditions." + required: false + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" + search: + type: object + description: "Search by pattern." + required: false + limit: + type: integer + description: "Maximum number of results." + required: false + sortfield: + type: string + description: "Field to sort by." + required: false + sortorder: + type: string + description: "Sort order: ASC or DESC." + required: false + druleids: + type: array + description: "Filter by discovery rule IDs." + required: false + selectDChecks: + type: string + description: "Include discovery checks. Set to 'extend'." + required: false diff --git a/actions/list.dservices.yaml b/actions/list.dservices.yaml new file mode 100644 index 0000000..ab248b4 --- /dev/null +++ b/actions/list.dservices.yaml @@ -0,0 +1,47 @@ +--- +name: list.dservices +pack: zabbix +runner_type: python-script +description: "List discovered services." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "dservice.get" + immutable: true + filter: + type: object + description: "Filter conditions." + required: false + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" + search: + type: object + description: "Search by pattern." + required: false + limit: + type: integer + description: "Maximum number of results." + required: false + sortfield: + type: string + description: "Field to sort by." + required: false + sortorder: + type: string + description: "Sort order: ASC or DESC." + required: false + dhostids: + type: array + description: "Filter by discovered host IDs." + required: false + druleids: + type: array + description: "Filter by discovery rule IDs." + required: false + dserviceids: + type: array + description: "Filter by specific discovered service IDs." + required: false diff --git a/actions/list.events.yaml b/actions/list.events.yaml new file mode 100644 index 0000000..1925c9e --- /dev/null +++ b/actions/list.events.yaml @@ -0,0 +1,59 @@ +--- +name: list.events +pack: zabbix +runner_type: python-script +description: "List Zabbix events with optional filtering." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "event.get" + immutable: true + hostids: + type: array + description: "Filter events by host IDs." + required: false + objectids: + type: array + description: "Filter by trigger/item/etc IDs that generated events." + required: false + source: + type: integer + description: "Event source: 0 (trigger), 1 (discovery), 2 (autoregistration), 3 (internal)." + required: false + object: + type: integer + description: "Event object type: 0 (trigger), 1 (discovered host), etc." + required: false + value: + type: integer + description: "Event value: 0 (OK/Up), 1 (Problem/Down)." + required: false + time_from: + type: string + description: "Return only events after this time (unix timestamp)." + required: false + time_till: + type: string + description: "Return only events before this time (unix timestamp)." + required: false + filter: + type: object + description: "Filter conditions." + required: false + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" + sortfield: + type: array + description: "Fields to sort by (e.g. ['clock', 'eventid'])." + required: false + sortorder: + type: string + description: "Sort order: ASC or DESC." + required: false + limit: + type: integer + description: "Maximum number of results." + required: false diff --git a/actions/list.graphs.yaml b/actions/list.graphs.yaml new file mode 100644 index 0000000..712663c --- /dev/null +++ b/actions/list.graphs.yaml @@ -0,0 +1,51 @@ +--- +name: list.graphs +pack: zabbix +runner_type: python-script +description: "List Zabbix graphs." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "graph.get" + immutable: true + filter: + type: object + description: "Filter conditions." + required: false + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" + search: + type: object + description: "Search by pattern." + required: false + limit: + type: integer + description: "Maximum number of results." + required: false + sortfield: + type: string + description: "Field to sort by." + required: false + sortorder: + type: string + description: "Sort order: ASC or DESC." + required: false + hostids: + type: array + description: "Filter by host IDs." + required: false + groupids: + type: array + description: "Filter by host group IDs." + required: false + templateids: + type: array + description: "Filter by template IDs." + required: false + graphids: + type: array + description: "Filter by graph IDs." + required: false diff --git a/actions/list.hostgroups.yaml b/actions/list.hostgroups.yaml new file mode 100644 index 0000000..5f8a8e8 --- /dev/null +++ b/actions/list.hostgroups.yaml @@ -0,0 +1,35 @@ +--- +name: list.hostgroups +pack: zabbix +runner_type: python-script +description: "List Zabbix host groups." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "hostgroup.get" + immutable: true + filter: + type: object + description: "Filter conditions." + required: false + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" + search: + type: object + description: "Search by pattern." + required: false + limit: + type: integer + description: "Maximum number of results." + required: false + sortfield: + type: string + description: "Field to sort by." + required: false + sortorder: + type: string + description: "Sort order: ASC or DESC." + required: false diff --git a/actions/list.hostinterfaces.yaml b/actions/list.hostinterfaces.yaml new file mode 100644 index 0000000..91b7ddc --- /dev/null +++ b/actions/list.hostinterfaces.yaml @@ -0,0 +1,43 @@ +--- +name: list.hostinterfaces +pack: zabbix +runner_type: python-script +description: "List host interfaces." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "hostinterface.get" + immutable: true + filter: + type: object + description: "Filter conditions." + required: false + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" + search: + type: object + description: "Search by pattern." + required: false + limit: + type: integer + description: "Maximum number of results." + required: false + sortfield: + type: string + description: "Field to sort by." + required: false + sortorder: + type: string + description: "Sort order: ASC or DESC." + required: false + hostids: + type: array + description: "Filter by host IDs." + required: false + interfaceids: + type: array + description: "Filter by interface IDs." + required: false diff --git a/actions/list.hosts.yaml b/actions/list.hosts.yaml new file mode 100644 index 0000000..1ae9675 --- /dev/null +++ b/actions/list.hosts.yaml @@ -0,0 +1,55 @@ +--- +name: list.hosts +pack: zabbix +runner_type: python-script +description: "List and search Zabbix hosts with optional filtering." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "host.get" + immutable: true + filter: + type: object + description: "Filter conditions (e.g. {\"host\": \"myhost\"})." + required: false + output: + type: array + description: "Fields to return (e.g. [\"hostid\", \"host\", \"name\", \"status\"])." + required: false + groupids: + type: array + description: "Filter by host group IDs." + required: false + templateids: + type: array + description: "Filter by template IDs." + required: false + proxyids: + type: array + description: "Filter by proxy IDs." + required: false + hostids: + type: array + description: "Filter by specific host IDs." + required: false + search: + type: object + description: "Search by pattern (e.g. {\"host\": \"web*\"})." + required: false + searchWildcardsEnabled: + type: boolean + description: "Enable wildcard matching in search." + required: false + limit: + type: integer + description: "Maximum number of results." + required: false + sortfield: + type: string + description: "Field to sort by." + required: false + sortorder: + type: string + description: "Sort order: ASC or DESC." + required: false diff --git a/actions/list.httptests.yaml b/actions/list.httptests.yaml new file mode 100644 index 0000000..ee05d47 --- /dev/null +++ b/actions/list.httptests.yaml @@ -0,0 +1,51 @@ +--- +name: list.httptests +pack: zabbix +runner_type: python-script +description: "List web monitoring scenarios." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "httptest.get" + immutable: true + filter: + type: object + description: "Filter conditions." + required: false + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" + search: + type: object + description: "Search by pattern." + required: false + limit: + type: integer + description: "Maximum number of results." + required: false + sortfield: + type: string + description: "Field to sort by." + required: false + sortorder: + type: string + description: "Sort order: ASC or DESC." + required: false + hostids: + type: array + description: "Filter by host IDs." + required: false + groupids: + type: array + description: "Filter by host group IDs." + required: false + httptestids: + type: array + description: "Filter by web scenario IDs." + required: false + selectSteps: + type: string + description: "Include web scenario steps. Set to 'extend'." + required: false diff --git a/actions/list.items.yaml b/actions/list.items.yaml new file mode 100644 index 0000000..d6d12ac --- /dev/null +++ b/actions/list.items.yaml @@ -0,0 +1,51 @@ +--- +name: list.items +pack: zabbix +runner_type: python-script +description: "List Zabbix monitoring items." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "item.get" + immutable: true + filter: + type: object + description: "Filter conditions." + required: false + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" + search: + type: object + description: "Search by pattern." + required: false + limit: + type: integer + description: "Maximum number of results." + required: false + sortfield: + type: string + description: "Field to sort by." + required: false + sortorder: + type: string + description: "Sort order: ASC or DESC." + required: false + hostids: + type: array + description: "Filter items by host IDs." + required: false + groupids: + type: array + description: "Filter items by host group IDs." + required: false + templateids: + type: array + description: "Filter items by template IDs." + required: false + itemids: + type: array + description: "Filter by specific item IDs." + required: false diff --git a/actions/list.maintenances.yaml b/actions/list.maintenances.yaml new file mode 100644 index 0000000..64f3304 --- /dev/null +++ b/actions/list.maintenances.yaml @@ -0,0 +1,47 @@ +--- +name: list.maintenances +pack: zabbix +runner_type: python-script +description: "List Zabbix maintenance windows." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "maintenance.get" + immutable: true + filter: + type: object + description: "Filter conditions." + required: false + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" + search: + type: object + description: "Search by pattern." + required: false + limit: + type: integer + description: "Maximum number of results." + required: false + sortfield: + type: string + description: "Field to sort by." + required: false + sortorder: + type: string + description: "Sort order: ASC or DESC." + required: false + hostids: + type: array + description: "Filter by host IDs in maintenance." + required: false + groupids: + type: array + description: "Filter by host group IDs in maintenance." + required: false + maintenanceids: + type: array + description: "Filter by specific maintenance IDs." + required: false diff --git a/actions/list.maps.yaml b/actions/list.maps.yaml new file mode 100644 index 0000000..99e91ef --- /dev/null +++ b/actions/list.maps.yaml @@ -0,0 +1,39 @@ +--- +name: list.maps +pack: zabbix +runner_type: python-script +description: "List Zabbix network maps." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "map.get" + immutable: true + filter: + type: object + description: "Filter conditions." + required: false + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" + search: + type: object + description: "Search by pattern." + required: false + limit: + type: integer + description: "Maximum number of results." + required: false + sortfield: + type: string + description: "Field to sort by." + required: false + sortorder: + type: string + description: "Sort order: ASC or DESC." + required: false + sysmapids: + type: array + description: "Filter by map IDs." + required: false diff --git a/actions/list.mediatypes.yaml b/actions/list.mediatypes.yaml new file mode 100644 index 0000000..2f408bb --- /dev/null +++ b/actions/list.mediatypes.yaml @@ -0,0 +1,39 @@ +--- +name: list.mediatypes +pack: zabbix +runner_type: python-script +description: "List Zabbix media types." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "mediatype.get" + immutable: true + filter: + type: object + description: "Filter conditions." + required: false + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" + search: + type: object + description: "Search by pattern." + required: false + limit: + type: integer + description: "Maximum number of results." + required: false + sortfield: + type: string + description: "Field to sort by." + required: false + sortorder: + type: string + description: "Sort order: ASC or DESC." + required: false + mediatypeids: + type: array + description: "Filter by media type IDs." + required: false diff --git a/actions/list.problems.yaml b/actions/list.problems.yaml new file mode 100644 index 0000000..cb33cfc --- /dev/null +++ b/actions/list.problems.yaml @@ -0,0 +1,75 @@ +--- +name: list.problems +pack: zabbix +runner_type: python-script +description: "List active Zabbix problems with optional filtering." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "problem.get" + immutable: true + hostids: + type: array + description: "Filter problems by host IDs." + required: false + groupids: + type: array + description: "Filter problems by host group IDs." + required: false + objectids: + type: array + description: "Filter by trigger IDs that generated problems." + required: false + severities: + type: array + description: "Filter by severity levels (0-5)." + required: false + acknowledged: + type: boolean + description: "Filter by acknowledgement state." + required: false + suppressed: + type: boolean + description: "Filter by suppression state." + required: false + tags: + type: array + description: "Filter by problem tags." + required: false + time_from: + type: string + description: "Return only problems after this time (unix timestamp)." + required: false + time_till: + type: string + description: "Return only problems before this time (unix timestamp)." + required: false + recent: + type: boolean + description: "Return only recently created problems." + required: false + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" + selectAcknowledges: + type: string + description: "Include acknowledgements. Set to 'extend'." + required: false + selectTags: + type: string + description: "Include tags. Set to 'extend'." + required: false + sortfield: + type: array + description: "Fields to sort by." + required: false + sortorder: + type: string + description: "Sort order: ASC or DESC." + required: false + limit: + type: integer + description: "Maximum number of results." + required: false diff --git a/actions/list.proxies.yaml b/actions/list.proxies.yaml new file mode 100644 index 0000000..3e83318 --- /dev/null +++ b/actions/list.proxies.yaml @@ -0,0 +1,39 @@ +--- +name: list.proxies +pack: zabbix +runner_type: python-script +description: "List Zabbix proxies." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "proxy.get" + immutable: true + filter: + type: object + description: "Filter conditions." + required: false + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" + search: + type: object + description: "Search by pattern." + required: false + limit: + type: integer + description: "Maximum number of results." + required: false + sortfield: + type: string + description: "Field to sort by." + required: false + sortorder: + type: string + description: "Sort order: ASC or DESC." + required: false + proxyids: + type: array + description: "Filter by proxy IDs." + required: false diff --git a/actions/list.roles.yaml b/actions/list.roles.yaml new file mode 100644 index 0000000..9acbed2 --- /dev/null +++ b/actions/list.roles.yaml @@ -0,0 +1,39 @@ +--- +name: list.roles +pack: zabbix +runner_type: python-script +description: "List Zabbix user roles." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "role.get" + immutable: true + filter: + type: object + description: "Filter conditions." + required: false + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" + search: + type: object + description: "Search by pattern." + required: false + limit: + type: integer + description: "Maximum number of results." + required: false + sortfield: + type: string + description: "Field to sort by." + required: false + sortorder: + type: string + description: "Sort order: ASC or DESC." + required: false + roleids: + type: array + description: "Filter by role IDs." + required: false diff --git a/actions/list.scripts.yaml b/actions/list.scripts.yaml new file mode 100644 index 0000000..c05272a --- /dev/null +++ b/actions/list.scripts.yaml @@ -0,0 +1,47 @@ +--- +name: list.scripts +pack: zabbix +runner_type: python-script +description: "List Zabbix scripts." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "script.get" + immutable: true + filter: + type: object + description: "Filter conditions." + required: false + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" + search: + type: object + description: "Search by pattern." + required: false + limit: + type: integer + description: "Maximum number of results." + required: false + sortfield: + type: string + description: "Field to sort by." + required: false + sortorder: + type: string + description: "Sort order: ASC or DESC." + required: false + hostids: + type: array + description: "Filter scripts available for these hosts." + required: false + groupids: + type: array + description: "Filter scripts available for these host groups." + required: false + scriptids: + type: array + description: "Filter by specific script IDs." + required: false diff --git a/actions/list.services.yaml b/actions/list.services.yaml new file mode 100644 index 0000000..dae1ed2 --- /dev/null +++ b/actions/list.services.yaml @@ -0,0 +1,55 @@ +--- +name: list.services +pack: zabbix +runner_type: python-script +description: "List Zabbix services." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "service.get" + immutable: true + filter: + type: object + description: "Filter conditions." + required: false + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" + search: + type: object + description: "Search by pattern." + required: false + limit: + type: integer + description: "Maximum number of results." + required: false + sortfield: + type: string + description: "Field to sort by." + required: false + sortorder: + type: string + description: "Sort order: ASC or DESC." + required: false + serviceids: + type: array + description: "Filter by service IDs." + required: false + selectChildren: + type: string + description: "Include child services. Set to 'extend'." + required: false + selectParents: + type: string + description: "Include parent services. Set to 'extend'." + required: false + selectTags: + type: string + description: "Include service tags. Set to 'extend'." + required: false + selectProblemTags: + type: string + description: "Include problem tags. Set to 'extend'." + required: false diff --git a/actions/list.sla.yaml b/actions/list.sla.yaml new file mode 100644 index 0000000..2eff595 --- /dev/null +++ b/actions/list.sla.yaml @@ -0,0 +1,51 @@ +--- +name: list.sla +pack: zabbix +runner_type: python-script +description: "List SLA definitions." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "sla.get" + immutable: true + filter: + type: object + description: "Filter conditions." + required: false + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" + search: + type: object + description: "Search by pattern." + required: false + limit: + type: integer + description: "Maximum number of results." + required: false + sortfield: + type: string + description: "Field to sort by." + required: false + sortorder: + type: string + description: "Sort order: ASC or DESC." + required: false + slaids: + type: array + description: "Filter by SLA IDs." + required: false + serviceids: + type: array + description: "Filter SLAs linked to these services." + required: false + selectSchedule: + type: string + description: "Include schedule. Set to 'extend'." + required: false + selectServiceTags: + type: string + description: "Include service tags. Set to 'extend'." + required: false diff --git a/actions/list.templates.yaml b/actions/list.templates.yaml new file mode 100644 index 0000000..fdf51ed --- /dev/null +++ b/actions/list.templates.yaml @@ -0,0 +1,43 @@ +--- +name: list.templates +pack: zabbix +runner_type: python-script +description: "List Zabbix templates." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "template.get" + immutable: true + filter: + type: object + description: "Filter conditions." + required: false + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" + search: + type: object + description: "Search by pattern." + required: false + limit: + type: integer + description: "Maximum number of results." + required: false + sortfield: + type: string + description: "Field to sort by." + required: false + sortorder: + type: string + description: "Sort order: ASC or DESC." + required: false + hostids: + type: array + description: "Filter templates linked to these hosts." + required: false + groupids: + type: array + description: "Filter templates in these host groups." + required: false diff --git a/actions/list.tokens.yaml b/actions/list.tokens.yaml new file mode 100644 index 0000000..e5df5fa --- /dev/null +++ b/actions/list.tokens.yaml @@ -0,0 +1,43 @@ +--- +name: list.tokens +pack: zabbix +runner_type: python-script +description: "List API tokens." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "token.get" + immutable: true + filter: + type: object + description: "Filter conditions." + required: false + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" + search: + type: object + description: "Search by pattern." + required: false + limit: + type: integer + description: "Maximum number of results." + required: false + sortfield: + type: string + description: "Field to sort by." + required: false + sortorder: + type: string + description: "Sort order: ASC or DESC." + required: false + tokenids: + type: array + description: "Filter by token IDs." + required: false + userids: + type: array + description: "Filter by user IDs." + required: false diff --git a/actions/list.triggers.yaml b/actions/list.triggers.yaml new file mode 100644 index 0000000..e1997a1 --- /dev/null +++ b/actions/list.triggers.yaml @@ -0,0 +1,67 @@ +--- +name: list.triggers +pack: zabbix +runner_type: python-script +description: "List Zabbix triggers." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "trigger.get" + immutable: true + filter: + type: object + description: "Filter conditions." + required: false + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" + search: + type: object + description: "Search by pattern." + required: false + limit: + type: integer + description: "Maximum number of results." + required: false + sortfield: + type: string + description: "Field to sort by." + required: false + sortorder: + type: string + description: "Sort order: ASC or DESC." + required: false + hostids: + type: array + description: "Filter triggers by host IDs." + required: false + groupids: + type: array + description: "Filter triggers by host group IDs." + required: false + templateids: + type: array + description: "Filter triggers by template IDs." + required: false + triggerids: + type: array + description: "Filter by specific trigger IDs." + required: false + expandDescription: + type: boolean + description: "Expand macros in trigger description." + required: false + only_true: + type: boolean + description: "Return only triggers in problem state." + required: false + min_severity: + type: integer + description: "Minimum trigger severity (0-5)." + required: false + selectHosts: + type: string + description: "Include hosts. Set to 'extend'." + required: false diff --git a/actions/list.usergroups.yaml b/actions/list.usergroups.yaml new file mode 100644 index 0000000..e6bb970 --- /dev/null +++ b/actions/list.usergroups.yaml @@ -0,0 +1,43 @@ +--- +name: list.usergroups +pack: zabbix +runner_type: python-script +description: "List Zabbix user groups." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "usergroup.get" + immutable: true + filter: + type: object + description: "Filter conditions." + required: false + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" + search: + type: object + description: "Search by pattern." + required: false + limit: + type: integer + description: "Maximum number of results." + required: false + sortfield: + type: string + description: "Field to sort by." + required: false + sortorder: + type: string + description: "Sort order: ASC or DESC." + required: false + usrgrpids: + type: array + description: "Filter by user group IDs." + required: false + userids: + type: array + description: "Filter by user IDs that belong to the groups." + required: false diff --git a/actions/list.usermacros.yaml b/actions/list.usermacros.yaml new file mode 100644 index 0000000..c791a9f --- /dev/null +++ b/actions/list.usermacros.yaml @@ -0,0 +1,43 @@ +--- +name: list.usermacros +pack: zabbix +runner_type: python-script +description: "List host and global user macros." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "usermacro.get" + immutable: true + filter: + type: object + description: "Filter conditions." + required: false + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" + search: + type: object + description: "Search by pattern." + required: false + limit: + type: integer + description: "Maximum number of results." + required: false + sortfield: + type: string + description: "Field to sort by." + required: false + sortorder: + type: string + description: "Sort order: ASC or DESC." + required: false + hostids: + type: array + description: "Filter macros by host IDs." + required: false + globalmacro: + type: boolean + description: "Return global macros instead of host macros." + required: false diff --git a/actions/list.users.yaml b/actions/list.users.yaml new file mode 100644 index 0000000..29cc945 --- /dev/null +++ b/actions/list.users.yaml @@ -0,0 +1,51 @@ +--- +name: list.users +pack: zabbix +runner_type: python-script +description: "List Zabbix users." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "user.get" + immutable: true + filter: + type: object + description: "Filter conditions." + required: false + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" + search: + type: object + description: "Search by pattern." + required: false + limit: + type: integer + description: "Maximum number of results." + required: false + sortfield: + type: string + description: "Field to sort by." + required: false + sortorder: + type: string + description: "Sort order: ASC or DESC." + required: false + userids: + type: array + description: "Filter by user IDs." + required: false + usrgrpids: + type: array + description: "Filter by user group IDs." + required: false + selectMedias: + type: string + description: "Include user medias. Set to 'extend'." + required: false + selectUsrgrps: + type: string + description: "Include user groups. Set to 'extend'." + required: false diff --git a/actions/list.valuemaps.yaml b/actions/list.valuemaps.yaml new file mode 100644 index 0000000..b8e41d3 --- /dev/null +++ b/actions/list.valuemaps.yaml @@ -0,0 +1,43 @@ +--- +name: list.valuemaps +pack: zabbix +runner_type: python-script +description: "List value maps." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "valuemap.get" + immutable: true + filter: + type: object + description: "Filter conditions." + required: false + output: + type: string + description: "Output fields. Use 'extend' for all." + default: "extend" + search: + type: object + description: "Search by pattern." + required: false + limit: + type: integer + description: "Maximum number of results." + required: false + sortfield: + type: string + description: "Field to sort by." + required: false + sortorder: + type: string + description: "Sort order: ASC or DESC." + required: false + valuemapids: + type: array + description: "Filter by value map IDs." + required: false + hostids: + type: array + description: "Filter by host IDs." + required: false diff --git a/actions/list_host_groups.yaml b/actions/list_host_groups.yaml deleted file mode 100644 index 9ca9464..0000000 --- a/actions/list_host_groups.yaml +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: list_host_groups -pack: zabbix -runner_type: python-script -description: List all host_groups objects which are registered in Zabbix -enabled: true -entry_point: call_api.py -parameters: - filter: - type: object - description: Condition to filter the result - token: - type: string - description: Encrypted access token to authenticate to ZabbixServer - default: | - {% if st2kv.user.zabbix.secret_token|string != '' %}{{ st2kv.user.zabbix.secret_token | decrypt_kv }}{% endif %} - secret: true - api_method: - default: hostgroup.get - immutable: true diff --git a/actions/list_host_interfaces.yaml b/actions/list_host_interfaces.yaml deleted file mode 100644 index 46f2f5e..0000000 --- a/actions/list_host_interfaces.yaml +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: list_host_interfaces -pack: zabbix -runner_type: python-script -description: List all hostinterfaces objects which are registered in Zabbix -enabled: true -entry_point: call_api.py -parameters: - filter: - type: object - description: Condition to filter the result - token: - type: string - description: Encrypted access token to authenticate to ZabbixServer - default: | - {% if st2kv.user.zabbix.secret_token|string != '' %}{{ st2kv.user.zabbix.secret_token | decrypt_kv }}{% endif %} - secret: true - api_method: - default: hostinterface.get - immutable: true diff --git a/actions/list_hosts.yaml b/actions/list_hosts.yaml deleted file mode 100644 index de783a3..0000000 --- a/actions/list_hosts.yaml +++ /dev/null @@ -1,26 +0,0 @@ ---- -name: list_hosts -pack: zabbix -runner_type: python-script -description: List all host objects which are registered in Zabbix -enabled: true -entry_point: call_api.py -parameters: - filter: - type: object - description: 'Condition to filter the result. Example - {"hostid": "12345"}' - output: - description: A list of key names that limit the response data. 'hostid' is always present. Example - ["maintenance_status", "name"] - type: array - groupids: - description: list of groupids to limit the results to. Example - ["123", "456"] - type: array - token: - type: string - description: Encrypted access token to authenticate to ZabbixServer - default: | - {% if st2kv.user.zabbix.secret_token|string != '' %}{{ st2kv.user.zabbix.secret_token | decrypt_kv }}{% endif %} - secret: true - api_method: - default: host.get - immutable: true diff --git a/actions/list_templates.yaml b/actions/list_templates.yaml deleted file mode 100644 index 1b13f10..0000000 --- a/actions/list_templates.yaml +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: list_templates -pack: zabbix -runner_type: python-script -description: List all templates objects which are registered in Zabbix -enabled: true -entry_point: call_api.py -parameters: - filter: - type: object - description: Condition to filter the result - token: - type: string - description: Encrypted access token to authenticate to ZabbixServer - default: | - {% if st2kv.user.zabbix.secret_token|string != '' %}{{ st2kv.user.zabbix.secret_token | decrypt_kv }}{% endif %} - secret: true - api_method: - default: template.get - immutable: true diff --git a/actions/maintenance_create_or_update.py b/actions/maintenance_create_or_update.py deleted file mode 100644 index fd068dd..0000000 --- a/actions/maintenance_create_or_update.py +++ /dev/null @@ -1,70 +0,0 @@ -# Licensed to the StackStorm, Inc ('StackStorm') under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from datetime import datetime -from tzlocal import get_localzone -from lib.actions import ZabbixBaseAction - - -class MaintenanceCreateOrUpdate(ZabbixBaseAction): - def run(self, - host=None, - time_type=None, - maintenance_window_name=None, - maintenance_type=None, - start_date=None, - end_date=None): - """ Creates or updates a Zabbix maintenance window by looking - for the supplied maintenance_window_name and creating the mainenance window if it - does not exist or updating the mainenance window if it already exists. - """ - self.connect() - - host_id = self.find_host(host) - - start_time = None - end_time = None - period = None - if start_date is not None and end_date is not None: - local_tz = get_localzone() - - start_local = datetime.strptime(start_date, "%Y-%m-%d %H:%M") - start_local = start_local.replace(tzinfo=local_tz) - start_time = int(start_local.strftime('%s')) - - end_local = datetime.strptime(end_date, "%Y-%m-%d %H:%M") - end_local = end_local.replace(tzinfo=local_tz) - end_time = int(end_local.strftime('%s')) - - period = end_time - start_time - else: - raise ValueError("Must supply a start_date and end_date") - - time_period = [{'start_date': start_time, - 'timeperiod_type': time_type, - 'period': period}] - - maintenance_params = { - 'hostids': [host_id], - 'name': maintenance_window_name, - 'active_since': start_time, - 'active_till': end_time, - 'maintenance_type': maintenance_type, - 'timeperiods': time_period - } - - maintenance_result = self.maintenance_create_or_update(maintenance_params) - - return {'maintenance_id': maintenance_result['maintenanceids'][0]} diff --git a/actions/maintenance_create_or_update.yaml b/actions/maintenance_create_or_update.yaml deleted file mode 100644 index e092bab..0000000 --- a/actions/maintenance_create_or_update.yaml +++ /dev/null @@ -1,32 +0,0 @@ ---- -name: maintenance_create_or_update -pack: zabbix -runner_type: python-script -description: Create or update Zabbix Maintenance Window -enabled: true -entry_point: maintenance_create_or_update.py -parameters: - end_date: - type: string - description: "Date and time to end maintenance window ex. 2017-10-13 20:00 (Y-m-d H:M)" - required: True - host: - type: string - description: "Name of the Zabbix Host" - required: True - maintenance_window_name: - type: string - description: "Name that of the maintenance window" - required: True - maintenance_type: - type: integer - description: "Type of maintenance valid values: 0 - with data collection 1 - without data collection" - default: 0 - start_date: - type: string - description: "Date and time to start maintenance window ex. 2017-10-13 11:00 (Y-m-d H:M)" - required: True - time_type: - type: integer - description: "Type of time period valid values: 0 - one time only; 2 - daily; 3 - weekly; 4 - monthly" - default: 0 diff --git a/actions/maintenance_delete.py b/actions/maintenance_delete.py deleted file mode 100644 index 03f8266..0000000 --- a/actions/maintenance_delete.py +++ /dev/null @@ -1,47 +0,0 @@ -# Licensed to the StackStorm, Inc ('StackStorm') under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from lib.actions import ZabbixBaseAction -from pyzabbix.api import ZabbixAPIException - - -class MaintenanceDelete(ZabbixBaseAction): - def run(self, maintenance_id=None, maintenance_window_name=None): - """ Delete a maintenance window base on the given maintenance_window_name - or a maintenance_id - """ - self.connect() - - if maintenance_window_name is not None: - maintenance_result = self.maintenance_get(maintenance_window_name) - - if len(maintenance_result) == 0: - raise ValueError(("Could not find maintenance windows with name: " - "{0}").format(maintenance_window_name)) - elif len(maintenance_result) == 1: - maintenance_id = maintenance_result[0]['maintenanceid'] - elif len(maintenance_result) >= 2: - raise ValueError(("There are multiple maintenance windows with the " - "name: {0}").format(maintenance_window_name)) - elif maintenance_window_name is None and maintenance_id is None: - raise ValueError("Must provide either a maintenance_window_name or a maintenance_id") - - try: - self.client.maintenance.delete(maintenance_id) - except ZabbixAPIException as e: - raise ZabbixAPIException(("There was a problem deleting the " - "maintenance window: {0}").format(e)) - - return True diff --git a/actions/maintenance_delete.yaml b/actions/maintenance_delete.yaml deleted file mode 100644 index 3edf877..0000000 --- a/actions/maintenance_delete.yaml +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: maintenance_delete -pack: zabbix -runner_type: python-script -description: Delete Zabbix Maintenance Window -enabled: true -entry_point: maintenance_delete.py -parameters: - maintenance_id: - type: string - description: "ID of the maintenance window" - maintenance_window_name: - type: string - description: "Name that of the maintenance window" diff --git a/actions/test_credentials.py b/actions/test_credentials.py deleted file mode 100644 index 06032bf..0000000 --- a/actions/test_credentials.py +++ /dev/null @@ -1,26 +0,0 @@ -# Licensed to the StackStorm, Inc ('StackStorm') under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from lib.actions import ZabbixBaseAction - - -class TestCredentials(ZabbixBaseAction): - - def run(self, host=None): - """ Attempt to login to the Zabbix server given the credentials in the - config - """ - self.connect() - return True diff --git a/actions/test_credentials.yaml b/actions/test_credentials.yaml deleted file mode 100644 index bd44323..0000000 --- a/actions/test_credentials.yaml +++ /dev/null @@ -1,7 +0,0 @@ ---- -name: test_credentials -pack: zabbix -runner_type: python-script -description: Attempts to login to Zabbix given the credentials in the config -enabled: true -entry_point: test_credentials.py diff --git a/actions/update.action.yaml b/actions/update.action.yaml new file mode 100644 index 0000000..ff4ee45 --- /dev/null +++ b/actions/update.action.yaml @@ -0,0 +1,39 @@ +--- +name: update.action +pack: zabbix +runner_type: python-script +description: "Update an alert action." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "action.update" + immutable: true + actionid: + type: string + description: "ID of the action to update." + required: true + name: + type: string + description: "Action name." + required: false + status: + type: integer + description: "Status: 0 (enabled), 1 (disabled)." + required: false + esc_period: + type: string + description: "Default escalation period." + required: false + operations: + type: array + description: "Action operations." + required: false + recovery_operations: + type: array + description: "Recovery operations." + required: false + filter: + type: object + description: "Action filter." + required: false diff --git a/actions/update.correlation.yaml b/actions/update.correlation.yaml new file mode 100644 index 0000000..313e03f --- /dev/null +++ b/actions/update.correlation.yaml @@ -0,0 +1,35 @@ +--- +name: update.correlation +pack: zabbix +runner_type: python-script +description: "Update an event correlation rule." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "correlation.update" + immutable: true + correlationid: + type: string + description: "ID of the correlation to update." + required: true + name: + type: string + description: "Correlation name." + required: false + filter: + type: object + description: "Correlation filter." + required: false + operations: + type: array + description: "Correlation operations." + required: false + status: + type: integer + description: "Status." + required: false + description: + type: string + description: "Description." + required: false diff --git a/actions/update.dashboard.yaml b/actions/update.dashboard.yaml new file mode 100644 index 0000000..1b43a6c --- /dev/null +++ b/actions/update.dashboard.yaml @@ -0,0 +1,31 @@ +--- +name: update.dashboard +pack: zabbix +runner_type: python-script +description: "Update a dashboard." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "dashboard.update" + immutable: true + dashboardid: + type: string + description: "ID of the dashboard to update." + required: true + name: + type: string + description: "Dashboard name." + required: false + pages: + type: array + description: "Dashboard pages." + required: false + userid: + type: string + description: "Owner user ID." + required: false + display_period: + type: integer + description: "Page display period." + required: false diff --git a/actions/update.drule.yaml b/actions/update.drule.yaml new file mode 100644 index 0000000..3eb6b1c --- /dev/null +++ b/actions/update.drule.yaml @@ -0,0 +1,35 @@ +--- +name: update.drule +pack: zabbix +runner_type: python-script +description: "Update a network discovery rule." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "drule.update" + immutable: true + druleid: + type: string + description: "ID of the discovery rule to update." + required: true + name: + type: string + description: "Rule name." + required: false + iprange: + type: string + description: "IP range." + required: false + dchecks: + type: array + description: "Discovery checks." + required: false + delay: + type: string + description: "Execution interval." + required: false + status: + type: integer + description: "Status." + required: false diff --git a/actions/update.graph.yaml b/actions/update.graph.yaml new file mode 100644 index 0000000..6e1714a --- /dev/null +++ b/actions/update.graph.yaml @@ -0,0 +1,35 @@ +--- +name: update.graph +pack: zabbix +runner_type: python-script +description: "Update a graph." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "graph.update" + immutable: true + graphid: + type: string + description: "ID of the graph to update." + required: true + name: + type: string + description: "Graph name." + required: false + gitems: + type: array + description: "Graph items." + required: false + width: + type: integer + description: "Width in pixels." + required: false + height: + type: integer + description: "Height in pixels." + required: false + graphtype: + type: integer + description: "Graph type." + required: false diff --git a/actions/update.host.status.yaml b/actions/update.host.status.yaml new file mode 100644 index 0000000..e1b2c72 --- /dev/null +++ b/actions/update.host.status.yaml @@ -0,0 +1,16 @@ +--- +name: update.host.status +pack: zabbix +runner_type: python-script +description: "Update the monitoring status of a host by hostname." +enabled: true +entry_point: host_status.py +parameters: + hostname: + type: string + description: "Name of the Zabbix host." + required: true + status: + type: integer + description: "Status value: 0 (monitored) or 1 (unmonitored)." + required: true diff --git a/actions/update.host.yaml b/actions/update.host.yaml new file mode 100644 index 0000000..5be97f3 --- /dev/null +++ b/actions/update.host.yaml @@ -0,0 +1,55 @@ +--- +name: update.host +pack: zabbix +runner_type: python-script +description: "Update properties of an existing Zabbix host." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "host.update" + immutable: true + hostid: + type: string + description: "ID of the host to update." + required: true + host: + type: string + description: "New technical name for the host." + required: false + name: + type: string + description: "New visible name for the host." + required: false + description: + type: string + description: "Host description." + required: false + status: + type: integer + description: "Host status: 0 (monitored) or 1 (unmonitored)." + required: false + groups: + type: array + description: "Host groups to assign (replaces existing)." + required: false + interfaces: + type: array + description: "Host interfaces to set (replaces existing)." + required: false + templates: + type: array + description: "Templates to link (replaces existing)." + required: false + macros: + type: array + description: "User macros to set." + required: false + inventory: + type: object + description: "Host inventory properties to update." + required: false + tags: + type: array + description: "Host tags to set." + required: false diff --git a/actions/update.hostgroup.yaml b/actions/update.hostgroup.yaml new file mode 100644 index 0000000..bf61214 --- /dev/null +++ b/actions/update.hostgroup.yaml @@ -0,0 +1,19 @@ +--- +name: update.hostgroup +pack: zabbix +runner_type: python-script +description: "Update a host group." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "hostgroup.update" + immutable: true + groupid: + type: string + description: "ID of the host group to update." + required: true + name: + type: string + description: "New name for the host group." + required: false diff --git a/actions/update.hostinterface.yaml b/actions/update.hostinterface.yaml new file mode 100644 index 0000000..a1c1548 --- /dev/null +++ b/actions/update.hostinterface.yaml @@ -0,0 +1,39 @@ +--- +name: update.hostinterface +pack: zabbix +runner_type: python-script +description: "Update a host interface." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "hostinterface.update" + immutable: true + interfaceid: + type: string + description: "ID of the interface to update." + required: true + type: + type: integer + description: "Interface type." + required: false + main: + type: integer + description: "Is main interface." + required: false + useip: + type: integer + description: "Connect via IP or DNS." + required: false + ip: + type: string + description: "IP address." + required: false + dns: + type: string + description: "DNS name." + required: false + port: + type: string + description: "Port number." + required: false diff --git a/actions/update.httptest.yaml b/actions/update.httptest.yaml new file mode 100644 index 0000000..50a4c20 --- /dev/null +++ b/actions/update.httptest.yaml @@ -0,0 +1,39 @@ +--- +name: update.httptest +pack: zabbix +runner_type: python-script +description: "Update a web monitoring scenario." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "httptest.update" + immutable: true + httptestid: + type: string + description: "ID of the web scenario to update." + required: true + name: + type: string + description: "Web scenario name." + required: false + steps: + type: array + description: "Web scenario steps." + required: false + delay: + type: string + description: "Execution interval." + required: false + retries: + type: integer + description: "Number of retries." + required: false + agent: + type: string + description: "HTTP user agent." + required: false + status: + type: integer + description: "Status." + required: false diff --git a/actions/update.item.yaml b/actions/update.item.yaml new file mode 100644 index 0000000..c4a0a99 --- /dev/null +++ b/actions/update.item.yaml @@ -0,0 +1,43 @@ +--- +name: update.item +pack: zabbix +runner_type: python-script +description: "Update a monitoring item." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "item.update" + immutable: true + itemid: + type: string + description: "ID of the item to update." + required: true + name: + type: string + description: "Item name." + required: false + key_: + type: string + description: "Item key." + required: false + type: + type: integer + description: "Item type." + required: false + value_type: + type: integer + description: "Value type." + required: false + delay: + type: string + description: "Update interval." + required: false + status: + type: integer + description: "Status: 0 (enabled), 1 (disabled)." + required: false + description: + type: string + description: "Item description." + required: false diff --git a/actions/update.maintenance.yaml b/actions/update.maintenance.yaml new file mode 100644 index 0000000..3c6c6af --- /dev/null +++ b/actions/update.maintenance.yaml @@ -0,0 +1,51 @@ +--- +name: update.maintenance +pack: zabbix +runner_type: python-script +description: "Update a maintenance window." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "maintenance.update" + immutable: true + maintenanceid: + type: string + description: "ID of the maintenance window to update." + required: true + name: + type: string + description: "Maintenance window name." + required: false + active_since: + type: string + description: "Active since (unix timestamp)." + required: false + active_till: + type: string + description: "Active till (unix timestamp)." + required: false + hosts: + type: array + description: "Hosts in maintenance." + required: false + groups: + type: array + description: "Host groups in maintenance." + required: false + timeperiods: + type: array + description: "Time periods." + required: false + maintenance_type: + type: integer + description: "Maintenance type." + required: false + description: + type: string + description: "Description." + required: false + tags: + type: array + description: "Problem tags." + required: false diff --git a/actions/update.map.yaml b/actions/update.map.yaml new file mode 100644 index 0000000..5d78626 --- /dev/null +++ b/actions/update.map.yaml @@ -0,0 +1,35 @@ +--- +name: update.map +pack: zabbix +runner_type: python-script +description: "Update a network map." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "map.update" + immutable: true + sysmapid: + type: string + description: "ID of the map to update." + required: true + name: + type: string + description: "Map name." + required: false + width: + type: integer + description: "Map width." + required: false + height: + type: integer + description: "Map height." + required: false + selements: + type: array + description: "Map elements." + required: false + links: + type: array + description: "Map links." + required: false diff --git a/actions/update.mediatype.yaml b/actions/update.mediatype.yaml new file mode 100644 index 0000000..2e4a6ba --- /dev/null +++ b/actions/update.mediatype.yaml @@ -0,0 +1,39 @@ +--- +name: update.mediatype +pack: zabbix +runner_type: python-script +description: "Update a media type." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "mediatype.update" + immutable: true + mediatypeid: + type: string + description: "ID of the media type to update." + required: true + name: + type: string + description: "Media type name." + required: false + type: + type: integer + description: "Media type." + required: false + status: + type: integer + description: "Status: 0 (enabled), 1 (disabled)." + required: false + description: + type: string + description: "Description." + required: false + script: + type: string + description: "Webhook JavaScript body." + required: false + parameters: + type: array + description: "Webhook parameters." + required: false diff --git a/actions/update.proxy.yaml b/actions/update.proxy.yaml new file mode 100644 index 0000000..8666d3f --- /dev/null +++ b/actions/update.proxy.yaml @@ -0,0 +1,35 @@ +--- +name: update.proxy +pack: zabbix +runner_type: python-script +description: "Update a proxy." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "proxy.update" + immutable: true + proxyid: + type: string + description: "ID of the proxy to update." + required: true + host: + type: string + description: "Proxy name." + required: false + status: + type: integer + description: "Proxy mode: 5 (active), 6 (passive)." + required: false + description: + type: string + description: "Proxy description." + required: false + interface: + type: object + description: "Proxy interface." + required: false + hosts: + type: array + description: "Hosts to assign." + required: false diff --git a/actions/update.script.yaml b/actions/update.script.yaml new file mode 100644 index 0000000..5374350 --- /dev/null +++ b/actions/update.script.yaml @@ -0,0 +1,39 @@ +--- +name: update.script +pack: zabbix +runner_type: python-script +description: "Update a script." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "script.update" + immutable: true + scriptid: + type: string + description: "ID of the script to update." + required: true + name: + type: string + description: "Script name." + required: false + command: + type: string + description: "Script command." + required: false + type: + type: integer + description: "Script type." + required: false + scope: + type: integer + description: "Script scope." + required: false + execute_on: + type: integer + description: "Execution target." + required: false + description: + type: string + description: "Script description." + required: false diff --git a/actions/update.service.yaml b/actions/update.service.yaml new file mode 100644 index 0000000..438923c --- /dev/null +++ b/actions/update.service.yaml @@ -0,0 +1,47 @@ +--- +name: update.service +pack: zabbix +runner_type: python-script +description: "Update a service." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "service.update" + immutable: true + serviceid: + type: string + description: "ID of the service to update." + required: true + name: + type: string + description: "Service name." + required: false + algorithm: + type: integer + description: "Status calculation algorithm." + required: false + sortorder: + type: integer + description: "Sorting position." + required: false + weight: + type: integer + description: "Service weight." + required: false + tags: + type: array + description: "Service tags." + required: false + problem_tags: + type: array + description: "Problem tags." + required: false + parents: + type: array + description: "Parent services." + required: false + children: + type: array + description: "Child services." + required: false diff --git a/actions/update.template.yaml b/actions/update.template.yaml new file mode 100644 index 0000000..c3d10a2 --- /dev/null +++ b/actions/update.template.yaml @@ -0,0 +1,43 @@ +--- +name: update.template +pack: zabbix +runner_type: python-script +description: "Update a template." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "template.update" + immutable: true + templateid: + type: string + description: "ID of the template to update." + required: true + host: + type: string + description: "New technical name." + required: false + name: + type: string + description: "New visible name." + required: false + description: + type: string + description: "Template description." + required: false + groups: + type: array + description: "Host groups (replaces existing)." + required: false + templates: + type: array + description: "Linked templates (replaces existing)." + required: false + macros: + type: array + description: "User macros." + required: false + tags: + type: array + description: "Template tags." + required: false diff --git a/actions/update.trigger.yaml b/actions/update.trigger.yaml new file mode 100644 index 0000000..29ad7ab --- /dev/null +++ b/actions/update.trigger.yaml @@ -0,0 +1,39 @@ +--- +name: update.trigger +pack: zabbix +runner_type: python-script +description: "Update a trigger." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "trigger.update" + immutable: true + triggerid: + type: string + description: "ID of the trigger to update." + required: true + description: + type: string + description: "Trigger name." + required: false + expression: + type: string + description: "Trigger expression." + required: false + priority: + type: integer + description: "Severity (0-5)." + required: false + status: + type: integer + description: "Status: 0 (enabled), 1 (disabled)." + required: false + comments: + type: string + description: "Trigger comments." + required: false + tags: + type: array + description: "Trigger tags." + required: false diff --git a/actions/update.user.yaml b/actions/update.user.yaml new file mode 100644 index 0000000..57c3f7e --- /dev/null +++ b/actions/update.user.yaml @@ -0,0 +1,43 @@ +--- +name: update.user +pack: zabbix +runner_type: python-script +description: "Update a user." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "user.update" + immutable: true + userid: + type: string + description: "ID of the user to update." + required: true + username: + type: string + description: "Username." + required: false + passwd: + type: string + description: "New password." + required: false + roleid: + type: string + description: "Role ID." + required: false + name: + type: string + description: "First name." + required: false + surname: + type: string + description: "Surname." + required: false + usrgrps: + type: array + description: "User groups." + required: false + medias: + type: array + description: "User medias." + required: false diff --git a/actions/update.usermacro.global.yaml b/actions/update.usermacro.global.yaml new file mode 100644 index 0000000..b2e3a45 --- /dev/null +++ b/actions/update.usermacro.global.yaml @@ -0,0 +1,31 @@ +--- +name: update.usermacro.global +pack: zabbix +runner_type: python-script +description: "Update a global user macro." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "usermacro.updateglobal" + immutable: true + globalmacroid: + type: string + description: "ID of the global macro to update." + required: true + macro: + type: string + description: "Macro name." + required: false + value: + type: string + description: "Macro value." + required: false + type: + type: integer + description: "Type: 0 (text), 1 (secret), 2 (vault secret)." + required: false + description: + type: string + description: "Macro description." + required: false diff --git a/actions/update.usermacro.yaml b/actions/update.usermacro.yaml new file mode 100644 index 0000000..38a4342 --- /dev/null +++ b/actions/update.usermacro.yaml @@ -0,0 +1,31 @@ +--- +name: update.usermacro +pack: zabbix +runner_type: python-script +description: "Update a host user macro." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "usermacro.update" + immutable: true + hostmacroid: + type: string + description: "ID of the host macro to update." + required: true + macro: + type: string + description: "Macro name." + required: false + value: + type: string + description: "Macro value." + required: false + type: + type: integer + description: "Type: 0 (text), 1 (secret), 2 (vault secret)." + required: false + description: + type: string + description: "Macro description." + required: false diff --git a/actions/update.valuemap.yaml b/actions/update.valuemap.yaml new file mode 100644 index 0000000..287e9a7 --- /dev/null +++ b/actions/update.valuemap.yaml @@ -0,0 +1,23 @@ +--- +name: update.valuemap +pack: zabbix +runner_type: python-script +description: "Update a value map." +enabled: true +entry_point: call_api.py +parameters: + api_method: + default: "valuemap.update" + immutable: true + valuemapid: + type: string + description: "ID of the value map to update." + required: true + name: + type: string + description: "Value map name." + required: false + mappings: + type: array + description: "Value mappings." + required: false diff --git a/actions/update_host.yaml b/actions/update_host.yaml deleted file mode 100644 index a1a06e0..0000000 --- a/actions/update_host.yaml +++ /dev/null @@ -1,44 +0,0 @@ ---- -name: update_host -pack: zabbix -runner_type: python-script -description: A primitive action to update host information -enabled: true -entry_point: call_api.py -parameters: - hostid: - type: string - description: ID of Host to be updated - description: - type: string - description: Description of the host. - groups: - type: array - description: Host groups to replace the current host groups the host belongs to. - host: - type: string - description: Technical name of the host. - interfaces: - type: array - description: Host interfaces to replace the current host interfaces. - inventory: - type: object - description: Host inventory properties. - macros: - type: array - description: User macros to replace the current user macros. - name: - type: string - description: Visible name of the host. - templates: - type: array - description: Templates to replace the currently linked templates. - token: - type: string - description: Encrypted access token to authenticate to ZabbixServer - default: | - {% if st2kv.user.zabbix.secret_token|string != '' %}{{ st2kv.user.zabbix.secret_token | decrypt_kv }}{% endif %} - secret: true - api_method: - default: host.update - immutable: true diff --git a/actions/verify.credentials.yaml b/actions/verify.credentials.yaml new file mode 100644 index 0000000..0a7fcff --- /dev/null +++ b/actions/verify.credentials.yaml @@ -0,0 +1,8 @@ +--- +name: verify.credentials +pack: zabbix +runner_type: python-script +description: "Verify Zabbix API connectivity and authentication." +enabled: true +entry_point: verify_credentials.py +parameters: {} diff --git a/actions/verify_credentials.py b/actions/verify_credentials.py new file mode 100644 index 0000000..5a913d9 --- /dev/null +++ b/actions/verify_credentials.py @@ -0,0 +1,9 @@ +from lib.actions import ZabbixBaseAction + + +class VerifyCredentials(ZabbixBaseAction): + """Verify Zabbix API connectivity and authentication.""" + + def run(self): + self.connect() + return True diff --git a/actions/workflows/host_get_active_triggers.yaml b/actions/workflows/get.host.active_triggers.yaml similarity index 88% rename from actions/workflows/host_get_active_triggers.yaml rename to actions/workflows/get.host.active_triggers.yaml index 14b2a40..204abd5 100644 --- a/actions/workflows/host_get_active_triggers.yaml +++ b/actions/workflows/get.host.active_triggers.yaml @@ -4,7 +4,7 @@ version: 1.0 description: List all active triggers for a given host input: - - host + - hostname - priority vars: @@ -18,9 +18,9 @@ output: tasks: get_zabbix_id: - action: zabbix.host_get_id + action: zabbix.find.host input: - host: "{{ ctx().host }}" + name: "{{ ctx().hostname }}" next: - when: "{{ succeeded() }}" publish: @@ -34,7 +34,7 @@ tasks: - fail get_triggers: - action: zabbix.host_get_triggers + action: zabbix.list.triggers input: filter: hostid: "{{ ctx().host_id }}" diff --git a/config.schema.yaml b/config.schema.yaml index 9479910..97dc5fd 100644 --- a/config.schema.yaml +++ b/config.schema.yaml @@ -1,18 +1,19 @@ --- -zabbix: - description: Configuration to authenticate with Zabbix Server - type: object +url: + type: string required: true - additionalProperties: false - properties: - url: - type: string - description: The zabbix login URL - default: http://localhost/zabbix - username: - type: string - default: Admin - password: - type: string - default: zabbix - secret: true + description: The Zabbix frontend URL (e.g. http://zabbix.example.com:8080) + default: http://localhost:8080 +username: + type: string + description: Zabbix username (required if api_token not set) + default: Admin +password: + type: string + description: Zabbix password (required if api_token not set) + default: zabbix + secret: true +api_token: + type: string + description: Zabbix API token (preferred over username/password) + secret: true diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000..2aaa4de --- /dev/null +++ b/conftest.py @@ -0,0 +1,6 @@ +import sys +import os + +# Add actions/ and tests/ to the path so imports resolve as they do in StackStorm +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'actions')) +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'tests')) diff --git a/docker-compose.yaml b/docker-compose.yaml index 3b48b11..ad6f38d 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,7 +1,8 @@ -version: '3' +version: '3.8' services: mysql: - image: mysql:5.7 + image: mysql:8.0 + command: --default-authentication-plugin=mysql_native_password ports: - "3306:3306" environment: @@ -9,27 +10,62 @@ services: MYSQL_USER: zabbix MYSQL_PASSWORD: zabbix MYSQL_ROOT_PASSWORD: passwd + volumes: + - mysql-data:/var/lib/mysql + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-ppasswd"] + interval: 10s + timeout: 5s + retries: 5 zabbix-server: - image: zabbix/zabbix-server-mysql:${TAG} + image: zabbix/zabbix-server-mysql:6.0-ubuntu-latest environment: DB_SERVER_HOST: mysql + MYSQL_DATABASE: zabbix + MYSQL_USER: zabbix + MYSQL_PASSWORD: zabbix MYSQL_ROOT_PASSWORD: passwd depends_on: - - mysql - volumes: - - ./tools/scripts/st2_dispatch.py:/usr/lib/zabbix/alertscripts/st2_dispatch.py + mysql: + condition: service_healthy ports: - "10051:10051" zabbix-web: - image: zabbix/zabbix-web-nginx-mysql:${TAG} - restart: always + image: zabbix/zabbix-web-nginx-mysql:6.0-ubuntu-latest environment: DB_SERVER_HOST: mysql + MYSQL_DATABASE: zabbix + MYSQL_USER: zabbix + MYSQL_PASSWORD: zabbix MYSQL_ROOT_PASSWORD: passwd - ports: - - 3033:80 + ZBX_SERVER_HOST: zabbix-server + PHP_TZ: America/New_York depends_on: - - mysql - - zabbix-server + mysql: + condition: service_healthy + zabbix-server: + condition: service_started + ports: + - "8080:8080" + + # Uncomment to enable RabbitMQ for webhook testing. + # The RabbitMQ webhook media type publishes messages to the Management API. + # Use with stackstorm-rabbitmq pack to consume messages into StackStorm. + # rabbitmq: + # image: rabbitmq:3.12-management + # ports: + # - "5672:5672" + # - "15672:15672" + # environment: + # RABBITMQ_DEFAULT_USER: guest + # RABBITMQ_DEFAULT_PASS: guest + # healthcheck: + # test: ["CMD", "rabbitmqctl", "status"] + # interval: 10s + # timeout: 5s + # retries: 5 + +volumes: + mysql-data: diff --git a/images/apikey_example.png b/images/apikey_example.png deleted file mode 100644 index a91d13d..0000000 Binary files a/images/apikey_example.png and /dev/null differ diff --git a/images/configuration_for_action1.png b/images/configuration_for_action1.png deleted file mode 100644 index f473ce6..0000000 Binary files a/images/configuration_for_action1.png and /dev/null differ diff --git a/images/configuration_for_action2.png b/images/configuration_for_action2.png deleted file mode 100644 index 8bc38f1..0000000 Binary files a/images/configuration_for_action2.png and /dev/null differ diff --git a/images/configuration_for_mediatype1.png b/images/configuration_for_mediatype1.png deleted file mode 100644 index a216ee0..0000000 Binary files a/images/configuration_for_mediatype1.png and /dev/null differ diff --git a/images/configuration_for_mediatype2.png b/images/configuration_for_mediatype2.png deleted file mode 100644 index 0f1f67f..0000000 Binary files a/images/configuration_for_mediatype2.png and /dev/null differ diff --git a/images/description_alertscript1.png b/images/description_alertscript1.png deleted file mode 100644 index 8569fe8..0000000 Binary files a/images/description_alertscript1.png and /dev/null differ diff --git a/images/description_alertscript2.png b/images/description_alertscript2.png deleted file mode 100644 index 001ad5f..0000000 Binary files a/images/description_alertscript2.png and /dev/null differ diff --git a/images/internal_construction.png b/images/internal_construction.png deleted file mode 100644 index b27bb2d..0000000 Binary files a/images/internal_construction.png and /dev/null differ diff --git a/images/zabbix_dependency_flow.png b/images/zabbix_dependency_flow.png deleted file mode 100644 index 3b850e2..0000000 Binary files a/images/zabbix_dependency_flow.png and /dev/null differ diff --git a/pack.yaml b/pack.yaml index 685e79c..3a33460 100644 --- a/pack.yaml +++ b/pack.yaml @@ -1,12 +1,16 @@ --- ref: zabbix name: zabbix -description: Zabbix Monitoring System +description: Zabbix 6.0+ Monitoring Integration Pack for StackStorm keywords: - zabbix - monitoring -version: 1.2.4 -author: Hiroyasu OHYAMA -email: user.localhost2000@gmail.com + - webhooks + - alerts +version: 2.0.0 python_versions: - "3" +author: Hiroyasu OHYAMA +email: user.localhost2000@gmail.com +contributors: + - "namachieli" diff --git a/requirements.txt b/requirements.txt index 3c6485e..93bca84 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -git+https://github.com/EncoreTechnologies/py-zabbix.git -pytz +zabbix-utils>=2.0.0 tzlocal +requests diff --git a/scripts/register_webhook_rabbitmq.sh b/scripts/register_webhook_rabbitmq.sh new file mode 100755 index 0000000..e05ec32 --- /dev/null +++ b/scripts/register_webhook_rabbitmq.sh @@ -0,0 +1,309 @@ +#!/usr/bin/env bash +# Register the "StackStorm RabbitMQ" webhook media type in Zabbix and +# configure the RabbitMQ exchange/queue/binding via Management API. +# +# This script: +# 1. Creates RabbitMQ exchange, queue, and binding via Management API +# 2. Authenticates to the Zabbix API +# 3. Creates/updates a webhook media type (type=4) that publishes to RabbitMQ +# 4. Assigns media to the Admin user +# +# Required environment variables: +# ZABBIX_URL - Zabbix frontend URL (e.g. http://localhost:8080) +# RABBITMQ_URL - RabbitMQ Management API URL (e.g. http://localhost:15672) +# +# Authentication (one of): +# ZABBIX_API_TOKEN - Zabbix API token (preferred) +# ZABBIX_USER - Zabbix username (default: Admin) +# ZABBIX_PASSWORD - Zabbix password (default: zabbix) +# +# Optional: +# RABBITMQ_USER - RabbitMQ username (default: guest) +# RABBITMQ_PASSWORD - RabbitMQ password (default: guest) +# RABBITMQ_VHOST - RabbitMQ virtual host (default: /) +# RABBITMQ_EXCHANGE - Exchange name (default: st2.zabbix) +# RABBITMQ_ROUTING_KEY - Routing key (default: zabbix.alerts) +# RABBITMQ_QUEUE - Queue name (default: zabbix.alerts) +# ZABBIX_ADMIN_USER_ID - Zabbix user ID to assign media to (default: 1 = Admin) + +set -euo pipefail + +: "${ZABBIX_URL:?ZABBIX_URL is required}" +: "${RABBITMQ_URL:?RABBITMQ_URL is required}" + +ZABBIX_USER="${ZABBIX_USER:-Admin}" +ZABBIX_PASSWORD="${ZABBIX_PASSWORD:-zabbix}" +ZABBIX_ADMIN_USER_ID="${ZABBIX_ADMIN_USER_ID:-1}" +RABBITMQ_USER="${RABBITMQ_USER:-guest}" +RABBITMQ_PASSWORD="${RABBITMQ_PASSWORD:-guest}" +RABBITMQ_VHOST="${RABBITMQ_VHOST:-/}" +RABBITMQ_EXCHANGE="${RABBITMQ_EXCHANGE:-st2.zabbix}" +RABBITMQ_ROUTING_KEY="${RABBITMQ_ROUTING_KEY:-zabbix.alerts}" +RABBITMQ_QUEUE="${RABBITMQ_QUEUE:-zabbix.alerts}" +MEDIA_TYPE_NAME="StackStorm RabbitMQ" + +# URL-encode the vhost for RabbitMQ Management API paths +VHOST_ENCODED=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${RABBITMQ_VHOST}', safe=''))") + +# --- RabbitMQ Setup --- + +echo "=== RabbitMQ Setup ===" + +rabbitmq_api() { + local method="$1" + local path="$2" + local data="${3:-}" + + local args=(-s -o /dev/null -w "%{http_code}" -X "${method}" + -u "${RABBITMQ_USER}:${RABBITMQ_PASSWORD}" + -H "Content-Type: application/json") + + if [[ -n "${data}" ]]; then + args+=(-d "${data}") + fi + + curl "${args[@]}" "${RABBITMQ_URL}/api${path}" +} + +# Create exchange +echo "Creating exchange '${RABBITMQ_EXCHANGE}' on vhost '${RABBITMQ_VHOST}'..." +HTTP_CODE=$(rabbitmq_api PUT "/exchanges/${VHOST_ENCODED}/${RABBITMQ_EXCHANGE}" \ + '{"type": "topic", "durable": true, "auto_delete": false}') +if [[ "${HTTP_CODE}" == "201" || "${HTTP_CODE}" == "204" ]]; then + echo "Exchange created/confirmed." +elif [[ "${HTTP_CODE}" == "204" ]]; then + echo "Exchange already exists." +else + echo "WARNING: Exchange creation returned HTTP ${HTTP_CODE}" >&2 +fi + +# Create queue +echo "Creating queue '${RABBITMQ_QUEUE}' on vhost '${RABBITMQ_VHOST}'..." +HTTP_CODE=$(rabbitmq_api PUT "/queues/${VHOST_ENCODED}/${RABBITMQ_QUEUE}" \ + '{"durable": true, "auto_delete": false}') +if [[ "${HTTP_CODE}" == "201" || "${HTTP_CODE}" == "204" ]]; then + echo "Queue created/confirmed." +else + echo "WARNING: Queue creation returned HTTP ${HTTP_CODE}" >&2 +fi + +# Create binding +echo "Binding queue '${RABBITMQ_QUEUE}' to exchange '${RABBITMQ_EXCHANGE}' with key '${RABBITMQ_ROUTING_KEY}'..." +HTTP_CODE=$(rabbitmq_api POST "/bindings/${VHOST_ENCODED}/e/${RABBITMQ_EXCHANGE}/q/${RABBITMQ_QUEUE}" \ + "{\"routing_key\": \"${RABBITMQ_ROUTING_KEY}\"}") +if [[ "${HTTP_CODE}" == "201" || "${HTTP_CODE}" == "204" ]]; then + echo "Binding created." +elif [[ "${HTTP_CODE}" == "409" ]]; then + echo "Binding already exists." +else + echo "WARNING: Binding creation returned HTTP ${HTTP_CODE}" >&2 +fi + +echo "" + +# --- Zabbix Setup --- + +echo "=== Zabbix Webhook Setup ===" + +# JSON-RPC request helper +request_id=1 +zabbix_api() { + local method="$1" + local params="$2" + local auth_field="" + + if [[ -n "${AUTH_TOKEN:-}" ]]; then + auth_field="\"auth\": \"${AUTH_TOKEN}\"," + fi + + local payload + payload=$(cat </dev/null || true) + if [[ -n "${error}" ]]; then + echo "ERROR: Zabbix API ${method} failed: ${error}" >&2 + exit 1 + fi + + echo "${response}" | python3 -c "import sys,json; print(json.dumps(json.load(sys.stdin).get('result')))" +} + +# Authenticate +echo "Authenticating to Zabbix at ${ZABBIX_URL}..." +AUTH_TOKEN="" +if [[ -n "${ZABBIX_API_TOKEN:-}" ]]; then + AUTH_TOKEN="${ZABBIX_API_TOKEN}" + echo "Using API token authentication." +else + AUTH_TOKEN=$(zabbix_api "user.login" "{\"username\": \"${ZABBIX_USER}\", \"password\": \"${ZABBIX_PASSWORD}\"}" | tr -d '"') + echo "Authenticated as ${ZABBIX_USER}." +fi + +# Webhook JavaScript +WEBHOOK_JS=$(cat <<'JSEOF' +var params = JSON.parse(value); +var req = new HttpRequest(); +req.addHeader('Content-Type: application/json'); +var auth = Base64.encode(params.RABBITMQ_USER + ':' + params.RABBITMQ_PASSWORD); +req.addHeader('Authorization: Basic ' + auth); +var vhost = encodeURIComponent(params.RABBITMQ_VHOST || '/'); +var exchange = encodeURIComponent(params.RABBITMQ_EXCHANGE || 'st2.zabbix'); +var url = params.RABBITMQ_URL + '/api/exchanges/' + vhost + '/' + exchange + '/publish'; +var message = { + properties: {content_type: 'application/json', delivery_mode: 2}, + routing_key: params.RABBITMQ_ROUTING_KEY || 'zabbix.alerts', + payload: JSON.stringify({ + trigger: 'zabbix.event_handler', + payload: { + alert_sendto: params.To, + alert_subject: params.Subject, + alert_message: params.Message, + host: params.HostName, + event_id: params.EventID, + trigger_id: params.TriggerID, + trigger_name: params.TriggerName, + trigger_status: params.TriggerStatus, + trigger_severity: params.TriggerSeverity, + event_time: params.EventTime, + event_date: params.EventDate + } + }), + payload_encoding: 'string' +}; +var resp = req.post(url, JSON.stringify(message)); +if (req.getStatus() >= 200 && req.getStatus() < 300) { + return 'OK'; +} else { + throw 'RabbitMQ publish failed with status ' + req.getStatus() + ': ' + resp; +} +JSEOF +) + +# Build parameters JSON array +PARAMETERS='[ + {"name": "RABBITMQ_URL", "value": "'"${RABBITMQ_URL}"'"}, + {"name": "RABBITMQ_USER", "value": "'"${RABBITMQ_USER}"'"}, + {"name": "RABBITMQ_PASSWORD", "value": "'"${RABBITMQ_PASSWORD}"'"}, + {"name": "RABBITMQ_VHOST", "value": "'"${RABBITMQ_VHOST}"'"}, + {"name": "RABBITMQ_EXCHANGE", "value": "'"${RABBITMQ_EXCHANGE}"'"}, + {"name": "RABBITMQ_ROUTING_KEY", "value": "'"${RABBITMQ_ROUTING_KEY}"'"}, + {"name": "To", "value": "{ALERT.SENDTO}"}, + {"name": "Subject", "value": "{ALERT.SUBJECT}"}, + {"name": "Message", "value": "{ALERT.MESSAGE}"}, + {"name": "HostName", "value": "{HOST.NAME}"}, + {"name": "EventID", "value": "{EVENT.ID}"}, + {"name": "TriggerID", "value": "{TRIGGER.ID}"}, + {"name": "TriggerName", "value": "{TRIGGER.NAME}"}, + {"name": "TriggerStatus", "value": "{TRIGGER.STATUS}"}, + {"name": "TriggerSeverity", "value": "{TRIGGER.SEVERITY}"}, + {"name": "EventTime", "value": "{EVENT.TIME}"}, + {"name": "EventDate", "value": "{EVENT.DATE}"} +]' + +# Escape JS for JSON embedding +WEBHOOK_JS_ESCAPED=$(echo "${WEBHOOK_JS}" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))") + +# Check if media type already exists +echo "Checking for existing '${MEDIA_TYPE_NAME}' media type..." +EXISTING=$(zabbix_api "mediatype.get" "{\"filter\": {\"name\": \"${MEDIA_TYPE_NAME}\"}}") +EXISTING_ID=$(echo "${EXISTING}" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d[0]['mediatypeid'] if d else '')" 2>/dev/null || true) + +if [[ -n "${EXISTING_ID}" ]]; then + echo "Updating existing media type (ID: ${EXISTING_ID})..." + zabbix_api "mediatype.update" "{ + \"mediatypeid\": \"${EXISTING_ID}\", + \"name\": \"${MEDIA_TYPE_NAME}\", + \"type\": \"4\", + \"script\": ${WEBHOOK_JS_ESCAPED}, + \"parameters\": ${PARAMETERS}, + \"timeout\": \"30s\", + \"process_tags\": \"0\", + \"description\": \"Publishes Zabbix alerts to RabbitMQ for StackStorm consumption.\" + }" > /dev/null + MEDIA_TYPE_ID="${EXISTING_ID}" + echo "Media type updated." +else + echo "Creating '${MEDIA_TYPE_NAME}' webhook media type..." + RESULT=$(zabbix_api "mediatype.create" "{ + \"name\": \"${MEDIA_TYPE_NAME}\", + \"type\": \"4\", + \"script\": ${WEBHOOK_JS_ESCAPED}, + \"parameters\": ${PARAMETERS}, + \"timeout\": \"30s\", + \"process_tags\": \"0\", + \"description\": \"Publishes Zabbix alerts to RabbitMQ for StackStorm consumption.\" + }") + MEDIA_TYPE_ID=$(echo "${RESULT}" | python3 -c "import sys,json; print(json.load(sys.stdin)['mediatypeids'][0])") + echo "Media type created (ID: ${MEDIA_TYPE_ID})." +fi + +# Add media to Admin user +echo "Configuring media on user ID ${ZABBIX_ADMIN_USER_ID}..." +EXISTING_MEDIA=$(zabbix_api "user.get" "{\"userids\": \"${ZABBIX_ADMIN_USER_ID}\", \"selectMedias\": \"extend\"}") +HAS_MEDIA=$(echo "${EXISTING_MEDIA}" | python3 -c " +import sys, json +users = json.load(sys.stdin) +if users: + for m in users[0].get('medias', []): + if m.get('mediatypeid') == '${MEDIA_TYPE_ID}': + print('yes') + break +" 2>/dev/null || true) + +if [[ "${HAS_MEDIA}" != "yes" ]]; then + CURRENT_MEDIAS=$(echo "${EXISTING_MEDIA}" | python3 -c " +import sys, json +users = json.load(sys.stdin) +medias = users[0].get('medias', []) if users else [] +for m in medias: + m.pop('mediaid', None) + m.pop('userid', None) +medias.append({ + 'mediatypeid': '${MEDIA_TYPE_ID}', + 'sendto': 'stackstorm-rabbitmq', + 'active': '0', + 'severity': '63', + 'period': '1-7,00:00-24:00' +}) +print(json.dumps(medias)) +") + zabbix_api "user.update" "{ + \"userid\": \"${ZABBIX_ADMIN_USER_ID}\", + \"medias\": ${CURRENT_MEDIAS} + }" > /dev/null + echo "Media assigned to user." +else + echo "Media already assigned to user." +fi + +echo "" +echo "=== Registration Complete ===" +echo "Media Type: ${MEDIA_TYPE_NAME} (ID: ${MEDIA_TYPE_ID})" +echo "RabbitMQ Exchange: ${RABBITMQ_EXCHANGE} (vhost: ${RABBITMQ_VHOST})" +echo "RabbitMQ Queue: ${RABBITMQ_QUEUE}" +echo "Routing Key: ${RABBITMQ_ROUTING_KEY}" +echo "" +echo "Next steps:" +echo " 1. Create a Zabbix Action (Alerts > Actions > Trigger actions) that" +echo " uses this media type to send notifications on problem events." +echo " 2. Install the stackstorm-rabbitmq pack: st2 pack install rabbitmq" +echo " 3. Configure rabbitmq pack to consume from queue '${RABBITMQ_QUEUE}'" +echo " 4. Create StackStorm rules to process rabbitmq.new_message triggers" +echo " (see rules/zabbix_rabbitmq_bridge.yaml for an example)" diff --git a/scripts/register_webhook_st2.sh b/scripts/register_webhook_st2.sh new file mode 100755 index 0000000..54b7dbd --- /dev/null +++ b/scripts/register_webhook_st2.sh @@ -0,0 +1,219 @@ +#!/usr/bin/env bash +# Register the "StackStorm Direct" webhook media type in Zabbix. +# +# This script authenticates to the Zabbix API and creates/updates a webhook +# media type (type=4) that POSTs alerts directly to the StackStorm API. +# +# Required environment variables: +# ZABBIX_URL - Zabbix frontend URL (e.g. http://localhost:8080) +# ST2_API_URL - StackStorm API URL (e.g. http://localhost:81) +# ST2_API_KEY - StackStorm API key for authentication +# +# Authentication (one of): +# ZABBIX_API_TOKEN - Zabbix API token (preferred) +# ZABBIX_USER - Zabbix username (default: Admin) +# ZABBIX_PASSWORD - Zabbix password (default: zabbix) +# +# Optional: +# ZABBIX_ADMIN_USER_ID - Zabbix user ID to assign media to (default: 1 = Admin) + +set -euo pipefail + +: "${ZABBIX_URL:?ZABBIX_URL is required}" +: "${ST2_API_URL:?ST2_API_URL is required}" +: "${ST2_API_KEY:?ST2_API_KEY is required}" + +ZABBIX_USER="${ZABBIX_USER:-Admin}" +ZABBIX_PASSWORD="${ZABBIX_PASSWORD:-zabbix}" +ZABBIX_ADMIN_USER_ID="${ZABBIX_ADMIN_USER_ID:-1}" +MEDIA_TYPE_NAME="StackStorm Direct" + +# JSON-RPC request helper +request_id=1 +zabbix_api() { + local method="$1" + local params="$2" + local auth_field="" + + if [[ -n "${AUTH_TOKEN:-}" ]]; then + auth_field="\"auth\": \"${AUTH_TOKEN}\"," + fi + + local payload + payload=$(cat </dev/null || true) + if [[ -n "${error}" ]]; then + echo "ERROR: Zabbix API ${method} failed: ${error}" >&2 + exit 1 + fi + + echo "${response}" | python3 -c "import sys,json; print(json.dumps(json.load(sys.stdin).get('result')))" +} + +# Authenticate +echo "Authenticating to Zabbix at ${ZABBIX_URL}..." +AUTH_TOKEN="" +if [[ -n "${ZABBIX_API_TOKEN:-}" ]]; then + AUTH_TOKEN="${ZABBIX_API_TOKEN}" + echo "Using API token authentication." +else + AUTH_TOKEN=$(zabbix_api "user.login" "{\"username\": \"${ZABBIX_USER}\", \"password\": \"${ZABBIX_PASSWORD}\"}" | tr -d '"') + echo "Authenticated as ${ZABBIX_USER}." +fi + +# Webhook JavaScript +WEBHOOK_JS=$(cat <<'JSEOF' +var params = JSON.parse(value); +var req = new HttpRequest(); +req.addHeader('Content-Type: application/json'); +req.addHeader('St2-Api-Key: ' + params.ST2_API_KEY); +var payload = JSON.stringify({ + trigger: 'zabbix.event_handler', + payload: { + alert_sendto: params.To, + alert_subject: params.Subject, + alert_message: params.Message, + host: params.HostName, + event_id: params.EventID, + trigger_id: params.TriggerID, + trigger_name: params.TriggerName, + trigger_status: params.TriggerStatus, + trigger_severity: params.TriggerSeverity, + event_time: params.EventTime, + event_date: params.EventDate + } +}); +var url = params.ST2_API_URL + '/api/v1/webhooks/st2'; +var resp = req.post(url, payload); +if (req.getStatus() >= 200 && req.getStatus() < 300) { + return 'OK'; +} else { + throw 'Failed with status ' + req.getStatus() + ': ' + resp; +} +JSEOF +) + +# Build parameters JSON array +PARAMETERS='[ + {"name": "ST2_API_URL", "value": "'"${ST2_API_URL}"'"}, + {"name": "ST2_API_KEY", "value": "'"${ST2_API_KEY}"'"}, + {"name": "HTTPProxy", "value": ""}, + {"name": "To", "value": "{ALERT.SENDTO}"}, + {"name": "Subject", "value": "{ALERT.SUBJECT}"}, + {"name": "Message", "value": "{ALERT.MESSAGE}"}, + {"name": "HostName", "value": "{HOST.NAME}"}, + {"name": "EventID", "value": "{EVENT.ID}"}, + {"name": "TriggerID", "value": "{TRIGGER.ID}"}, + {"name": "TriggerName", "value": "{TRIGGER.NAME}"}, + {"name": "TriggerStatus", "value": "{TRIGGER.STATUS}"}, + {"name": "TriggerSeverity", "value": "{TRIGGER.SEVERITY}"}, + {"name": "EventTime", "value": "{EVENT.TIME}"}, + {"name": "EventDate", "value": "{EVENT.DATE}"} +]' + +# Escape JS for JSON embedding +WEBHOOK_JS_ESCAPED=$(echo "${WEBHOOK_JS}" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))") + +# Check if media type already exists +echo "Checking for existing '${MEDIA_TYPE_NAME}' media type..." +EXISTING=$(zabbix_api "mediatype.get" "{\"filter\": {\"name\": \"${MEDIA_TYPE_NAME}\"}}") +EXISTING_ID=$(echo "${EXISTING}" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d[0]['mediatypeid'] if d else '')" 2>/dev/null || true) + +if [[ -n "${EXISTING_ID}" ]]; then + echo "Updating existing media type (ID: ${EXISTING_ID})..." + zabbix_api "mediatype.update" "{ + \"mediatypeid\": \"${EXISTING_ID}\", + \"name\": \"${MEDIA_TYPE_NAME}\", + \"type\": \"4\", + \"script\": ${WEBHOOK_JS_ESCAPED}, + \"parameters\": ${PARAMETERS}, + \"timeout\": \"30s\", + \"process_tags\": \"0\", + \"description\": \"Posts Zabbix alerts directly to StackStorm API webhook endpoint.\" + }" > /dev/null + MEDIA_TYPE_ID="${EXISTING_ID}" + echo "Media type updated." +else + echo "Creating '${MEDIA_TYPE_NAME}' webhook media type..." + RESULT=$(zabbix_api "mediatype.create" "{ + \"name\": \"${MEDIA_TYPE_NAME}\", + \"type\": \"4\", + \"script\": ${WEBHOOK_JS_ESCAPED}, + \"parameters\": ${PARAMETERS}, + \"timeout\": \"30s\", + \"process_tags\": \"0\", + \"description\": \"Posts Zabbix alerts directly to StackStorm API webhook endpoint.\" + }") + MEDIA_TYPE_ID=$(echo "${RESULT}" | python3 -c "import sys,json; print(json.load(sys.stdin)['mediatypeids'][0])") + echo "Media type created (ID: ${MEDIA_TYPE_ID})." +fi + +# Add media to Admin user +echo "Configuring media on user ID ${ZABBIX_ADMIN_USER_ID}..." +EXISTING_MEDIA=$(zabbix_api "user.get" "{\"userids\": \"${ZABBIX_ADMIN_USER_ID}\", \"selectMedias\": \"extend\"}") +HAS_MEDIA=$(echo "${EXISTING_MEDIA}" | python3 -c " +import sys, json +users = json.load(sys.stdin) +if users: + for m in users[0].get('medias', []): + if m.get('mediatypeid') == '${MEDIA_TYPE_ID}': + print('yes') + break +" 2>/dev/null || true) + +if [[ "${HAS_MEDIA}" != "yes" ]]; then + # Get current medias and append new one + CURRENT_MEDIAS=$(echo "${EXISTING_MEDIA}" | python3 -c " +import sys, json +users = json.load(sys.stdin) +medias = users[0].get('medias', []) if users else [] +# Strip mediaid so Zabbix treats them as new entries during update +for m in medias: + m.pop('mediaid', None) + m.pop('userid', None) +medias.append({ + 'mediatypeid': '${MEDIA_TYPE_ID}', + 'sendto': 'stackstorm', + 'active': '0', + 'severity': '63', + 'period': '1-7,00:00-24:00' +}) +print(json.dumps(medias)) +") + zabbix_api "user.update" "{ + \"userid\": \"${ZABBIX_ADMIN_USER_ID}\", + \"medias\": ${CURRENT_MEDIAS} + }" > /dev/null + echo "Media assigned to user." +else + echo "Media already assigned to user." +fi + +echo "" +echo "=== Registration Complete ===" +echo "Media Type: ${MEDIA_TYPE_NAME} (ID: ${MEDIA_TYPE_ID})" +echo "Webhook URL: ${ST2_API_URL}/api/v1/webhooks/st2" +echo "" +echo "Next steps:" +echo " 1. Create a Zabbix Action (Alerts > Actions > Trigger actions) that" +echo " uses this media type to send notifications on problem events." +echo " 2. Ensure the StackStorm zabbix.event_handler trigger is registered" +echo " and rules are configured to process incoming alerts." diff --git a/spec/localhost/tools_register_config_for_st2_spec.rb b/spec/localhost/tools_register_config_for_st2_spec.rb deleted file mode 100644 index 28aedec..0000000 --- a/spec/localhost/tools_register_config_for_st2_spec.rb +++ /dev/null @@ -1,52 +0,0 @@ -require 'zbxapi' -require 'spec_helper' - -ZABBIX_USER = ENV['ZABBIX_USER'] || 'admin' -ZABBIX_SENDTO = ENV['ZABBIX_SENDTO'] || 'admin' -ZABBIX_PASSWORD = ENV['ZABBIX_PASSWORD'] || 'zabbix' -ZABBIX_API_ENDPOINT = ENV['ZABBIX_API'] || 'http://localhost/' - -describe 'Tests for registering Zabbix for StackStorm' do - before(:all) do - @client = ZabbixAPI.new(ZABBIX_API_ENDPOINT) - - expect(try_to_login).not_to be_a(RuntimeError) - end - - # run script to register configurations for StackStorm to the Zabbix - describe command("tools/register_st2_config_to_zabbix.py " \ - "-u #{ ZABBIX_USER } " \ - "-s #{ ZABBIX_SENDTO } " \ - "-p #{ ZABBIX_PASSWORD } " \ - "-z #{ ZABBIX_API_ENDPOINT }") do - its(:exit_status) { should eq 0 } - its(:stdout) do - should match /^Success to register the configurations for StackStorm to the Zabbix Server./ - end - - describe 'Check each configurations are actually set in the Zabbix' do - it 'MediaType configuration is set' do - expect(@client.mediatype.get.find {|x| x['description'] == 'StackStorm'}).to_not be_nil - end - - it 'Action configuration is set' do - expect(@client.action.get.find { |x| x['name'].include?('StackStorm')}).to_not be_nil - end - end - end - - # This method wait to start and initialize Zabbix-server and Zabbix-Web - def try_to_login(retry_count = 0) - begin - return @client.login('admin', 'zabbix') - rescue => e - if retry_count < 60 - # make a delay before retrying - sleep 1 - return try_to_login(retry_count + 1) - else - return e - end - end - end -end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb deleted file mode 100644 index 677ae95..0000000 --- a/spec/spec_helper.rb +++ /dev/null @@ -1,4 +0,0 @@ -require 'serverspec' - -set :backend, :exec - diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..9661d46 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,277 @@ +# Tests + +This document describes how the test suite is structured, how to run it, and how to add new tests. + +--- + +## Running Tests + +From the pack root (`stackstorm-zabbix/`): + +```bash +# Run all tests +python3 -m pytest tests/ -v + +# Run a single test file +python3 -m pytest tests/test_call_api.py -v + +# Run a specific test +python3 -m pytest tests/test_find_object.py::FindObjectTestCase::test_find_single_host -v +``` + +### Prerequisites + +- Python 3.12+ +- Virtual environment with dependencies installed: + +```bash +pip install -r requirements.txt +``` + +The `requirements.txt` includes `st2common` (provides the test base class) and `zabbix-utils`. + +--- + +## Architecture + +### Path Setup + +The `conftest.py` at the pack root adds `actions/` and `tests/` to `sys.path`. This mirrors how StackStorm resolves imports at runtime — action Python files import from `lib.actions` as a relative path, and tests import action classes directly by module name. + +```python +# conftest.py +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'actions')) +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'tests')) +``` + +### Base Test Class + +All tests extend `ZabbixBaseActionTestCase` (defined in `zabbix_base_action_test_case.py`): + +```python +from zabbix_base_action_test_case import ZabbixBaseActionTestCase +from my_action import MyAction + +class MyActionTestCase(ZabbixBaseActionTestCase): + __test__ = True + action_cls = MyAction +``` + +This base class: +- Extends `st2tests.base.BaseActionTestCase` (provides `get_action_instance()`, `get_fixture_content()`) +- Loads fixture configs in `setUp()`: `self.full_config` and `self.blank_config` +- Provides `load_yaml()` and `load_json()` helpers for fixture files + +### Key Attributes + +| Attribute | Purpose | +|-----------|---------| +| `__test__ = True` | Tells pytest to collect this class (set `False` on base classes) | +| `action_cls = MyAction` | The action class under test — required by `get_action_instance()` | +| `self.full_config` | Valid Zabbix config (URL + credentials) loaded from `fixtures/full.yaml` | +| `self.blank_config` | Empty config — triggers `ValueError` on action instantiation | + +--- + +## Fixtures + +Located in `tests/fixtures/`: + +| File | Purpose | +|------|---------| +| `full.yaml` | Symlink to `../../zabbix.yaml.example` — valid pack config with URL, username, password | +| `token.yaml` | Config with API token auth — tests token-based authentication path | +| `blank.yaml` | Empty YAML — used to test config validation errors | + +Add new fixture files here for complex test scenarios (JSON responses, multi-host data, etc.). + +--- + +## Test Patterns + +### Pattern 1: Testing a dedicated Python action + +```python +import mock + +from zabbix_base_action_test_case import ZabbixBaseActionTestCase +from my_action import MyAction + +from zabbix_utils.exceptions import APIRequestError + + +class MyActionTestCase(ZabbixBaseActionTestCase): + __test__ = True + action_cls = MyAction + + @mock.patch('lib.actions.ZabbixBaseAction.connect') + def test_happy_path(self, mock_connect): + action = self.get_action_instance(self.full_config) + action.client = mock.Mock() + action.client.host.get.return_value = [{'hostid': '10084'}] + + result = action.run(hostname='myhost') + self.assertEqual(result, '10084') + + @mock.patch('lib.actions.ZabbixBaseAction.connect') + def test_not_found_raises(self, mock_connect): + action = self.get_action_instance(self.full_config) + action.client = mock.Mock() + action.client.host.get.return_value = [] + + with self.assertRaises(ValueError): + action.run(hostname='nonexistent') + + @mock.patch('lib.actions.ZabbixBaseAction.connect') + def test_api_error_propagates(self, mock_connect): + action = self.get_action_instance(self.full_config) + action.client = mock.Mock() + action.client.host.get.side_effect = APIRequestError('server error') + + with self.assertRaises(APIRequestError): + action.run(hostname='myhost') +``` + +### Pattern 2: Testing connection failures + +```python +@mock.patch('lib.actions.ZabbixBaseAction.connect') +def test_connection_error(self, mock_connect): + action = self.get_action_instance(self.full_config) + mock_connect.side_effect = ProcessingError('connection error') + + with self.assertRaises(ProcessingError): + action.run(hostname='test') +``` + +### Pattern 3: Testing config validation + +```python +def test_blank_config_raises(self): + self.assertRaises(ValueError, self.action_cls, self.blank_config) +``` + +### Pattern 4: Mocking multi-level API calls + +For `call_api.py` style tests where the API path is dotted (e.g. `host.get`): + +```python +@mock.patch('lib.actions.ZabbixBaseAction.connect') +def test_dotted_method(self, mock_connect): + action = self.get_action_instance(self.full_config) + action.client = mock.Mock(spec=['host']) + action.client.host = mock.Mock(spec=['get']) + action.client.host.get.return_value = [{'hostid': '1'}] + + result = action.run(api_method='host.get', filter={'host': 'test'}) + self.assertEqual(result, [{'hostid': '1'}]) +``` + +### Pattern 5: Testing helper methods on the base class + +```python +@mock.patch('lib.actions.ZabbixAPI') +def test_find_host(self, mock_client): + action = self.get_action_instance(self.full_config) + mock_client.host.get.return_value = [{'hostid': '1', 'host': 'test'}] + action.client = mock_client + + result = action.find_host('test') + self.assertEqual(result, '1') +``` + +--- + +## Mocking Strategy + +### Always mock `connect()` + +Every test that calls `action.run()` must mock `ZabbixBaseAction.connect` to prevent real network calls: + +```python +@mock.patch('lib.actions.ZabbixBaseAction.connect') +def test_something(self, mock_connect): + ... +``` + +### Mock `self.client` directly + +After mocking `connect()`, set `action.client` to a `mock.Mock()` to control API responses: + +```python +action = self.get_action_instance(self.full_config) +action.client = mock.Mock() +action.client.host.get.return_value = [...] +``` + +### Mock helper methods when testing higher-level logic + +If an action calls `self.find_host()`, mock it to isolate the test: + +```python +action.find_host = mock.MagicMock(return_value='10084') +``` + +--- + +## What to Test + +Each test file should cover: + +| Category | What to Assert | +|----------|---------------| +| **Happy path** | Correct return value, correct API method called with expected args | +| **Not found** | `ValueError` raised when lookup yields no results | +| **Multiple found** | `ValueError` raised when unique lookup is ambiguous | +| **API errors** | `APIRequestError` propagates (or re-raises with context) | +| **Connection errors** | `ProcessingError` propagates | +| **Edge cases** | `None` parameters filtered, empty lists handled, boundary values | + +### Coverage goals + +- Every dedicated Python action file (`actions/*.py`) must have a corresponding `tests/test_*.py` +- YAML-only actions (using `call_api.py`) do NOT need individual tests — they are covered by `test_call_api.py` which validates the dispatcher logic +- Base class helpers are tested in `test_action_base.py` + +--- + +## Adding a New Test File + +1. Create `tests/test_.py` +2. Import the action class and base test case: + +```python +import mock +from zabbix_base_action_test_case import ZabbixBaseActionTestCase +from my_action import MyAction +``` + +3. Define the test class: + +```python +class MyActionTestCase(ZabbixBaseActionTestCase): + __test__ = True + action_cls = MyAction +``` + +4. Write tests covering happy path, error cases, and edge cases +5. Run and verify: + +```bash +python3 -m pytest tests/test_my_action.py -v +``` + +--- + +## File Naming + +| Convention | Example | +|------------|---------| +| Test file | `test_.py` | +| Test class | `TestCase` | +| Test method | `test_` | + +The test filename maps to the Python entry point it tests, **not** the YAML action name. For example: +- `call_api.py` → `test_call_api.py` +- `find_object.py` → `test_find_object.py` +- `delete_host.py` → `test_delete_host.py` diff --git a/tests/fixtures/token.yaml b/tests/fixtures/token.yaml new file mode 100644 index 0000000..9192909 --- /dev/null +++ b/tests/fixtures/token.yaml @@ -0,0 +1,3 @@ +--- +url: http://localhost:8080 +api_token: my-test-token-12345 diff --git a/tests/test_acknowledge_event.py b/tests/test_acknowledge_event.py new file mode 100644 index 0000000..1ac49e4 --- /dev/null +++ b/tests/test_acknowledge_event.py @@ -0,0 +1,42 @@ +import mock + +from zabbix_base_action_test_case import ZabbixBaseActionTestCase +from acknowledge_event import AcknowledgeEvent + +from zabbix_utils.exceptions import APIRequestError + + +class AcknowledgeEventTestCase(ZabbixBaseActionTestCase): + __test__ = True + action_cls = AcknowledgeEvent + + @mock.patch('lib.actions.ZabbixBaseAction.connect') + def test_acknowledge_with_close(self, mock_connect): + action = self.get_action_instance(self.full_config) + action.client = mock.Mock() + action.client.event.acknowledge.return_value = {'eventids': ['123']} + + result = action.run(eventid='123', message='Fixed', will_close=True) + action.client.event.acknowledge.assert_called_with( + eventids='123', message='Fixed', action=1) + self.assertEqual(result, {'eventids': ['123']}) + + @mock.patch('lib.actions.ZabbixBaseAction.connect') + def test_acknowledge_without_close(self, mock_connect): + action = self.get_action_instance(self.full_config) + action.client = mock.Mock() + action.client.event.acknowledge.return_value = {'eventids': ['456']} + + result = action.run(eventid='456', message='Acknowledged', will_close=False) + action.client.event.acknowledge.assert_called_with( + eventids='456', message='Acknowledged', action=0) + self.assertEqual(result, {'eventids': ['456']}) + + @mock.patch('lib.actions.ZabbixBaseAction.connect') + def test_acknowledge_api_error(self, mock_connect): + action = self.get_action_instance(self.full_config) + action.client = mock.Mock() + action.client.event.acknowledge.side_effect = APIRequestError('failed') + + with self.assertRaises(APIRequestError): + action.run(eventid='789', message='test') diff --git a/tests/test_action_base.py b/tests/test_action_base.py index 4ed668d..c35df22 100644 --- a/tests/test_action_base.py +++ b/tests/test_action_base.py @@ -1,200 +1,264 @@ import mock from zabbix_base_action_test_case import ZabbixBaseActionTestCase -from event_action_runner import EventActionRunner +from verify_credentials import VerifyCredentials -from six.moves.urllib.error import URLError -from pyzabbix.api import ZabbixAPIException +from zabbix_utils.exceptions import ProcessingError +from zabbix_utils.exceptions import APIRequestError -class EventActionTestCase(ZabbixBaseActionTestCase): +class BaseActionTestCase(ZabbixBaseActionTestCase): __test__ = True - action_cls = EventActionRunner + action_cls = VerifyCredentials def test_run_action_without_configuration(self): self.assertRaises(ValueError, self.action_cls, self.blank_config) - @mock.patch('lib.actions.ZabbixAPI') - def test_run_action_with_invalid_config_of_endpoint(self, mock_client): - # make an exception that means failure to connect server. - mock_client.side_effect = URLError('connection error') + def test_init_with_token_only_config(self): + action = self.get_action_instance(self.token_config) + self.assertIsNotNone(action) - action = self.get_action_instance(self.full_config) + def test_init_missing_auth(self): + config = {"url": "http://localhost:8080"} + with self.assertRaises(ValueError): + self.action_cls(config) - with self.assertRaises(URLError): - action.run(action='something') + def test_init_empty_username(self): + config = {"url": "http://localhost:8080", "username": "", "password": "zabbix"} + with self.assertRaises(ValueError): + self.action_cls(config) - @mock.patch('lib.actions.ZabbixAPI') - def test_run_action_with_invalid_config_of_account(self, mock_client): - # make an exception that means failure to authenticate with Zabbix-server. - mock_client.side_effect = ZabbixAPIException('auth error') + def test_init_empty_password(self): + config = {"url": "http://localhost:8080", "username": "Admin", "password": ""} + with self.assertRaises(ValueError): + self.action_cls(config) - action = self.get_action_instance(self.full_config) + def test_init_none_credentials(self): + config = {"url": "http://localhost:8080", "username": None, "password": None} + with self.assertRaises(ValueError): + self.action_cls(config) - with self.assertRaises(ZabbixAPIException): - action.run(action='something') + @mock.patch("lib.actions.ZabbixAPI") + def test_connect_with_token(self, mock_zabbix_cls): + mock_client = mock.Mock() + mock_zabbix_cls.return_value = mock_client - @mock.patch('lib.actions.ZabbixAPI') - def test_run_action_with_invalid_config_of_action(self, mock_client): - mock_obj = mock.Mock() - mock_obj.invalid = [] + action = self.get_action_instance(self.token_config) + action.connect() - mock_client.return_value = mock_obj + mock_zabbix_cls.assert_called_with(url="http://localhost:8080") + mock_client.login.assert_called_once_with(token="my-test-token-12345") + + @mock.patch("lib.actions.ZabbixAPI") + def test_connect_with_username_password(self, mock_zabbix_cls): + mock_client = mock.Mock() + mock_zabbix_cls.return_value = mock_client action = self.get_action_instance(self.full_config) - result = action.run(action='invalid.action') + action.connect() - self.assertFalse(result[0]) - self.assertEqual(result[1], "Specified action(invalid.action) is invalid") + mock_client.login.assert_called_once_with(user="Admin", password="zabbix") - @mock.patch('lib.actions.ZabbixAPI') - def test_run_action_with_valid_config(self, mock_client): - def mock_double(param): - return param * 2 + @mock.patch("lib.actions.ZabbixAPI") + def test_run_action_with_invalid_config_of_endpoint(self, mock_client): + mock_client.side_effect = ProcessingError("connection error") - mock_handler = mock.Mock() - mock_handler.double = mock.Mock(side_effect=mock_double) + action = self.get_action_instance(self.full_config) - mock_obj = mock.Mock() - mock_obj.action = mock_handler + with self.assertRaises(ProcessingError): + action.run() - mock_client.return_value = mock_obj + @mock.patch("lib.actions.ZabbixAPI") + def test_run_action_with_invalid_config_of_account(self, mock_client): + mock_client.side_effect = APIRequestError("auth error") action = self.get_action_instance(self.full_config) - result = action.run(action='action.double', param=4) - self.assertTrue(result[0]) - self.assertEqual(result[1], 8) + with self.assertRaises(APIRequestError): + action.run() - @mock.patch('lib.actions.ZabbixAPI') + @mock.patch("lib.actions.ZabbixAPI") def test_find_host(self, mock_client): action = self.get_action_instance(self.full_config) - test_dict = {'host_name': "test", 'hostid': "1"} + test_dict = {"host_name": "test", "hostid": "1"} mock_client.host.get.return_value = [test_dict] action.client = mock_client - result = action.find_host(test_dict['host_name']) - self.assertEqual(result, test_dict['hostid']) + result = action.find_host(test_dict["host_name"]) + self.assertEqual(result, test_dict["hostid"]) - @mock.patch('lib.actions.ZabbixAPI') + @mock.patch("lib.actions.ZabbixAPI") def test_find_host_no_host(self, mock_client): action = self.get_action_instance(self.full_config) - test_dict = {'host_name': "test", 'host_id': "1"} + test_dict = {"host_name": "test", "host_id": "1"} mock_client.host.get.return_value = [] action.client = mock_client with self.assertRaises(ValueError): - action.find_host(test_dict['host_name']) + action.find_host(test_dict["host_name"]) - @mock.patch('lib.actions.ZabbixAPI') + @mock.patch("lib.actions.ZabbixAPI") def test_find_host_too_many_host(self, mock_client): action = self.get_action_instance(self.full_config) - test_dict = [{'host_name': "test", 'hostid': "1"}, - {'host_name': "test", 'hostid': "2"}] + test_dict = [ + {"host_name": "test", "hostid": "1"}, + {"host_name": "test", "hostid": "2"}, + ] mock_client.host.get.return_value = test_dict action.client = mock_client with self.assertRaises(ValueError): - action.find_host(test_dict[0]['host_name']) + action.find_host(test_dict[0]["host_name"]) - @mock.patch('lib.actions.ZabbixAPI') + @mock.patch("lib.actions.ZabbixAPI") def test_find_host_fail(self, mock_client): action = self.get_action_instance(self.full_config) - test_dict = {'host_name': "test", 'hostid': "1"} - mock_client.host.get.side_effect = ZabbixAPIException('host error') + test_dict = {"host_name": "test", "hostid": "1"} + mock_client.host.get.side_effect = APIRequestError("host error") mock_client.host.get.return_value = [test_dict] action.client = mock_client - with self.assertRaises(ZabbixAPIException): - action.find_host(test_dict['host_name']) + with self.assertRaises(APIRequestError): + action.find_host(test_dict["host_name"]) - @mock.patch('lib.actions.ZabbixAPI') + @mock.patch("lib.actions.ZabbixAPI") def test_maintenance_get(self, mock_client): action = self.get_action_instance(self.full_config) - test_dict = {'maintenance_name': "test", 'maintenanceid': "1"} + test_dict = {"maintenance_name": "test", "maintenanceid": "1"} mock_client.maintenance.get.return_value = [test_dict] action.client = mock_client - result = action.maintenance_get(test_dict['maintenance_name']) + result = action.maintenance_get(test_dict["maintenance_name"]) self.assertEqual(result, [test_dict]) - @mock.patch('lib.actions.ZabbixAPI') + @mock.patch("lib.actions.ZabbixAPI") def test_maintenance_get_fail(self, mock_client): action = self.get_action_instance(self.full_config) - test_dict = {'maintenance_name': "test", 'maintenanceid': "1"} - mock_client.maintenance.get.side_effect = ZabbixAPIException('maintenance error') + test_dict = {"maintenance_name": "test", "maintenanceid": "1"} + mock_client.maintenance.get.side_effect = APIRequestError("maintenance error") mock_client.maintenance.get.return_value = [test_dict] action.client = mock_client - with self.assertRaises(ZabbixAPIException): - action.maintenance_get(test_dict['maintenance_name']) + with self.assertRaises(APIRequestError): + action.maintenance_get(test_dict["maintenance_name"]) - @mock.patch('lib.actions.ZabbixBaseAction.maintenance_get') - @mock.patch('lib.actions.ZabbixAPI') - def test_maintenance_create_or_update_update(self, mock_client, mock_maintenance_get): + @mock.patch("lib.actions.ZabbixBaseAction.maintenance_get") + @mock.patch("lib.actions.ZabbixAPI") + def test_maintenance_create_or_update_update( + self, mock_client, mock_maintenance_get + ): action = self.get_action_instance(self.full_config) - test_dict = {'name': "test"} - maintenance_dict = {'maintenance_name': "test", 'maintenanceid': "1"} + test_dict = {"name": "test"} + maintenance_dict = {"maintenance_name": "test", "maintenanceid": "1"} mock_maintenance_get.return_value = [maintenance_dict] - mock_client.maintenance.update.return_value = [maintenance_dict['maintenanceid']] + mock_client.maintenance.update.return_value = [ + maintenance_dict["maintenanceid"] + ] action.client = mock_client result = action.maintenance_create_or_update(test_dict) - self.assertEqual(result, [maintenance_dict['maintenanceid']]) + self.assertEqual(result, [maintenance_dict["maintenanceid"]]) - @mock.patch('lib.actions.ZabbixBaseAction.maintenance_get') - @mock.patch('lib.actions.ZabbixAPI') - def test_maintenance_create_or_update_update_fail(self, mock_client, mock_maintenance_get): + @mock.patch("lib.actions.ZabbixBaseAction.maintenance_get") + @mock.patch("lib.actions.ZabbixAPI") + def test_maintenance_create_or_update_update_fail( + self, mock_client, mock_maintenance_get + ): action = self.get_action_instance(self.full_config) - test_dict = {'name': "test"} - maintenance_dict = {'maintenance_name': "test", 'maintenanceid': "1"} + test_dict = {"name": "test"} + maintenance_dict = {"maintenance_name": "test", "maintenanceid": "1"} mock_maintenance_get.return_value = [maintenance_dict] - mock_client.maintenance.update.return_value = [maintenance_dict['maintenanceid']] - mock_client.maintenance.update.side_effect = ZabbixAPIException('maintenance error') + mock_client.maintenance.update.return_value = [ + maintenance_dict["maintenanceid"] + ] + mock_client.maintenance.update.side_effect = APIRequestError( + "maintenance error" + ) action.client = mock_client - with self.assertRaises(ZabbixAPIException): + with self.assertRaises(APIRequestError): action.maintenance_create_or_update(test_dict) - @mock.patch('lib.actions.ZabbixBaseAction.maintenance_get') - @mock.patch('lib.actions.ZabbixAPI') - def test_maintenance_create_or_update_create(self, mock_client, mock_maintenance_get): + @mock.patch("lib.actions.ZabbixBaseAction.maintenance_get") + @mock.patch("lib.actions.ZabbixAPI") + def test_maintenance_create_or_update_create( + self, mock_client, mock_maintenance_get + ): action = self.get_action_instance(self.full_config) - test_dict = {'name': "test"} - maintenance_dict = {'maintenance_name': "test", 'maintenanceid': "1"} + test_dict = {"name": "test"} + maintenance_dict = {"maintenance_name": "test", "maintenanceid": "1"} mock_maintenance_get.return_value = [] - mock_client.maintenance.create.return_value = [maintenance_dict['maintenanceid']] + mock_client.maintenance.create.return_value = [ + maintenance_dict["maintenanceid"] + ] action.client = mock_client result = action.maintenance_create_or_update(test_dict) - self.assertEqual(result, [maintenance_dict['maintenanceid']]) + self.assertEqual(result, [maintenance_dict["maintenanceid"]]) - @mock.patch('lib.actions.ZabbixBaseAction.maintenance_get') - @mock.patch('lib.actions.ZabbixAPI') - def test_maintenance_create_or_update_create_fail(self, mock_client, mock_maintenance_get): + @mock.patch("lib.actions.ZabbixBaseAction.maintenance_get") + @mock.patch("lib.actions.ZabbixAPI") + def test_maintenance_create_or_update_create_fail( + self, mock_client, mock_maintenance_get + ): action = self.get_action_instance(self.full_config) - test_dict = {'name': "test"} - maintenance_dict = {'maintenance_name': "test", 'maintenanceid': "1"} + test_dict = {"name": "test"} + maintenance_dict = {"maintenance_name": "test", "maintenanceid": "1"} mock_maintenance_get.return_value = [] - mock_client.maintenance.create.return_value = [maintenance_dict['maintenanceid']] - mock_client.maintenance.create.side_effect = ZabbixAPIException('maintenance error') + mock_client.maintenance.create.return_value = [ + maintenance_dict["maintenanceid"] + ] + mock_client.maintenance.create.side_effect = APIRequestError( + "maintenance error" + ) action.client = mock_client - with self.assertRaises(ZabbixAPIException): + with self.assertRaises(APIRequestError): action.maintenance_create_or_update(test_dict) - @mock.patch('lib.actions.ZabbixBaseAction.maintenance_get') - @mock.patch('lib.actions.ZabbixAPI') - def test_maintenance_create_or_update_too_many_maintenance_windows(self, - mock_client, - mock_maintenance_get): + @mock.patch("lib.actions.ZabbixBaseAction.maintenance_get") + @mock.patch("lib.actions.ZabbixAPI") + def test_maintenance_create_or_update_too_many_maintenance_windows( + self, mock_client, mock_maintenance_get + ): action = self.get_action_instance(self.full_config) - test_dict = {'name': "test"} - maintenance_dict = [{'maintenance_name': "test", 'maintenanceid': "1"}, - {'maintenance_name': "test", 'maintenanceid': "2"}] + test_dict = {"name": "test"} + maintenance_dict = [ + {"maintenance_name": "test", "maintenanceid": "1"}, + {"maintenance_name": "test", "maintenanceid": "2"}, + ] mock_maintenance_get.return_value = maintenance_dict - mock_client.maintenance.create.return_value = maintenance_dict[0]['maintenanceid'] + mock_client.maintenance.create.return_value = maintenance_dict[0][ + "maintenanceid" + ] action.client = mock_client with self.assertRaises(ValueError): action.maintenance_create_or_update(test_dict) + + @mock.patch("lib.actions.ZabbixAPI") + def test_host_get_extended(self, mock_client): + action = self.get_action_instance(self.full_config) + mock_client.host.get.return_value = [ + {"hostid": "1", "interfaces": [{"interfaceid": "10"}]} + ] + action.client = mock_client + + result = action.host_get_extended( + "1", "selectInterfaces", ["hostid", "interfaces"] + ) + self.assertEqual( + result, [{"hostid": "1", "interfaces": [{"interfaceid": "10"}]}] + ) + mock_client.host.get.assert_called_with( + hostids="1", selectInterfaces="extend", output=["hostid", "interfaces"] + ) + + @mock.patch("lib.actions.ZabbixAPI") + def test_host_get_extended_api_error(self, mock_client): + action = self.get_action_instance(self.full_config) + mock_client.host.get.side_effect = APIRequestError("host error") + action.client = mock_client + + with self.assertRaises(APIRequestError): + action.host_get_extended("1", "selectInterfaces", ["hostid", "interfaces"]) diff --git a/tests/test_call_api.py b/tests/test_call_api.py index 9b051c7..2eeaac7 100644 --- a/tests/test_call_api.py +++ b/tests/test_call_api.py @@ -8,68 +8,56 @@ class CallAPITest(ZabbixBaseActionTestCase): __test__ = True action_cls = CallAPI - @mock.patch('lib.actions.ZabbixBaseAction.connect') - def test_run_action_without_token(self, mock_conn): + @mock.patch("lib.actions.ZabbixBaseAction.connect") + def test_run_action(self, mock_conn): action = self.get_action_instance(self.full_config) - # This is a mock of calling API 'hoge' action.client = mock.Mock() - action.client.hoge.return_value = 'result' + action.client.hoge.return_value = "result" - # This checks that a method which is specified in the api_method parameter would be called - self.assertEqual(action.run(api_method='hoge', token=None, param='foo'), 'result') + self.assertEqual(action.run(api_method="hoge", param="foo"), "result") - @mock.patch('call_api.ZabbixAPI') - def test_run_action_with_token(self, mock_client): - action = self.get_action_instance(self.full_config) - - # This is a mock of calling API 'hoge' to confirm that - # specified parameters would be passed correctly. - def side_effect(*args, **kwargs): - return (args, kwargs) - - _mock_client = mock.Mock() - _mock_client.hoge.side_effect = side_effect - mock_client.return_value = _mock_client - - # This checks that specified parameter and access token would be set expectedly - result = action.run(api_method='hoge', token='test_token', param='foo') - self.assertEqual(result, ((), {'param': 'foo'})) - self.assertEqual(action.auth, 'test_token') - - @mock.patch('lib.actions.ZabbixBaseAction.connect') + @mock.patch("lib.actions.ZabbixBaseAction.connect") def test_call_hierarchized_method(self, mock_conn): action = self.get_action_instance(self.full_config) - # Initialize client object that only accepts request to 'foo.bar' method. - action.client = mock.Mock(spec=['foo']) - action.client.foo = mock.Mock(spec=['bar']) - action.client.foo.bar.return_value = 'result' + action.client = mock.Mock(spec=["foo"]) + action.client.foo = mock.Mock(spec=["bar"]) + action.client.foo.bar.return_value = "result" - # Send request with proper parameter - self.assertEqual(action.run(api_method='foo.bar', token=None, param='hoge'), 'result') + self.assertEqual(action.run(api_method="foo.bar", param="hoge"), "result") - # Send request with invalid api_method with self.assertRaises(RuntimeError): - action.run(api_method='foo.hoge', token=None, param='hoge') + action.run(api_method="foo.hoge", param="hoge") - @mock.patch('call_api.ZabbixAPI') - def test_run_action_with_empty_parameters(self, mock_client): + @mock.patch("lib.actions.ZabbixBaseAction.connect") + def test_run_action_with_empty_parameters(self, mock_conn): action = self.get_action_instance(self.full_config) - # This is a mock of calling API 'hoge' to confirm that - # params with a value of None (p0) are removed prior to execution - # Should not remove [ '123', False, {}, [], 0 ] def side_effect(*args, **kwargs): return (args, kwargs) - _mock_client = mock.Mock() - _mock_client.hoge.side_effect = side_effect - mock_client.return_value = _mock_client + action.client = mock.Mock() + action.client.hoge.side_effect = side_effect + + result = action.run( + api_method="hoge", + **{"p0": None, "p1": "123", "p2": False, "p3": {}, "p4": [], "p5": 0} + ) + self.assertEqual( + result, ((), {"p1": "123", "p2": False, "p3": {}, "p4": [], "p5": 0}) + ) + action.client.hoge.assert_called_with( + **{"p1": "123", "p2": False, "p3": {}, "p4": [], "p5": 0} + ) + + @mock.patch("lib.actions.ZabbixBaseAction.connect") + def test_run_with_params_list(self, mock_conn): + action = self.get_action_instance(self.full_config) + + action.client = mock.Mock() + action.client.host.delete.return_value = {"hostids": ["10084"]} - result = action.run(api_method='hoge', token='test_token', - **{'p0': None, 'p1': '123', 'p2': False, 'p3': {}, 'p4': [], 'p5': 0}) - self.assertEqual(result, ((), - {'p1': '123', 'p2': False, 'p3': {}, 'p4': [], 'p5': 0})) - _mock_client.hoge.assert_called_with( - **{'p1': '123', 'p2': False, 'p3': {}, 'p4': [], 'p5': 0}) + result = action.run(api_method="host.delete", params_list=["10084", "10085"]) + action.client.host.delete.assert_called_with("10084", "10085") + self.assertEqual(result, {"hostids": ["10084"]}) diff --git a/tests/test_create_host.py b/tests/test_create_host.py index dc1024f..b501bd3 100644 --- a/tests/test_create_host.py +++ b/tests/test_create_host.py @@ -35,13 +35,11 @@ def side_effect_connect(): self._check_data['password_authentication'] = True mock_connect.side_effect = side_effect_connect - # set mock client to AirOne action = self.get_action_instance(self.full_config) action.client = self._mock_client - (result, data) = action.run(name='test-host', groups=[], domains=['example.com']) - self.assertTrue(result) - self.assertEqual(data, {'hostids': ['1234']}) + result = action.run(name='test-host', groups=[], domains=['example.com']) + self.assertEqual(result, {'hostids': ['1234']}) self.assertTrue(self._check_data['password_authentication']) self.assertFalse('is_set_proxy' in self._check_data) self.assertEqual(self._check_data['interfaces'], [{ @@ -61,29 +59,36 @@ def side_effect_connect(): self.assertEqual([x['main'] for x in ifdata if x['dns'] == 'foo.test'], [0]) self.assertEqual([x['main'] for x in ifdata if x['dns'] == 'bar.test'], [1]) - @mock.patch('create_host.ZabbixAPI') @mock.patch('lib.actions.ZabbixBaseAction.connect') - def test_create_host_with_token_and_proxy(self, mock_connect, mock_client): - def side_effect(): - self._check_data['password_authentication'] = True - mock_connect.side_effect = side_effect - - # set mock client to AirOne - mock_client.return_value = self._mock_client + def test_create_host_with_proxy(self, mock_connect): action = self.get_action_instance(self.full_config) + action.client = self._mock_client - (result, data) = action.run(name='test-host', groups=[], domains=['example.com'], - token='token', proxy_host='proxy') - self.assertTrue(result) - self.assertEqual(data, {'hostids': ['1234']}) - self.assertFalse('password_authentication' in self._check_data) + result = action.run(name='test-host', groups=[], domains=['example.com'], + proxy_host='proxy') + self.assertEqual(result, {'hostids': ['1234']}) self.assertTrue(self._check_data['is_set_proxy']) @mock.patch('lib.actions.ZabbixBaseAction.connect') def test_create_host_without_interface_information(self, mock_connect): action = self.get_action_instance(self.full_config) action.client = self._mock_client - (result, data) = action.run(name='test-host', groups=[]) - self.assertFalse(result) - self.assertEqual(data, 'You have to IP address or domain value at least one.') + with self.assertRaises(ValueError): + action.run(name='test-host', groups=[]) + + @mock.patch('lib.actions.ZabbixBaseAction.connect') + def test_create_host_with_ip(self, mock_connect): + action = self.get_action_instance(self.full_config) + action.client = self._mock_client + + result = action.run(name='test-host', groups=[], ipaddrs=['192.168.1.1']) + self.assertEqual(result, {'hostids': ['1234']}) + self.assertEqual(self._check_data['interfaces'], [{ + 'type': 1, + 'main': 1, + 'useip': 1, + 'dns': '', + 'ip': '192.168.1.1', + 'port': '10050' + }]) diff --git a/tests/test_create_or_update_maintenance.py b/tests/test_create_or_update_maintenance.py new file mode 100644 index 0000000..1f6648a --- /dev/null +++ b/tests/test_create_or_update_maintenance.py @@ -0,0 +1,92 @@ +import mock + +from zabbix_base_action_test_case import ZabbixBaseActionTestCase +from create_or_update_maintenance import MaintenanceCreateOrUpdate + +from zabbix_utils.exceptions import ProcessingError +from zabbix_utils.exceptions import APIRequestError + + +class MaintenanceCreateOrUpdateTestCase(ZabbixBaseActionTestCase): + __test__ = True + action_cls = MaintenanceCreateOrUpdate + + @mock.patch("lib.actions.ZabbixBaseAction.connect") + def test_run_connection_error(self, mock_connect): + action = self.get_action_instance(self.full_config) + mock_connect.side_effect = ProcessingError("connection error") + test_dict = { + "hostname": "test", + "time_type": 0, + "maintenance_window_name": "test", + "maintenance_type": 0, + "start_date": "2017-11-14 10:40", + "end_date": "2017-11-14 10:45", + } + + with self.assertRaises(ProcessingError): + action.run(**test_dict) + + @mock.patch("lib.actions.ZabbixBaseAction.connect") + def test_run_host_error(self, mock_connect): + action = self.get_action_instance(self.full_config) + mock_connect.return_vaue = "connect return" + test_dict = { + "hostname": "test", + "time_type": 0, + "maintenance_window_name": "test", + "maintenance_type": 0, + "start_date": "2017-11-14 10:40", + "end_date": "2017-11-14 10:45", + } + action.find_host = mock.MagicMock(side_effect=APIRequestError("host error")) + action.connect = mock_connect + + with self.assertRaises(APIRequestError): + action.run(**test_dict) + + @mock.patch("lib.actions.ZabbixBaseAction.connect") + def test_run_maintenance_error(self, mock_connect): + action = self.get_action_instance(self.full_config) + mock_connect.return_vaue = "connect return" + test_dict = { + "hostname": "test", + "time_type": 0, + "maintenance_window_name": "test", + "maintenance_type": 0, + "start_date": "2017-11-14 10:40", + "end_date": "2017-11-14 10:45", + } + host_dict = {"name": "test", "hostid": "1"} + action.connect = mock_connect + action.find_host = mock.MagicMock(return_value=host_dict["hostid"]) + action.maintenance_create_or_update = mock.MagicMock( + side_effect=APIRequestError("maintenance error") + ) + + with self.assertRaises(APIRequestError): + action.run(**test_dict) + + @mock.patch("lib.actions.ZabbixBaseAction.connect") + def test_run(self, mock_connect): + action = self.get_action_instance(self.full_config) + mock_connect.return_vaue = "connect return" + test_dict = { + "hostname": "test", + "time_type": 0, + "maintenance_window_name": "test", + "maintenance_type": 0, + "start_date": "2017-11-14 10:40", + "end_date": "2017-11-14 10:45", + } + host_dict = {"name": "test", "hostid": "1"} + maintenance_dict = {"maintenanceids": ["1"]} + expected_return = maintenance_dict["maintenanceids"][0] + action.connect = mock_connect + action.find_host = mock.MagicMock(return_value=host_dict["hostid"]) + action.maintenance_create_or_update = mock.MagicMock( + return_value=maintenance_dict + ) + + result = action.run(**test_dict) + self.assertEqual(result, expected_return) diff --git a/tests/test_host_delete.py b/tests/test_delete_host.py similarity index 51% rename from tests/test_host_delete.py rename to tests/test_delete_host.py index 4922033..16b4e6d 100644 --- a/tests/test_host_delete.py +++ b/tests/test_delete_host.py @@ -1,82 +1,80 @@ import mock from zabbix_base_action_test_case import ZabbixBaseActionTestCase -from host_delete import HostDelete +from delete_host import HostDelete -from six.moves.urllib.error import URLError -from pyzabbix.api import ZabbixAPIException +from zabbix_utils.exceptions import ProcessingError +from zabbix_utils.exceptions import APIRequestError class HostDeleteTestCase(ZabbixBaseActionTestCase): __test__ = True action_cls = HostDelete - @mock.patch('lib.actions.ZabbixBaseAction.connect') + @mock.patch("lib.actions.ZabbixBaseAction.connect") def test_run_connection_error(self, mock_connect): action = self.get_action_instance(self.full_config) - mock_connect.side_effect = URLError('connection error') - test_dict = {'host': "test"} - host_dict = {'name': "test", 'hostid': '1'} - mock.MagicMock(return_value=host_dict['hostid']) + mock_connect.side_effect = ProcessingError("connection error") + test_dict = {"hostname": "test"} - with self.assertRaises(URLError): + with self.assertRaises(ProcessingError): action.run(**test_dict) - @mock.patch('lib.actions.ZabbixBaseAction.connect') + @mock.patch("lib.actions.ZabbixBaseAction.connect") def test_run_host_error(self, mock_connect): action = self.get_action_instance(self.full_config) mock_connect.return_vaue = "connect return" - test_dict = {'host': "test"} - host_dict = {'name': "test", 'hostid': '1'} - action.find_host = mock.MagicMock(return_value=host_dict['hostid'], - side_effect=ZabbixAPIException('host error')) + test_dict = {"hostname": "test"} + action.find_host = mock.MagicMock(side_effect=APIRequestError("host error")) action.connect = mock_connect - with self.assertRaises(ZabbixAPIException): + with self.assertRaises(APIRequestError): action.run(**test_dict) - @mock.patch('lib.actions.ZabbixAPI') - @mock.patch('lib.actions.ZabbixBaseAction.connect') + @mock.patch("lib.actions.ZabbixAPI") + @mock.patch("lib.actions.ZabbixBaseAction.connect") def test_run(self, mock_connect, mock_client): action = self.get_action_instance(self.full_config) mock_connect.return_vaue = "connect return" - test_dict = {'host': "test"} - host_dict = {'name': "test", 'hostid': '1'} + test_dict = {"hostname": "test"} + host_dict = {"name": "test", "hostid": "1"} action.connect = mock_connect - action.find_host = mock.MagicMock(return_value=host_dict['hostid']) + action.find_host = mock.MagicMock(return_value=host_dict["hostid"]) mock_client.host.delete.return_value = "delete return" action.client = mock_client result = action.run(**test_dict) - mock_client.host.delete.assert_called_with(host_dict['hostid']) + mock_client.host.delete.assert_called_with(host_dict["hostid"]) self.assertEqual(result, True) - @mock.patch('lib.actions.ZabbixAPI') - @mock.patch('lib.actions.ZabbixBaseAction.connect') + @mock.patch("lib.actions.ZabbixAPI") + @mock.patch("lib.actions.ZabbixBaseAction.connect") def test_run_id(self, mock_connect, mock_client): action = self.get_action_instance(self.full_config) mock_connect.return_vaue = "connect return" - test_dict = {'host_id': "1"} + test_dict = {"host_id": "1"} action.connect = mock_connect mock_client.host.delete.return_value = "delete return" action.client = mock_client result = action.run(**test_dict) - mock_client.host.delete.assert_called_with(test_dict['host_id']) + mock_client.host.delete.assert_called_with(test_dict["host_id"]) self.assertEqual(result, True) - @mock.patch('lib.actions.ZabbixAPI') - @mock.patch('lib.actions.ZabbixBaseAction.connect') + @mock.patch("lib.actions.ZabbixAPI") + @mock.patch("lib.actions.ZabbixBaseAction.connect") def test_run_delete_error(self, mock_connect, mock_client): action = self.get_action_instance(self.full_config) mock_connect.return_vaue = "connect return" - test_dict = {'host': "test"} - host_dict = {'name': "test", 'hostid': '1'} + test_dict = {"hostname": "test"} + host_dict = {"name": "test", "hostid": "1"} action.connect = mock_connect - action.find_host = mock.MagicMock(return_value=host_dict['hostid']) - mock_client.host.delete.side_effect = ZabbixAPIException('host error') - mock_client.host.delete.return_value = "delete return" + action.find_host = mock.MagicMock(return_value=host_dict["hostid"]) + mock_client.host.delete.side_effect = APIRequestError("host error") action.client = mock_client - with self.assertRaises(ZabbixAPIException): + with self.assertRaises(APIRequestError): + action.run(**test_dict) + + with self.assertRaises(APIRequestError): action.run(**test_dict) diff --git a/tests/test_maintenance_delete.py b/tests/test_delete_maintenance.py similarity index 91% rename from tests/test_maintenance_delete.py rename to tests/test_delete_maintenance.py index ad12d3b..379c5aa 100644 --- a/tests/test_maintenance_delete.py +++ b/tests/test_delete_maintenance.py @@ -1,10 +1,10 @@ import mock from zabbix_base_action_test_case import ZabbixBaseActionTestCase -from maintenance_delete import MaintenanceDelete +from delete_maintenance import MaintenanceDelete -from six.moves.urllib.error import URLError -from pyzabbix.api import ZabbixAPIException +from zabbix_utils.exceptions import ProcessingError +from zabbix_utils.exceptions import APIRequestError class MaintenanceDeleteTestCase(ZabbixBaseActionTestCase): @@ -14,10 +14,10 @@ class MaintenanceDeleteTestCase(ZabbixBaseActionTestCase): @mock.patch('lib.actions.ZabbixBaseAction.connect') def test_run_connection_error(self, mock_connect): action = self.get_action_instance(self.full_config) - mock_connect.side_effect = URLError('connection error') + mock_connect.side_effect = ProcessingError('connection error') test_dict = {'maintenance_window_name': None, 'maintenance_id': '1'} - with self.assertRaises(URLError): + with self.assertRaises(ProcessingError): action.run(**test_dict) @mock.patch('lib.actions.ZabbixAPI') @@ -100,9 +100,9 @@ def test_run_delete_error(self, mock_connect, mock_client): mock_connect.return_vaue = "connect return" test_dict = {'maintenance_window_name': None, 'maintenance_id': '1'} action.connect = mock_connect - mock_client.maintenance.delete.side_effect = ZabbixAPIException('maintenance error') + mock_client.maintenance.delete.side_effect = APIRequestError('maintenance error') mock_client.maintenance.delete.return_value = "delete return" action.client = mock_client - with self.assertRaises(ZabbixAPIException): + with self.assertRaises(APIRequestError): action.run(**test_dict) diff --git a/tests/test_find_object.py b/tests/test_find_object.py new file mode 100644 index 0000000..2df7517 --- /dev/null +++ b/tests/test_find_object.py @@ -0,0 +1,112 @@ +import mock + +from zabbix_base_action_test_case import ZabbixBaseActionTestCase +from find_object import FindObject + +from zabbix_utils.exceptions import APIRequestError + + +class FindObjectTestCase(ZabbixBaseActionTestCase): + __test__ = True + action_cls = FindObject + + @mock.patch('lib.actions.ZabbixBaseAction.connect') + def test_find_single_host(self, mock_connect): + action = self.get_action_instance(self.full_config) + action.client = mock.Mock() + action.client.host.get.return_value = [{'hostid': '10084'}] + + result = action.run( + object_type='host', filter_field='host', + id_field='hostid', name='myhost') + self.assertEqual(result, '10084') + action.client.host.get.assert_called_with(filter={'host': 'myhost'}) + + @mock.patch('lib.actions.ZabbixBaseAction.connect') + def test_find_host_not_found(self, mock_connect): + action = self.get_action_instance(self.full_config) + action.client = mock.Mock() + action.client.host.get.return_value = [] + + with self.assertRaises(ValueError): + action.run( + object_type='host', filter_field='host', + id_field='hostid', name='nonexistent') + + @mock.patch('lib.actions.ZabbixBaseAction.connect') + def test_find_host_multiple_found(self, mock_connect): + action = self.get_action_instance(self.full_config) + action.client = mock.Mock() + action.client.host.get.return_value = [ + {'hostid': '1'}, {'hostid': '2'}] + + with self.assertRaises(ValueError): + action.run( + object_type='host', filter_field='host', + id_field='hostid', name='duplicate') + + @mock.patch('lib.actions.ZabbixBaseAction.connect') + def test_find_multiple_hosts(self, mock_connect): + action = self.get_action_instance(self.full_config) + action.client = mock.Mock() + action.client.host.get.return_value = [ + {'hostid': '1'}, {'hostid': '2'}] + + result = action.run( + object_type='host', filter_field='host', + id_field='hostid', name=['h1', 'h2'], allow_multiple=True) + self.assertEqual(result, ['1', '2']) + + @mock.patch('lib.actions.ZabbixBaseAction.connect') + def test_find_multiple_empty(self, mock_connect): + action = self.get_action_instance(self.full_config) + action.client = mock.Mock() + action.client.host.get.return_value = [] + + result = action.run( + object_type='host', filter_field='host', + id_field='hostid', name=['nonexistent'], allow_multiple=True) + self.assertEqual(result, []) + + @mock.patch('lib.actions.ZabbixBaseAction.connect') + def test_find_hostgroup(self, mock_connect): + action = self.get_action_instance(self.full_config) + action.client = mock.Mock() + action.client.hostgroup.get.return_value = [{'groupid': '5'}] + + result = action.run( + object_type='hostgroup', filter_field='name', + id_field='groupid', name='Linux servers') + self.assertEqual(result, '5') + + @mock.patch('lib.actions.ZabbixBaseAction.connect') + def test_find_template(self, mock_connect): + action = self.get_action_instance(self.full_config) + action.client = mock.Mock() + action.client.template.get.return_value = [{'templateid': '100'}] + + result = action.run( + object_type='template', filter_field='host', + id_field='templateid', name='Template OS Linux') + self.assertEqual(result, '100') + + @mock.patch('lib.actions.ZabbixBaseAction.connect') + def test_find_invalid_object_type(self, mock_connect): + action = self.get_action_instance(self.full_config) + action.client = mock.Mock(spec=[]) + + with self.assertRaises(ValueError): + action.run( + object_type='invalid', filter_field='name', + id_field='id', name='test') + + @mock.patch('lib.actions.ZabbixBaseAction.connect') + def test_find_api_error(self, mock_connect): + action = self.get_action_instance(self.full_config) + action.client = mock.Mock() + action.client.host.get.side_effect = APIRequestError('API error') + + with self.assertRaises(APIRequestError): + action.run( + object_type='host', filter_field='host', + id_field='hostid', name='test') diff --git a/tests/test_get_api_version.py b/tests/test_get_api_version.py new file mode 100644 index 0000000..479d4fc --- /dev/null +++ b/tests/test_get_api_version.py @@ -0,0 +1,29 @@ +import mock + +from zabbix_base_action_test_case import ZabbixBaseActionTestCase +from get_api_version import GetApiVersion + +from zabbix_utils.exceptions import ProcessingError + + +class GetApiVersionTestCase(ZabbixBaseActionTestCase): + __test__ = True + action_cls = GetApiVersion + + @mock.patch('lib.actions.ZabbixBaseAction.connect') + def test_get_api_version(self, mock_connect): + action = self.get_action_instance(self.full_config) + action.client = mock.Mock() + action.client.api_version.return_value = '6.0.46' + + result = action.run() + self.assertEqual(result, '6.0.46') + action.client.api_version.assert_called_once() + + @mock.patch('lib.actions.ZabbixBaseAction.connect') + def test_get_api_version_connection_error(self, mock_connect): + action = self.get_action_instance(self.full_config) + mock_connect.side_effect = ProcessingError('connection error') + + with self.assertRaises(ProcessingError): + action.run() diff --git a/tests/test_host_get_id.py b/tests/test_host_get_id.py deleted file mode 100644 index cd28a65..0000000 --- a/tests/test_host_get_id.py +++ /dev/null @@ -1,47 +0,0 @@ -import mock - -from zabbix_base_action_test_case import ZabbixBaseActionTestCase -from host_get_id import HostGetID - -from six.moves.urllib.error import URLError -from pyzabbix.api import ZabbixAPIException - - -class HostGetIDTestCase(ZabbixBaseActionTestCase): - __test__ = True - action_cls = HostGetID - - @mock.patch('lib.actions.ZabbixBaseAction.connect') - def test_run_connection_error(self, mock_connect): - action = self.get_action_instance(self.full_config) - mock_connect.side_effect = URLError('connection error') - test_dict = {'host': "test"} - host_dict = {'name': "test", 'hostid': '1'} - action.find_host = mock.MagicMock(return_value=host_dict['hostid']) - - with self.assertRaises(URLError): - action.run(**test_dict) - - @mock.patch('lib.actions.ZabbixBaseAction.connect') - def test_run_host_error(self, mock_connect): - action = self.get_action_instance(self.full_config) - mock_connect.return_vaue = "connect return" - test_dict = {'host': "test"} - host_dict = {'name': "test", 'hostid': '1'} - action.find_host = mock.MagicMock(return_value=host_dict['hostid'], - side_effect=ZabbixAPIException('host error')) - action.connect = mock_connect - with self.assertRaises(ZabbixAPIException): - action.run(**test_dict) - - @mock.patch('lib.actions.ZabbixBaseAction.connect') - def test_run(self, mock_connect): - action = self.get_action_instance(self.full_config) - mock_connect.return_vaue = "connect return" - test_dict = {'host': "test"} - host_dict = {'name': "test", 'hostid': '1'} - action.connect = mock_connect - action.find_host = mock.MagicMock(return_value=host_dict['hostid']) - - result = action.run(**test_dict) - self.assertEqual(result, host_dict['hostid']) diff --git a/tests/test_host_get_interfaces.py b/tests/test_host_get_interfaces.py deleted file mode 100644 index e90ce94..0000000 --- a/tests/test_host_get_interfaces.py +++ /dev/null @@ -1,99 +0,0 @@ -import mock - -from zabbix_base_action_test_case import ZabbixBaseActionTestCase -from host_get_interfaces import HostGetInterfaces - -from six.moves.urllib.error import URLError - - -class HostGetInterfacesTestCase(ZabbixBaseActionTestCase): - __test__ = True - action_cls = HostGetInterfaces - - @mock.patch('lib.actions.ZabbixBaseAction.connect') - def test_run_connection_error(self, mock_connect): - action = self.get_action_instance(self.full_config) - mock_connect.side_effect = URLError('connection error') - test_dict = {'host_ids': ["12345"]} - - with self.assertRaises(URLError): - action.run(**test_dict) - - @mock.patch('lib.actions.ZabbixAPI') - @mock.patch('lib.actions.ZabbixBaseAction.connect') - def test_run_host_error(self, mock_connect, mock_client): - action = self.get_action_instance(self.full_config) - mock_connect.return_vaue = "connect return" - test_dict = {'host_ids': ["12345"]} - interfaces_list = [{'hostid': "12345", 'interfaces': { - 'name': "test"}}] - action.connect = mock_connect - mock_client.host.get.return_value = interfaces_list - action.client = mock_client - - result = action.run(**test_dict) - mock_client.host.get.assert_called_with( - hostids=test_dict['host_ids'], - selectInterfaces='extend', - output=['hostid', 'interfaces']) - self.assertEqual(result, interfaces_list) - - @mock.patch('lib.actions.ZabbixAPI') - @mock.patch('lib.actions.ZabbixBaseAction.connect') - def test_run_none(self, mock_connect, mock_client): - action = self.get_action_instance(self.full_config) - mock_connect.return_vaue = "connect return" - test_dict = {'host_ids': ["12345"]} - interfaces_list = [] - action.connect = mock_connect - mock_client.host.get.return_value = interfaces_list - action.client = mock_client - - result = action.run(**test_dict) - self.assertEqual(result, []) - - @mock.patch('lib.actions.ZabbixAPI') - @mock.patch('lib.actions.ZabbixBaseAction.connect') - def test_run_single(self, mock_connect, mock_client): - action = self.get_action_instance(self.full_config) - mock_connect.return_vaue = "connect return" - test_dict = {'host_ids': ["12345"]} - interfaces_list = [{'hostid': "12345", 'interfaces': { - 'name': "test"}}] - action.connect = mock_connect - mock_client.host.get.return_value = interfaces_list - action.client = mock_client - expected_return = [{'hostid': interfaces_list[0][ - 'hostid'], 'interfaces': interfaces_list[0]['interfaces']}] - - result = action.run(**test_dict) - mock_client.host.get.assert_called_with( - hostids=test_dict['host_ids'], - selectInterfaces='extend', - output=['hostid', 'interfaces']) - self.assertEqual(result, expected_return) - - @mock.patch('lib.actions.ZabbixAPI') - @mock.patch('lib.actions.ZabbixBaseAction.connect') - def test_run_multiple(self, mock_connect, mock_client): - action = self.get_action_instance(self.full_config) - mock_connect.return_vaue = "connect return" - test_dict = {'host_ids': ["12345", "98765"]} - interfaces_list = [{'hostid': "12345", 'interfaces': - {'name': "test"}}, - {'hostid': "98765", 'interfaces': - {'name': "test2"}}] - action.connect = mock_connect - mock_client.host.get.return_value = interfaces_list - action.client = mock_client - expected_return = [{'hostid': interfaces_list[0]['hostid'], - 'interfaces': interfaces_list[0]['interfaces']}, - {'hostid': interfaces_list[1]['hostid'], - 'interfaces': interfaces_list[1]['interfaces']}] - - result = action.run(**test_dict) - mock_client.host.get.assert_called_with( - hostids=test_dict['host_ids'], - selectInterfaces='extend', - output=['hostid', 'interfaces']) - self.assertEqual(result, expected_return) diff --git a/tests/test_host_get_inventory.py b/tests/test_host_get_inventory.py deleted file mode 100644 index f808ed4..0000000 --- a/tests/test_host_get_inventory.py +++ /dev/null @@ -1,99 +0,0 @@ -import mock - -from zabbix_base_action_test_case import ZabbixBaseActionTestCase -from host_get_inventory import HostGetInventory - -from six.moves.urllib.error import URLError - - -class HostGetInventoryTestCase(ZabbixBaseActionTestCase): - __test__ = True - action_cls = HostGetInventory - - @mock.patch('lib.actions.ZabbixBaseAction.connect') - def test_run_connection_error(self, mock_connect): - action = self.get_action_instance(self.full_config) - mock_connect.side_effect = URLError('connection error') - test_dict = {'host_ids': ["12345"]} - - with self.assertRaises(URLError): - action.run(**test_dict) - - @mock.patch('lib.actions.ZabbixAPI') - @mock.patch('lib.actions.ZabbixBaseAction.connect') - def test_run_host_error(self, mock_connect, mock_client): - action = self.get_action_instance(self.full_config) - mock_connect.return_vaue = "connect return" - test_dict = {'host_ids': ["12345"]} - inventory_list = [{'hostid': "12345", 'inventory': { - 'serialno_a': "abcd1234", 'name': "test"}}] - action.connect = mock_connect - mock_client.host.get.return_value = inventory_list - action.client = mock_client - - result = action.run(**test_dict) - mock_client.host.get.assert_called_with( - hostids=test_dict['host_ids'], - selectInventory='extend', - output=['hostid', 'inventory']) - self.assertEqual(result, inventory_list) - - @mock.patch('lib.actions.ZabbixAPI') - @mock.patch('lib.actions.ZabbixBaseAction.connect') - def test_run_none(self, mock_connect, mock_client): - action = self.get_action_instance(self.full_config) - mock_connect.return_vaue = "connect return" - test_dict = {'host_ids': ["12345"]} - inventory_list = [] - action.connect = mock_connect - mock_client.host.get.return_value = inventory_list - action.client = mock_client - - result = action.run(**test_dict) - self.assertEqual(result, []) - - @mock.patch('lib.actions.ZabbixAPI') - @mock.patch('lib.actions.ZabbixBaseAction.connect') - def test_run_single(self, mock_connect, mock_client): - action = self.get_action_instance(self.full_config) - mock_connect.return_vaue = "connect return" - test_dict = {'host_ids': ["12345"]} - inventory_list = [{'hostid': "12345", 'inventory': { - 'serialno_a': "abcd1234", 'name': "test"}}] - action.connect = mock_connect - mock_client.host.get.return_value = inventory_list - action.client = mock_client - expected_return = [{'hostid': inventory_list[0][ - 'hostid'], 'inventory': inventory_list[0]['inventory']}] - - result = action.run(**test_dict) - mock_client.host.get.assert_called_with( - hostids=test_dict['host_ids'], - selectInventory='extend', - output=['hostid', 'inventory']) - self.assertEqual(result, expected_return) - - @mock.patch('lib.actions.ZabbixAPI') - @mock.patch('lib.actions.ZabbixBaseAction.connect') - def test_run_multiple(self, mock_connect, mock_client): - action = self.get_action_instance(self.full_config) - mock_connect.return_vaue = "connect return" - test_dict = {'host_ids': ["12345", "98765"]} - inventory_list = [{'hostid': "12345", 'inventory': - {'serialno_a': "abcd1234", 'name': "test"}}, - {'hostid': "98765", 'inventory': - {'serialno_a': "efgh5678", 'name': "test2"}}] - action.connect = mock_connect - mock_client.host.get.return_value = inventory_list - action.client = mock_client - expected_return = [{'hostid': inventory_list[0]['hostid'], - 'inventory': inventory_list[0]['inventory']}, - {'hostid': inventory_list[1]['hostid'], - 'inventory': inventory_list[1]['inventory']}] - - result = action.run(**test_dict) - mock_client.host.get.assert_called_with( - hostids=test_dict['host_ids'], - selectInventory='extend', - output=['hostid', 'inventory']) - self.assertEqual(result, expected_return) diff --git a/tests/test_host_get_multiple_ids.py b/tests/test_host_get_multiple_ids.py deleted file mode 100644 index 2aad2e3..0000000 --- a/tests/test_host_get_multiple_ids.py +++ /dev/null @@ -1,74 +0,0 @@ -import mock - -from zabbix_base_action_test_case import ZabbixBaseActionTestCase -from host_get_multiple_ids import ZabbixGetMultipleHostID - -from six.moves.urllib.error import URLError -from pyzabbix.api import ZabbixAPIException - - -class GetMultipleHostIDTestCase(ZabbixBaseActionTestCase): - __test__ = True - action_cls = ZabbixGetMultipleHostID - - @mock.patch('lib.actions.ZabbixBaseAction.connect') - def test_run_connection_error(self, mock_connect): - action = self.get_action_instance(self.full_config) - mock_connect.side_effect = URLError('connection error') - test_dict = {'host': "test"} - host_dict = {'name': "test", 'hostid': '1'} - action.find_host = mock.MagicMock(return_value=host_dict['hostid']) - - with self.assertRaises(URLError): - action.run(**test_dict) - - @mock.patch('lib.actions.ZabbixBaseAction.connect') - def test_run_host_error(self, mock_connect): - action = self.get_action_instance(self.full_config) - mock_connect.return_vaue = "connect return" - test_dict = {'host': "test"} - host_dict = {'name': "test", 'hostid': '1'} - action.find_hosts = mock.MagicMock(return_value=host_dict['hostid'], - side_effect=ZabbixAPIException('host error')) - action.connect = mock_connect - with self.assertRaises(ZabbixAPIException): - action.run(**test_dict) - - @mock.patch('lib.actions.ZabbixBaseAction.connect') - def test_run_none(self, mock_connect): - action = self.get_action_instance(self.full_config) - mock_connect.return_vaue = "connect return" - test_dict = {'host': "test"} - action.connect = mock_connect - action.find_hosts = mock.MagicMock(return_value=[]) - expected_return = {'host_ids': []} - - result = action.run(**test_dict) - self.assertEqual(result, expected_return) - - @mock.patch('lib.actions.ZabbixBaseAction.connect') - def test_run_single(self, mock_connect): - action = self.get_action_instance(self.full_config) - mock_connect.return_vaue = "connect return" - test_dict = {'host': "test"} - host_dict = {'name': "test", 'hostid': '1'} - action.connect = mock_connect - action.find_hosts = mock.MagicMock(return_value=[host_dict['hostid']]) - expected_return = {'host_ids': [host_dict['hostid']]} - - result = action.run(**test_dict) - self.assertEqual(result, expected_return) - - @mock.patch('lib.actions.ZabbixBaseAction.connect') - def test_run_multiple(self, mock_connect): - action = self.get_action_instance(self.full_config) - mock_connect.return_vaue = "connect return" - test_dict = {'host': "test"} - host_dict = [{'name': "test", 'hostid': '1'}, {'name': "test", 'hostid': '2'}] - action.connect = mock_connect - action.find_hosts = mock.MagicMock(return_value=[host_dict[0]['hostid'], - host_dict[1]['hostid']]) - expected_return = {'host_ids': [host_dict[0]['hostid'], host_dict[1]['hostid']]} - - result = action.run(**test_dict) - self.assertEqual(result, expected_return) diff --git a/tests/test_host_get_status.py b/tests/test_host_get_status.py deleted file mode 100644 index cb9b17b..0000000 --- a/tests/test_host_get_status.py +++ /dev/null @@ -1,49 +0,0 @@ -import mock - -from zabbix_base_action_test_case import ZabbixBaseActionTestCase -from host_get_status import HostGetStatus - -from six.moves.urllib.error import URLError -from pyzabbix.api import ZabbixAPIException - - -class HostGetStatusTestCase(ZabbixBaseActionTestCase): - __test__ = True - action_cls = HostGetStatus - - @mock.patch('lib.actions.ZabbixBaseAction.connect') - def test_run_connection_error(self, mock_connect): - action = self.get_action_instance(self.full_config) - mock_connect.side_effect = URLError('connection error') - test_dict = {'host': "test"} - host_dict = {'name': "test", 'hostid': '1'} - action.find_host = mock.MagicMock(return_value=host_dict['hostid']) - - with self.assertRaises(URLError): - action.run(**test_dict) - - @mock.patch('lib.actions.ZabbixBaseAction.connect') - def test_run_host_error(self, mock_connect): - action = self.get_action_instance(self.full_config) - mock_connect.return_vaue = "connect return" - test_dict = {'host': "test"} - host_dict = {'name': "test", 'hostid': '1'} - action.find_host = mock.MagicMock(return_value=host_dict['hostid'], - side_effect=ZabbixAPIException('host error')) - action.connect = mock_connect - with self.assertRaises(ZabbixAPIException): - action.run(**test_dict) - - @mock.patch('lib.actions.ZabbixBaseAction.connect') - @mock.patch('lib.actions.ZabbixBaseAction.find_host') - def test_run(self, mock_connect, mock_find_host): - action = self.get_action_instance(self.full_config) - mock_connect.return_vaue = "connect return" - test_dict = {'host': "test"} - host_dict = {'name': "test", 'hostid': '1', 'status': '0'} - action.connect = mock_connect - action.find_host = mock_find_host - action.zabbix_host = host_dict - - result = action.run(**test_dict) - self.assertEqual(result, host_dict['status']) diff --git a/tests/test_host_status.py b/tests/test_host_status.py new file mode 100644 index 0000000..18f2a6a --- /dev/null +++ b/tests/test_host_status.py @@ -0,0 +1,63 @@ +import mock + +from zabbix_base_action_test_case import ZabbixBaseActionTestCase +from host_status import HostStatus + +from zabbix_utils.exceptions import ProcessingError +from zabbix_utils.exceptions import APIRequestError + + +class HostStatusTestCase(ZabbixBaseActionTestCase): + __test__ = True + action_cls = HostStatus + + @mock.patch('lib.actions.ZabbixBaseAction.connect') + def test_get_status(self, mock_connect): + action = self.get_action_instance(self.full_config) + action.connect = mock_connect + action.find_host = mock.MagicMock(return_value='1') + action.zabbix_host = {'hostid': '1', 'status': '0'} + + result = action.run(hostname='test') + self.assertEqual(result, '0') + + @mock.patch('lib.actions.ZabbixBaseAction.connect') + def test_update_status(self, mock_connect): + action = self.get_action_instance(self.full_config) + action.connect = mock_connect + action.find_host = mock.MagicMock(return_value='1') + action.client = mock.Mock() + action.client.host.update.return_value = {'hostids': ['1']} + + result = action.run(hostname='test', status=1) + self.assertEqual(result, True) + action.client.host.update.assert_called_with(hostid='1', status=1) + + @mock.patch('lib.actions.ZabbixBaseAction.connect') + def test_connection_error(self, mock_connect): + action = self.get_action_instance(self.full_config) + mock_connect.side_effect = ProcessingError('connection error') + + with self.assertRaises(ProcessingError): + action.run(hostname='test') + + @mock.patch('lib.actions.ZabbixBaseAction.connect') + def test_host_not_found(self, mock_connect): + action = self.get_action_instance(self.full_config) + action.connect = mock_connect + action.find_host = mock.MagicMock( + side_effect=ValueError('Could not find host')) + + with self.assertRaises(ValueError): + action.run(hostname='nonexistent') + + @mock.patch('lib.actions.ZabbixBaseAction.connect') + def test_update_status_api_error(self, mock_connect): + action = self.get_action_instance(self.full_config) + action.connect = mock_connect + action.find_host = mock.MagicMock(return_value='1') + action.client = mock.Mock() + action.client.host.update.side_effect = APIRequestError('update failed') + + with self.assertRaises(APIRequestError): + action.run(hostname='test', status=1) diff --git a/tests/test_host_update_status.py b/tests/test_host_update_status.py deleted file mode 100644 index 991703e..0000000 --- a/tests/test_host_update_status.py +++ /dev/null @@ -1,69 +0,0 @@ -import mock - -from zabbix_base_action_test_case import ZabbixBaseActionTestCase -from host_update_status import HostUpdateStatus - -from six.moves.urllib.error import URLError -from pyzabbix.api import ZabbixAPIException - - -class HostUpdateStatusTestCase(ZabbixBaseActionTestCase): - __test__ = True - action_cls = HostUpdateStatus - - @mock.patch('lib.actions.ZabbixBaseAction.connect') - def test_run_connection_error(self, mock_connect): - action = self.get_action_instance(self.full_config) - mock_connect.side_effect = URLError('connection error') - test_dict = {'host': "test", 'status': 1} - host_dict = {'name': "test", 'hostid': '1'} - mock.MagicMock(return_value=host_dict['hostid']) - - with self.assertRaises(URLError): - action.run(**test_dict) - - @mock.patch('lib.actions.ZabbixBaseAction.connect') - def test_run_host_error(self, mock_connect): - action = self.get_action_instance(self.full_config) - mock_connect.return_vaue = "connect return" - test_dict = {'host': "test", 'status': 1} - host_dict = {'name': "test", 'hostid': '1'} - action.find_host = mock.MagicMock(return_value=host_dict['hostid'], - side_effect=ZabbixAPIException('host error')) - action.connect = mock_connect - - with self.assertRaises(ZabbixAPIException): - action.run(**test_dict) - - @mock.patch('lib.actions.ZabbixAPI') - @mock.patch('lib.actions.ZabbixBaseAction.connect') - def test_run(self, mock_connect, mock_client): - action = self.get_action_instance(self.full_config) - mock_connect.return_vaue = "connect return" - test_dict = {'host': "test", 'status': 1} - host_dict = {'name': "test", 'hostid': '1'} - action.connect = mock_connect - action.find_host = mock.MagicMock(return_value=host_dict['hostid']) - mock_client.host.update.return_value = "update return" - action.client = mock_client - - result = action.run(**test_dict) - mock_client.host.update.assert_called_with(hostid=host_dict['hostid'], - status=test_dict['status']) - self.assertEqual(result, True) - - @mock.patch('lib.actions.ZabbixAPI') - @mock.patch('lib.actions.ZabbixBaseAction.connect') - def test_run_update_error(self, mock_connect, mock_client): - action = self.get_action_instance(self.full_config) - mock_connect.return_vaue = "connect return" - test_dict = {'host': "test", 'status': 1} - host_dict = {'name': "test", 'hostid': '1'} - action.connect = mock_connect - action.find_host = mock.MagicMock(return_value=host_dict['hostid']) - mock_client.host.update.side_effect = ZabbixAPIException('host error') - mock_client.host.update.return_value = "update return" - action.client = mock_client - - with self.assertRaises(ZabbixAPIException): - action.run(**test_dict) diff --git a/tests/test_maintenance_create_or_update.py b/tests/test_maintenance_create_or_update.py deleted file mode 100644 index 6df64dc..0000000 --- a/tests/test_maintenance_create_or_update.py +++ /dev/null @@ -1,86 +0,0 @@ -import mock - -from zabbix_base_action_test_case import ZabbixBaseActionTestCase -from maintenance_create_or_update import MaintenanceCreateOrUpdate - -from six.moves.urllib.error import URLError -from pyzabbix.api import ZabbixAPIException - - -class MaintenanceCreateOrUpdateTestCase(ZabbixBaseActionTestCase): - __test__ = True - action_cls = MaintenanceCreateOrUpdate - - @mock.patch('lib.actions.ZabbixBaseAction.connect') - def test_run_connection_error(self, mock_connect): - action = self.get_action_instance(self.full_config) - mock_connect.side_effect = URLError('connection error') - test_dict = {'host': "test", - 'time_type': 0, - 'maintenance_window_name': "test", - 'maintenance_type': 0, - 'start_date': "2017-11-14 10:40", - 'end_date': "2017-11-14 10:45"} - host_dict = {'name': "test", 'hostid': '1'} - mock.MagicMock(return_value=host_dict['hostid']) - - with self.assertRaises(URLError): - action.run(**test_dict) - - @mock.patch('lib.actions.ZabbixBaseAction.connect') - def test_run_host_error(self, mock_connect): - action = self.get_action_instance(self.full_config) - mock_connect.return_vaue = "connect return" - test_dict = {'host': "test", - 'time_type': 0, - 'maintenance_window_name': "test", - 'maintenance_type': 0, - 'start_date': "2017-11-14 10:40", - 'end_date': "2017-11-14 10:45"} - host_dict = {'name': "test", 'hostid': '1'} - action.find_host = mock.MagicMock(return_value=host_dict['hostid'], - side_effect=ZabbixAPIException('host error')) - action.connect = mock_connect - - with self.assertRaises(ZabbixAPIException): - action.run(**test_dict) - - @mock.patch('lib.actions.ZabbixBaseAction.connect') - def test_run_maintenance_error(self, mock_connect): - action = self.get_action_instance(self.full_config) - mock_connect.return_vaue = "connect return" - test_dict = {'host': "test", - 'time_type': 0, - 'maintenance_window_name': "test", - 'maintenance_type': 0, - 'start_date': "2017-11-14 10:40", - 'end_date': "2017-11-14 10:45"} - host_dict = {'name': "test", 'hostid': '1'} - maintenance_dict = {'maintenanceids': ['1']} - action.connect = mock_connect - action.find_host = mock.MagicMock(return_value=host_dict['hostid']) - action.maintenance_create_or_update = mock.MagicMock(return_value=maintenance_dict, - side_effect=ZabbixAPIException('maintenance error')) - - with self.assertRaises(ZabbixAPIException): - action.run(**test_dict) - - @mock.patch('lib.actions.ZabbixBaseAction.connect') - def test_run(self, mock_connect): - action = self.get_action_instance(self.full_config) - mock_connect.return_vaue = "connect return" - test_dict = {'host': "test", - 'time_type': 0, - 'maintenance_window_name': "test", - 'maintenance_type': 0, - 'start_date': "2017-11-14 10:40", - 'end_date': "2017-11-14 10:45"} - host_dict = {'name': "test", 'hostid': '1'} - maintenance_dict = {'maintenanceids': ['1']} - expected_return = {'maintenance_id': maintenance_dict['maintenanceids'][0]} - action.connect = mock_connect - action.find_host = mock.MagicMock(return_value=host_dict['hostid']) - action.maintenance_create_or_update = mock.MagicMock(return_value=maintenance_dict) - - result = action.run(**test_dict) - self.assertEqual(result, expected_return) diff --git a/tests/test_tool_register_st2_config_to_zabbix.py b/tests/test_tool_register_st2_config_to_zabbix.py deleted file mode 100644 index 82ca5ac..0000000 --- a/tests/test_tool_register_st2_config_to_zabbix.py +++ /dev/null @@ -1,165 +0,0 @@ -import os -import re -import sys -import mock - -from six import StringIO -from unittest import TestCase - -from six.moves.urllib.error import URLError -from pyzabbix.api import ZabbixAPIException - -sys.path.append(os.path.dirname(os.path.realpath(__file__)) + '/../tools/') -import register_st2_config_to_zabbix - - -class TestRegisterMediaType(TestCase): - def setUp(self): - sys.argv = ['register_st2_config_to_zabbix.py'] - self.io_stdout = StringIO() - self.io_stderr = StringIO() - sys.stdout = self.io_stdout - sys.stderr = self.io_stderr - - def tearDown(self): - sys.stdout = sys.__stdout__ - sys.stderr = sys.__stderr__ - - def test_register_mediatype_without_argument(self): - with self.assertRaises(SystemExit): - register_st2_config_to_zabbix.main() - - self.assertTrue(re.match(r".*Zabbix Server URL is not given", - self.io_stderr.getvalue(), - flags=(re.MULTILINE | re.DOTALL))) - - @mock.patch('register_st2_config_to_zabbix.ZabbixAPI') - def test_register_mediatype_to_invalid_zabbix_server(self, mock_client): - sys.argv += ['-z', 'http://invalid-zabbix-host'] - - # make an exception that means failure to connect server. - mock_client.side_effect = URLError('connection error') - - with self.assertRaises(SystemExit): - register_st2_config_to_zabbix.main() - self.assertTrue(re.match(r"Failed to connect Zabbix server", self.io_stderr.getvalue())) - - @mock.patch('register_st2_config_to_zabbix.ZabbixAPI') - def test_register_mediatype_with_invalid_authentication(self, mock_client): - sys.argv += ['-z', 'http://invalid-zabbix-host', '-u', 'user', '-p', 'passwd'] - - # make an exception that means failure to authenticate with Zabbix-server. - mock_client.side_effect = ZabbixAPIException('auth error') - - with self.assertRaises(SystemExit): - register_st2_config_to_zabbix.main() - self.assertTrue(re.match(r"Failed to authenticate Zabbix", self.io_stderr.getvalue())) - - @mock.patch('register_st2_config_to_zabbix.ZabbixAPI') - def test_register_duplicate_mediatype(self, mock_client): - sys.argv += ['-z', 'http://zabbix-host'] - self.is_registered_media = False - self.is_registered_action = False - self.is_called_delete = False - - def side_effect_media(*args, **kwargs): - self.is_registered_media = True - - def side_effect_action(*args, **kwargs): - self.is_registered_action = True - - def side_effect_delete(*args, **kwargs): - self.is_called_delete = True - - # make mock to get target mediatype - mock_obj = mock.Mock() - mock_obj.apiinfo.version.return_value = '3.x' - mock_obj.mediatype.get.return_value = [{ - 'type': register_st2_config_to_zabbix.SCRIPT_MEDIA_TYPE, - 'exec_path': register_st2_config_to_zabbix.ST2_DISPATCHER_SCRIPT, - 'mediatypeid': '1', - }] - - # make mock to return no action - mock_obj.action.get.return_value = [] - mock_obj.mediatype.update.return_value = {'mediatypeids': ['1']} - mock_client.return_value = mock_obj - - mock_obj.user.addmedia.side_effect = side_effect_media - mock_obj.action.create.side_effect = side_effect_action - mock_obj.action.delete.side_effect = side_effect_delete - - register_st2_config_to_zabbix.main() - self.assertTrue(re.match(r"Success to register the configurations", - self.io_stdout.getvalue())) - self.assertTrue(self.is_registered_media) - self.assertTrue(self.is_registered_action) - self.assertFalse(self.is_called_delete) - - # make mock to return action which is alredy registered - mock_obj.action.get.return_value = [{ - 'name': register_st2_config_to_zabbix.ST2_ACTION_NAME, - 'actionid': 1, - }] - - register_st2_config_to_zabbix.main() - self.assertTrue(re.match(r"Success to register the configurations", - self.io_stdout.getvalue())) - self.assertTrue(self.is_registered_media) - self.assertTrue(self.is_registered_action) - self.assertTrue(self.is_called_delete) - - @mock.patch('register_st2_config_to_zabbix.ZabbixAPI') - def test_register_mediatype_successfully(self, mock_client): - sys.argv += ['-z', 'http://zabbix-host'] - self.is_registered_media = False - self.is_registered_action = False - - def side_effect_media(*args, **kwargs): - self.is_registered_media = True - - def side_effect_action(*args, **kwargs): - self.is_registered_action = True - - mock_obj = mock.Mock() - mock_obj.apiinfo.version.return_value = '3.x' - mock_obj.mediatype.get.return_value = [ - {'type': register_st2_config_to_zabbix.SCRIPT_MEDIA_TYPE, - 'exec_path': 'other-script.sh'}, - {'type': 0} - ] - mock_obj.mediatype.create.return_value = {'mediatypeids': ['1']} - mock_obj.action.get.return_value = [] - mock_obj.user.addmedia.side_effect = side_effect_media - mock_obj.action.create.side_effect = side_effect_action - mock_client.return_value = mock_obj - - register_st2_config_to_zabbix.main() - - self.assertTrue(re.match(r"Success to register the configurations", - self.io_stdout.getvalue())) - self.assertTrue(self.is_registered_media) - self.assertTrue(self.is_registered_action) - - def test_register_mediatype_with_different_zabbix_version(self): - mock_client = mock.Mock() - - def side_effect_addmedia(*args, **kwargs): - return 'user.addmedia is called' - - def side_effect_userupdate(*args, **kwargs): - return 'user.update is called' - - # set side_effect of caling user.update and user.addmedia API - mock_client.user.addmedia.side_effect = side_effect_addmedia - mock_client.user.update.side_effect = side_effect_userupdate - - # When sending request that changes MediaType to Zabbix3.x, this calls user.addmedia API - mock_client.apiinfo.version.return_value = '3.x.y' - ret = register_st2_config_to_zabbix.register_media_to_admin(mock_client, 1, mock.Mock()) - self.assertEqual(ret, 'user.addmedia is called') - - # When sending request that changes MediaType to Zabbix3.x, this calls user.update API - mock_client.apiinfo.version.return_value = '4.x.y' - ret = register_st2_config_to_zabbix.register_media_to_admin(mock_client, 1, mock.Mock()) - self.assertEqual(ret, 'user.update is called') diff --git a/tests/test_tool_st2_dispatch.py b/tests/test_tool_st2_dispatch.py deleted file mode 100644 index 29e0bdb..0000000 --- a/tests/test_tool_st2_dispatch.py +++ /dev/null @@ -1,77 +0,0 @@ -import os -import sys -import mock -import json -import requests -from optparse import OptionParser - -from unittest import TestCase - -sys.path.append(os.path.dirname(os.path.realpath(__file__)) + '/../tools/scripts') - -import st2_dispatch - - -class FakeResponse(object): - - def __init__(self, text, status_code, reason, *args): - self.text = text - self.content = text.encode('utf-8') - self.status_code = status_code - self.reason = reason - if args: - self.headers = args[0] - - def json(self): - return json.loads(self.text) - - def raise_for_status(self): - raise Exception(self.reason) - - -class TestZabbixDispatcher(TestCase): - TOKEN = { - 'user': 'st2admin', - 'token': '44583f15945b4095afbf57058535ca64', - 'expiry': '2017-02-12T00:53:09.632783Z', - 'id': '589e607532ed3535707f10eb', - 'metadata': {} - } - - def setUp(self): - self.parser = OptionParser() - - self.parser.add_option('--userid', dest='st2_userid') - self.parser.add_option('--passwd', dest='st2_passwd') - self.parser.add_option('--api-url', dest='api_url') - self.parser.add_option('--auth-url', dest='auth_url') - self.parser.add_option('--api_key', dest='api_key') - self.parser.add_option('--trigger', dest='trigger', default="zabbix.event_handler") - self.parser.add_option('--alert-sendto', dest="alert_sendto", default="") - self.parser.add_option('--alert-subject', dest="alert_subject", default="") - self.parser.add_option('--alert-message', dest="alert_message", default="") - self.parser.add_option('--skip-config', dest='skip_config', default=True) - self.parser.add_option('--config-file', dest='config_file') - - @mock.patch.object( - requests, 'post', - mock.MagicMock(return_value=FakeResponse(json.dumps(TOKEN), 200, 'OK'))) - def test_dispatch_trigger(self): - (options, _) = self.parser.parse_args([ - '--userid', 'foo', - '--passwd', 'bar', - '--api-url', 'https://localhost/api/v1', - '--auth-url', 'https://localhost/auth/v1', - ]) - - dispatcher = st2_dispatch.ZabbixDispatcher(options) - self.assertEqual(dispatcher.client.token, self.TOKEN['token']) - - resp = dispatcher.dispatch_trigger(args=[ - options.api_url, - options.auth_url, - options.st2_userid, - options.st2_passwd, - 'foo', 'bar', 'baz' - ]) - self.assertEqual(resp.status_code, 200) diff --git a/tests/test_test_credentials.py b/tests/test_verify_credentials.py similarity index 62% rename from tests/test_test_credentials.py rename to tests/test_verify_credentials.py index ed81abe..e48b979 100644 --- a/tests/test_test_credentials.py +++ b/tests/test_verify_credentials.py @@ -1,14 +1,14 @@ import mock from zabbix_base_action_test_case import ZabbixBaseActionTestCase -from test_credentials import TestCredentials +from verify_credentials import VerifyCredentials -from pyzabbix.api import ZabbixAPIException +from zabbix_utils.exceptions import APIRequestError -class TestCredentialsTestCase(ZabbixBaseActionTestCase): +class VerifyCredentialsTestCase(ZabbixBaseActionTestCase): __test__ = True - action_cls = TestCredentials + action_cls = VerifyCredentials @mock.patch('lib.actions.ZabbixBaseAction.connect') def test_run(self, mock_connect): @@ -19,6 +19,6 @@ def test_run(self, mock_connect): @mock.patch('lib.actions.ZabbixBaseAction.connect') def test_run_connection_error(self, mock_connect): action = self.get_action_instance(self.full_config) - mock_connect.side_effect = ZabbixAPIException('login error') - with self.assertRaises(ZabbixAPIException): + mock_connect.side_effect = APIRequestError('login error') + with self.assertRaises(APIRequestError): action.run() diff --git a/tests/zabbix_base_action_test_case.py b/tests/zabbix_base_action_test_case.py index f7c8e7a..f312965 100644 --- a/tests/zabbix_base_action_test_case.py +++ b/tests/zabbix_base_action_test_case.py @@ -12,6 +12,7 @@ def setUp(self): self._full_config = self.load_yaml('full.yaml') self._blank_config = self.load_yaml('blank.yaml') + self._token_config = self.load_yaml('token.yaml') def load_yaml(self, filename): return yaml.safe_load(self.get_fixture_content(filename)) @@ -26,3 +27,7 @@ def full_config(self): @property def blank_config(self): return self._blank_config + + @property + def token_config(self): + return self._token_config diff --git a/tools/register_st2_config_to_zabbix.py b/tools/register_st2_config_to_zabbix.py deleted file mode 100755 index b401c98..0000000 --- a/tools/register_st2_config_to_zabbix.py +++ /dev/null @@ -1,191 +0,0 @@ -#!/usr/bin/env python3 -import json -import sys - -from optparse import OptionParser -from zabbix.api import ZabbixAPI -from pyzabbix.api import ZabbixAPIException -from six.moves.urllib.error import URLError - -# This constant describes 'script' value of 'type' property in the MediaType, -# which is specified in the Zabbix API specification. -SCRIPT_MEDIA_TYPE = '1' - -# This is a constant for the metadata of MediaType to be registered -ST2_DISPATCHER_SCRIPT = 'st2_dispatch.py' -ST2_ACTION_NAME = 'Dispatching to StackStorm' - - -def get_options(): - parser = OptionParser() - - parser.add_option('-z', '--zabbix-url', dest="z_url", - help="The URL of Zabbix Server") - parser.add_option('-u', '--username', dest="z_userid", default='Admin', - help="Login username to login Zabbix Server") - parser.add_option('-p', '--password', dest="z_passwd", default='zabbix', - help="Password which is associated with the username") - parser.add_option('-s', '--sendto', dest="z_sendto", default='Admin', - help="Address, user name or other identifier of the recipient") - - (options, args) = parser.parse_args() - - if not options.z_url: - parser.error('Zabbix Server URL is not given') - - return (options, args) - - -def is_already_registered_mediatype(client, options): - """ - This method checks target MediaType has already been registered, or not. - """ - for mtype in client.mediatype.get(): - if mtype['type'] == SCRIPT_MEDIA_TYPE and mtype['exec_path'] == ST2_DISPATCHER_SCRIPT: - return mtype['mediatypeid'] - - -def is_already_registered_action(client, options): - """ - This method checks target Action has already been registered, or not. - """ - for action in client.action.get(): - if action['name'] == ST2_ACTION_NAME: - return action['actionid'] - - -def register_media_type(client, options, mediatype_id=None): - """ - This method registers a MediaType which dispatches alert to the StackStorm. - """ - mediatype_args = [ - '-- CHANGE ME : api_url (e.g. https://st2-node/api/v1)', - '-- CHANGE ME : auth_url (e.g. https://st2-node/auth/v1)', - '-- CHANGE ME : login uername of StackStorm --', - '-- CHANGE ME : login password of StackStorm --', - '{ALERT.SENDTO}', - '{ALERT.SUBJECT}', - '{ALERT.MESSAGE}', - ] - - # send request to register a new MediaType for StackStorm - params = { - 'description': 'StackStorm', - 'type': SCRIPT_MEDIA_TYPE, - 'exec_path': ST2_DISPATCHER_SCRIPT, - 'exec_params': "\n".join(mediatype_args) + "\n", - } - if mediatype_id: - params['mediatypeid'] = mediatype_id - - ret = client.mediatype.update(**params) - else: - ret = client.mediatype.create(**params) - - return ret['mediatypeids'][0] - - -def register_action(client, mediatype_id, options, action_id=None): - - if action_id: - client.action.delete(action_id) - - return client.action.create(**{ - 'name': ST2_ACTION_NAME, - 'esc_period': 360, - 'eventsource': 0, # means event created by a trigger - 'def_shortdata': '{TRIGGER.STATUS}: {TRIGGER.NAME}', - 'def_longdata': json.dumps({ - 'event': { - 'id': '{EVENT.ID}', - 'time': '{EVENT.TIME}', - }, - 'trigger': { - 'id': '{TRIGGER.ID}', - 'name': '{TRIGGER.NAME}', - 'status': '{TRIGGER.STATUS}', - }, - 'items': [{ - 'name': '{ITEM.NAME%s}' % index, - 'host': '{HOST.NAME%s}' % index, - 'key': '{ITEM.KEY%s}' % index, - 'value': '{ITEM.VALUE%s}' % index - } for index in range(1, 9)], - }), - 'operations': [{ - "operationtype": 0, - "esc_period": 0, - "esc_step_from": 1, - "esc_step_to": 1, - "evaltype": 0, - "opmessage_usr": [{"userid": "1"}], - "opmessage": { - "default_msg": 1, - "mediatypeid": mediatype_id, - } - }] - }) - - -def register_media_to_admin(client, mediatype_id, options): - major_version = int(client.apiinfo.version()[0]) - if major_version >= 4: - # This is because user.addmedia api was removed from Zabbix 4.0. - return client.user.update(**{ - "userid": "1", - "user_medias": [{ - "mediatypeid": mediatype_id, - "sendto": options.z_sendto, - "active": "0", - "severity": "63", - "period": "1-7,00:00-24:00", - }] - }) - else: - return client.user.addmedia(**{ - "users": [ - {"userid": "1"}, - ], - "medias": { - "mediatypeid": mediatype_id, - "sendto": options.z_sendto, - "active": "0", - "severity": "63", - "period": "1-7,00:00-24:00", - } - }) - - -def main(): - (options, _) = get_options() - - try: - client = ZabbixAPI(url=options.z_url, - user=options.z_userid, - password=options.z_passwd) - except URLError as e: - sys.exit('Failed to connect Zabbix server (%s)' % e) - except ZabbixAPIException as e: - sys.exit('Failed to authenticate Zabbix (%s)' % e) - - # get ID of MediaType for StackStorm if it exists, or None. - mediatype_id = is_already_registered_mediatype(client, options) - - # register a new MediaType or update one which is already registered to dispatch events - # to the StackStorm - mediatype_id = register_media_type(client, options, mediatype_id) - - # get ID of Action for StackStorm if it exists, or None. - action_id = is_already_registered_action(client, options) - - # register a Action which is associated with the registered MediaType - register_action(client, mediatype_id, options, action_id) - - # register a Media to the Admin user - register_media_to_admin(client, mediatype_id, options) - - print('Success to register the configurations for StackStorm to the Zabbix Server.') - - -if __name__ == '__main__': - main() diff --git a/tools/scripts/st2_dispatch.py b/tools/scripts/st2_dispatch.py deleted file mode 100755 index d2498a6..0000000 --- a/tools/scripts/st2_dispatch.py +++ /dev/null @@ -1,147 +0,0 @@ -#!/usr/bin/env python3 - -from st2client.base import BaseCLIApp - -from optparse import OptionParser - -import json - - -class ZabbixDispatcher(BaseCLIApp): - - def __init__(self, options): - self.options = options - - # make a client object to connect st2api - self.client = self.get_client(args=options) - - # If no API key was passed, get a token using user/pass - if not self.options.api_key: - self.client.token = self._get_auth_token(client=self.client, - username=options.st2_userid, - password=options.st2_passwd, - cache_token=False) - - def dispatch_trigger(self, args): - - # Validate if the Alert message is a valid JSON List or Dict and replace - # alert_message (string)with an object so that its correctly formatted. - try: - json_alert = json.loads(self.options.alert_message) - except: - pass - else: - setattr(self.options, 'alert_message', json_alert) - - body = { - 'trigger': self.options.trigger, - 'payload': { - 'alert_sendto': self.options.alert_sendto, - 'alert_subject': self.options.alert_subject, - 'alert_message': self.options.alert_message, - 'extra_args': args, - }, - } - - # API Key is preferred over User/Pass when both are present. - if self.options.api_key: - print('ST2 Auth Method: API Key') - auth_method = 'St2-Api-Key' - auth_value = self.options.api_key - else: - print('ST2 Auth Method: Auth Token') - auth_method = 'X-Auth-Token' - auth_value = self.client.token - - # send request to st2api to dispatch trigger of Zabbix - return self.client.managers['Webhook'].client.post('/webhooks/st2', body, headers={ - 'Content-Type': 'application/json', auth_method: auth_value}) - - -def get_options(): - parser = OptionParser() - - # Default values will be overridden by JSON Dict or poitional args. - # If a default is defined but its not a required opt (API key Vs User/Pass) - # it can cause issues. - parser.add_option('--st2-userid', dest="st2_userid", default="st2admin", - help="Login username of StackStorm") - parser.add_option('--st2-passwd', dest="st2_passwd", default="", - help="Login password associated with the user") - parser.add_option('--st2-api-url', dest="api_url", - help="Endpoint URL for API") - parser.add_option('--st2-auth-url', dest="auth_url", - help="Endpoint URL for auth") - parser.add_option('--api-key', dest="api_key", - help="ST2 API Key to be used when no user/pass defined") - parser.add_option('--alert-sendto', dest="alert_sendto", default="", - help="'Send to' value from user media configuration of Zabbix") - parser.add_option('--alert-subject', dest="alert_subject", default="", - help="'Default subject' value from action configuration of Zabbix") - parser.add_option('--alert-message', dest="alert_message", default="", - help="'Default message' value from action configuration of Zabbix") - parser.add_option('--trigger', dest="trigger", default="zabbix.event_handler", - help='Set the trigger name that dispatch should send to on St2') - parser.add_option('--skip-config', dest="skip_config", default=False, action='store_true', - help='Do NOT parse and use the CLI config file') - parser.add_option('--config-file', dest="config_file", - help='Path to the CLI config file') - - # Zabbix send argument as one string even though it includes whitespace - # (like $ st2_dispatch.py "foo bar" "hoge fuga" ...). - # And we can't specify keyward argument, we can only specify args. - # - # So it's hard for us to parse the argument of zabbix mediatype using optparse. - # Then, I decided to fix the order of the CLI arguemnts. - # - # See am_prepare_mediatype_exec_command in alert_manager.c in Zabbix src - - (options, args) = parser.parse_args() - - # Check if the very first positional argument is a valid JSON Dict. - try: - param_object = json.loads(args[0]) - except: - # First arg is not a JSON dict, assuming user/pass configuration - arg_list = ['api_url', 'auth_url', 'st2_userid', 'st2_passwd', - 'alert_sendto', 'alert_subject', 'alert_message'] - # Parse remaining positional args based on arg_list - for index, param in enumerate(arg_list): - if len(args) > index and args[index]: - setattr(options, param, args[index]) - - return (options, args[len(arg_list):]) - - else: - # First arg is a JSON dict, assuming apikey only - arg_list = ['alert_sendto', 'alert_subject', 'alert_message'] - # Since arg[0] is a JSON dict and we are handling it specifically, - # remove it from the list - args.pop(0) - # Assign all key/val in param_object to options - for k, v in param_object.items(): - setattr(options, k, v) - # Parse remaining positional args based on arg_list - for index, param in enumerate(arg_list): - if len(args) > index and args[index]: - setattr(options, param, args[index]) - - return (options, args[len(arg_list):]) - - -def main(): - # Parse and get arguments - print('Parsing Options') - (options, args) = get_options() - - # Instantiate st2 client and prepare data for dispatch to st2 - print('Preparing Dispatcher') - dispatcher = ZabbixDispatcher(options) - - # Dispatch data to trigger on st2 (default zabbix.event_handler) - print('Dispatching to ST2') - dispatcher.dispatch_trigger(args) - - -if __name__ == '__main__': - main() diff --git a/triggers/event_handler.yaml b/triggers/event_handler.yaml index 8748c75..4f433ee 100644 --- a/triggers/event_handler.yaml +++ b/triggers/event_handler.yaml @@ -1,20 +1,43 @@ --- name: event_handler pack: zabbix -description: 'Trigger type for zabbix event handler.' +description: "Trigger dispatched when Zabbix sends an alert via the direct StackStorm webhook." payload_schema: type: object properties: alert_sendto: type: string + description: "Recipient from Zabbix user media configuration ({ALERT.SENDTO})." alert_subject: type: string + description: "Alert subject from Zabbix action ({ALERT.SUBJECT})." alert_message: + description: "Alert message body ({ALERT.MESSAGE}). String or parsed JSON object." anyOf: - - type: array - type: object - type: string - extra_args: - type: array - items: - type: string + host: + type: string + description: "Host that triggered the event ({HOST.NAME})." + event_id: + type: string + description: "Zabbix event ID ({EVENT.ID})." + trigger_id: + type: string + description: "Zabbix trigger ID ({TRIGGER.ID})." + trigger_name: + type: string + description: "Name of the Zabbix trigger ({TRIGGER.NAME})." + trigger_status: + type: string + description: "PROBLEM or OK ({TRIGGER.STATUS})." + trigger_severity: + type: string + description: "Severity: Not classified, Information, Warning, Average, High, Disaster ({TRIGGER.SEVERITY})." + event_time: + type: string + description: "Time the event occurred ({EVENT.TIME})." + event_date: + type: string + description: "Date the event occurred ({EVENT.DATE})." + additionalProperties: true diff --git a/zabbix.yaml.example b/zabbix.yaml.example index bcf96c5..b49a95d 100644 --- a/zabbix.yaml.example +++ b/zabbix.yaml.example @@ -1,5 +1,5 @@ --- -zabbix: - url: http://localhost/zabbix - username: Admin - password: zabbix +url: http://localhost:8080 +username: Admin +password: zabbix +# api_token: "your-api-token-here" # preferred over username/password