diff --git a/.github/release-please-config.json b/.github/release-please-config.json
new file mode 100644
index 0000000..5ee1513
--- /dev/null
+++ b/.github/release-please-config.json
@@ -0,0 +1,12 @@
+{
+ "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
+ "bootstrap-sha": "f962fef7dc6d573cf6dbc54dff5a22f7f38414c9",
+ "include-component-in-tag": false,
+ "include-v-in-tag": false,
+ "packages": {
+ ".": {
+ "release-type": "python",
+ "package-name": "pyzabbix"
+ }
+ }
+}
diff --git a/.github/release-please-manifest.json b/.github/release-please-manifest.json
new file mode 100644
index 0000000..3e8c001
--- /dev/null
+++ b/.github/release-please-manifest.json
@@ -0,0 +1 @@
+{".":"1.3.1"}
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..d1106cd
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,128 @@
+name: CI
+
+on:
+ push:
+ tags: ["*.*.*"]
+ branches: [master]
+ pull_request:
+ branches: [master]
+
+ schedule:
+ - cron: 37 1 * * 1
+
+jobs:
+ pre-commit:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+
+ - uses: actions/setup-python@v4
+ with:
+ python-version: 3.x
+
+ - uses: pre-commit/action@v3.0.0
+
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - uses: actions/setup-python@v4
+ with:
+ python-version: 3.x
+
+ - uses: actions/cache@v3
+ with:
+ path: ~/.cache/pip
+ key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
+ restore-keys: |
+ ${{ runner.os }}-pip-
+
+ - run: make install
+ - run: make lint
+
+ test:
+ runs-on: ubuntu-20.04
+ strategy:
+ matrix:
+ python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
+
+ steps:
+ - uses: actions/checkout@v3
+
+ - uses: actions/setup-python@v4
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - uses: actions/cache@v3
+ with:
+ path: ~/.cache/pip
+ key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
+ restore-keys: |
+ ${{ runner.os }}-pip-
+
+ - run: make install
+ - run: make test
+
+ e2e:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ zabbix-version: ["4.0", "5.0", "6.0", "6.4"]
+
+ env:
+ ZABBIX_VERSION: ${{ matrix.zabbix-version }}
+
+ steps:
+ - uses: actions/checkout@v3
+
+ - uses: actions/setup-python@v4
+ with:
+ python-version: 3.x
+
+ - uses: actions/cache@v3
+ with:
+ path: ~/.cache/pip
+ key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
+ restore-keys: |
+ ${{ runner.os }}-pip-
+
+ - run: docker-compose up -d
+ - run: docker-compose images
+ - run: make install
+ - run: make e2e
+
+ publish:
+ if: startsWith(github.ref, 'refs/tags')
+ needs: [pre-commit, lint, test, e2e]
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+
+ - uses: actions/setup-python@v4
+ with:
+ python-version: 3.x
+
+ - uses: actions/cache@v3
+ with:
+ path: ~/.cache/pip
+ key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
+ restore-keys: |
+ ${{ runner.os }}-pip-
+
+ - run: make clean build
+
+ - if: github.repository_owner == 'lukecyca'
+ uses: pypa/gh-action-pypi-publish@v1.5.0
+ with:
+ password: ${{ secrets.PYPI_API_TOKEN }}
+
+ - if: github.repository_owner == 'lukecyca'
+ uses: softprops/action-gh-release@v1
+ with:
+ draft: true
+ body: |
+ Please see [CHANGELOG.md](https://github.com/lukecyca/pyzabbix/blob/master/CHANGELOG.md)
diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml
new file mode 100644
index 0000000..b56155c
--- /dev/null
+++ b/.github/workflows/release-please.yml
@@ -0,0 +1,16 @@
+name: Release please
+
+on:
+ push:
+ branches: [master]
+
+jobs:
+ release-please:
+ if: github.repository == 'lukecyca/pyzabbix'
+
+ runs-on: ubuntu-latest
+ steps:
+ - uses: google-github-actions/release-please-action@v4
+ with:
+ config-file: .github/release-please-config.json
+ manifest-file: .github/release-please-manifest.json
diff --git a/.gitignore b/.gitignore
index cd45890..02eb1f7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,167 @@
-build
+## Custom .gitignore
+################################################################################
+
+## Github Python .gitignore
+## See https://github.com/github/gitignore/blob/main/Python.gitignore
+################################################################################
+
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
dist/
-pyzabbix.egg-info/
-pyzabbix/*.pyc
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+# in version control.
+# https://pdm.fming.dev/#use-with-ide
+.pdm.toml
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
diff --git a/.hgtags b/.hgtags
deleted file mode 100644
index 030ff51..0000000
--- a/.hgtags
+++ /dev/null
@@ -1,2 +0,0 @@
-5211cf05c2f81c064e08075cdd2689760df0f7d1 0.1
-c70d2bb0ade06f5dbfb40a382a7bc17ad3d62bbc 0.1
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..720208f
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,53 @@
+---
+# See https://pre-commit.com for more information
+# See https://pre-commit.com/hooks.html for more hooks
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v4.4.0
+ hooks:
+ - id: check-added-large-files
+ - id: check-case-conflict
+ - id: check-executables-have-shebangs
+ - id: check-shebang-scripts-are-executable
+ - id: check-symlinks
+ - id: destroyed-symlinks
+
+ - id: check-json
+ - id: check-yaml
+ - id: check-toml
+
+ - id: check-merge-conflict
+ - id: end-of-file-fixer
+ - id: mixed-line-ending
+ - id: trailing-whitespace
+
+ - id: name-tests-test
+
+ - repo: https://github.com/pre-commit/mirrors-prettier
+ rev: v3.0.0
+ hooks:
+ - id: prettier
+ files: \.(md|yml|yaml|json)$
+ exclude: (\.github/release-please-manifest\.json|CHANGELOG\.md)$
+
+ - repo: https://github.com/codespell-project/codespell
+ rev: v2.2.5
+ hooks:
+ - id: codespell
+ exclude: ^examples/additemcsv.py$
+
+ - repo: https://github.com/pycqa/isort
+ rev: 5.12.0
+ hooks:
+ - id: isort
+
+ - repo: https://github.com/psf/black
+ rev: 23.7.0
+ hooks:
+ - id: black
+
+ - repo: https://github.com/asottile/pyupgrade
+ rev: v3.9.0
+ hooks:
+ - id: pyupgrade
+ args: [--py3-plus, --py36-plus]
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 5538df2..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,9 +0,0 @@
-language: python
-python:
- - "2.6"
- - "2.7"
- - "3.3"
- - "3.4"
-install:
- - pip install requests
-script: python setup.py nosetests
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..282e54e
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,60 @@
+# Changelog
+
+## [1.3.1](https://github.com/lukecyca/pyzabbix/compare/1.3.0...v1.3.1) (2023-12-18)
+
+
+### Documentation
+
+* add link to the zappix project ([#217](https://github.com/lukecyca/pyzabbix/issues/217)) ([962693d](https://github.com/lukecyca/pyzabbix/commit/962693d88a7cb3c53aeb0b7d62fc7198921abca8))
+* regenerate changelog ([8747c18](https://github.com/lukecyca/pyzabbix/commit/8747c185503a74282df2b66d07ccb8f58a114d86))
+
+
+
+## [1.3.0](https://github.com/lukecyca/pyzabbix/compare/1.2.1...1.3.0) (2023-04-08)
+
+### :rocket: Features
+
+- add zabbix 6.4 header authentication
+
+
+
+## [1.2.1](https://github.com/lukecyca/pyzabbix/compare/1.2.0...1.2.1) (2022-08-25)
+
+### :bug: Bug Fixes
+
+- improve deprecation message for confimport
+
+
+
+## [1.2.0](https://github.com/lukecyca/pyzabbix/compare/1.1.0...1.2.0) (2022-08-04)
+
+### :bug: Bug Fixes
+
+- catch ValueError during json parsing
+
+### :rocket: Features
+
+- parse version using packaging.version
+
+
+
+## [1.1.0](https://github.com/lukecyca/pyzabbix/compare/1.0.0...1.1.0) (2022-07-28)
+
+### :bug: Bug Fixes
+
+- api object/method attributes should be private
+- auto correct server url trailing slash
+
+### :rocket: Features
+
+- replace custom handler with logging.NullHandler
+- package is typed
+- deprecate ZabbixAPI.confimport alias
+- allow creating calls using dict syntax
+- replace dynamic func with ZabbixAPIMethod
+- rename ZabbixAPIObjectClass to ZabbixAPIObject
+- require >=python3.6
+
+### Reverts
+
+- chore: add more typings
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..ccf99af
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,48 @@
+all: lint test
+
+.DEFAULT_GOAL: install
+
+SHELL = bash
+CPU_CORES = $(shell N=$$(nproc); echo $$(( $$N > 4 ? 4 : $$N )))
+
+VENV = .venv
+$(VENV):
+ python3 -m venv $(VENV)
+ $(MAKE) install
+
+install: $(VENV)
+ $(VENV)/bin/pip install --upgrade pip setuptools wheel build
+ $(VENV)/bin/pip install --editable .[dev]
+
+format: $(VENV)
+ $(VENV)/bin/black .
+ $(VENV)/bin/isort .
+
+lint: $(VENV)
+ $(VENV)/bin/black . --check
+ $(VENV)/bin/isort . --check
+ $(VENV)/bin/pylint --jobs=$(CPU_CORES) --output-format=colorized pyzabbix tests
+ $(VENV)/bin/mypy pyzabbix tests
+
+
+PYTEST_CMD = $(VENV)/bin/pytest -v \
+ --numprocesses=$(CPU_CORES) \
+ --color=yes
+
+test: $(VENV)
+ $(PYTEST_CMD) \
+ --cov-config=./pyproject.toml \
+ --cov-report=term \
+ --cov-report=xml:./coverage.xml \
+ --cov=pyzabbix \
+ tests
+
+.PHONY: e2e
+e2e: $(VENV)
+ $(PYTEST_CMD) e2e
+
+build: $(VENV)
+ $(VENV)/bin/python -m build .
+
+clean:
+ rm -Rf $(VENV) dist
diff --git a/README.markdown b/README.md
similarity index 57%
rename from README.markdown
rename to README.md
index 0395042..5dcbb76 100644
--- a/README.markdown
+++ b/README.md
@@ -1,15 +1,20 @@
-# PyZabbix #
+# PyZabbix
-**PyZabbix** is a Python module for working with the [Zabbix API](https://www.zabbix.com/documentation/3.0/manual/api/reference).
+**PyZabbix** is a Python module for working with the [Zabbix API](https://www.zabbix.com/documentation/current/manual/api/reference).
-[](https://travis-ci.org/lukecyca/pyzabbix)
-[](https://pypi.python.org/pypi/pyzabbix/)
-[](https://pypi.python.org/pypi/pyzabbix/)
+[](https://github.com/lukecyca/pyzabbix/actions/workflows/ci.yml)
+[](https://pypi.org/project/pyzabbix/)
+[](https://pypi.org/project/pyzabbix/)
## Requirements
-* Tested against Zabbix 1.8 through 3.0
-## Documentation ##
+- Tested against Zabbix 4.0 LTS, 5.0 LTS, 6.0 LTS and 6.4.
+
+## Documentation
+
+> [!NOTE]
+> If you are looking for a `Zabbix Sender`, please use the [`zappix` library](https://pypi.org/project/zappix).
+
### Getting Started
Install PyZabbix using pip:
@@ -25,22 +30,26 @@ from pyzabbix import ZabbixAPI
zapi = ZabbixAPI("http://zabbixserver.example.com")
zapi.login("zabbix user", "zabbix pass")
+# You can also authenticate using an API token instead of user/pass with Zabbix >= 5.4
+# zapi.login(api_token='xxxxx')
print("Connected to Zabbix API Version %s" % zapi.api_version())
for h in zapi.host.get(output="extend"):
print(h['hostid'])
```
-Refer to the [Zabbix API Documentation](https://www.zabbix.com/documentation/3.0/manual/api/reference) and the [PyZabbix Examples](https://github.com/lukecyca/pyzabbix/tree/master/examples) for more information.
+Refer to the [Zabbix API Documentation](https://www.zabbix.com/documentation/current/manual/api/reference) and the [PyZabbix Examples](https://github.com/lukecyca/pyzabbix/tree/master/examples) for more information.
### Customizing the HTTP request
-PyZabbix uses the [requests](http://www.python-requests.org/en/latest/) library for http. You can customize the request parameters by configuring the [requests Session](http://docs.python-requests.org/en/latest/user/advanced/#session-objects) object used by PyZabbix.
+
+PyZabbix uses the [requests](https://requests.readthedocs.io/en/master/) library for HTTP. You can customize the request parameters by configuring the [requests Session](https://requests.readthedocs.io/en/master/user/advanced/#session-objects) object used by PyZabbix.
This is useful for:
-* Customizing headers
-* Enabling HTTP authentication
-* Enabling Keep-Alive
-* Disabling SSL certificate verification
+
+- Customizing headers
+- Enabling HTTP authentication
+- Enabling Keep-Alive
+- Disabling SSL certificate verification
```python
from pyzabbix import ZabbixAPI
@@ -58,10 +67,15 @@ zapi.timeout = 5.1
# Login (in case of HTTP Auth, only the username is needed, the password, if passed, will be ignored)
zapi.login("http user", "http password")
+
+# You can also authenticate using an API token instead of user/pass with Zabbix >= 5.4
+# zapi.login(api_token='xxxxx')
```
### Enabling debug logging
+
If you need to debug some issue with the Zabbix API, you can enable the output of logging, pyzabbix already uses the default python logging facility but by default, it logs to "Null", you can change this behavior on your program, here's an example:
+
```python
import sys
import logging
@@ -76,7 +90,12 @@ log.setLevel(logging.DEBUG)
zapi = ZabbixAPI("http://zabbixserver.example.com")
zapi.login('admin','password')
+
+# You can also authenticate using an API token instead of user/pass with Zabbix >= 5.4
+# zapi.login(api_token='xxxxx')
+
```
+
The expected output is as following:
```
@@ -98,8 +117,20 @@ Response Body: {
>>>
```
-## License ##
-LGPL 2.1 http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
+## Development
+
+To develop this project, start by reading the `Makefile` to have a basic understanding of the possible tasks.
+
+Install the project and the dependencies in a virtual environment:
+
+```sh
+make install
+source .venv/bin/activate
+```
+
+## License
+
+LGPL 2.1 http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
Zabbix API Python Library.
@@ -114,9 +145,9 @@ version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
-Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..fd7d4af
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,42 @@
+---
+# For development purpose only
+version: "3.8"
+
+services:
+ postgres-server:
+ image: postgres:13-alpine
+ environment:
+ POSTGRES_USER: zabbix
+ POSTGRES_PASSWORD: zabbix
+
+ zabbix-server:
+ image: zabbix/zabbix-server-pgsql:alpine-${ZABBIX_VERSION:-6.2}-latest
+ ports:
+ - 10051:10051
+ volumes:
+ - /etc/localtime:/etc/localtime:ro
+ environment:
+ POSTGRES_USER: zabbix
+ POSTGRES_PASSWORD: zabbix
+ ZBX_CACHEUPDATEFREQUENCY: 1
+ depends_on:
+ - postgres-server
+
+ zabbix-web:
+ image: zabbix/zabbix-web-nginx-pgsql:alpine-${ZABBIX_VERSION:-6.2}-latest
+ ports:
+ - 8888:8080
+ volumes:
+ - /etc/localtime:/etc/localtime:ro
+ environment:
+ POSTGRES_USER: zabbix
+ POSTGRES_PASSWORD: zabbix
+ depends_on:
+ - postgres-server
+ - zabbix-server
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:8080/"]
+ interval: 10s
+ timeout: 5s
+ retries: 3
+ start_period: 30s
diff --git a/e2e/__init__.py b/e2e/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/e2e/api_test.py b/e2e/api_test.py
new file mode 100644
index 0000000..a16a425
--- /dev/null
+++ b/e2e/api_test.py
@@ -0,0 +1,33 @@
+from pyzabbix import ZabbixAPI
+
+from .conftest import ZABBIX_VERSION
+
+
+def test_login(zapi: ZabbixAPI) -> None:
+ assert zapi.auth
+
+
+def test_version(zapi: ZabbixAPI) -> None:
+ assert zapi.api_version().startswith(ZABBIX_VERSION)
+
+
+def test_host_get(zapi: ZabbixAPI) -> None:
+ hosts = zapi.host.get(filter={"host": ["Zabbix server"]})
+ assert hosts[0]["host"] == "Zabbix server"
+
+
+def test_host_update_interface(zapi: ZabbixAPI) -> None:
+ hosts = zapi.host.get(filter={"host": ["Zabbix server"]}, output="extend")
+ assert hosts[0]["host"] == "Zabbix server"
+
+ interfaces = zapi.hostinterface.get(hostids=hosts[0]["hostid"])
+ assert interfaces[0]["ip"] == "127.0.0.1"
+
+ interfaces_update = zapi.hostinterface.update(
+ interfaceid=interfaces[0]["interfaceid"],
+ dns="zabbix-agent",
+ )
+ assert interfaces_update["interfaceids"] == [interfaces[0]["interfaceid"]]
+
+ interfaces = zapi.hostinterface.get(hostids=hosts[0]["hostid"])
+ assert interfaces[0]["dns"] == "zabbix-agent"
diff --git a/e2e/conftest.py b/e2e/conftest.py
new file mode 100644
index 0000000..8625f12
--- /dev/null
+++ b/e2e/conftest.py
@@ -0,0 +1,37 @@
+from os import getenv
+from time import sleep
+
+import pytest
+from requests.exceptions import ConnectionError
+
+from pyzabbix import ZabbixAPI, ZabbixAPIException
+
+ZABBIX_SERVER = "http://localhost:8888"
+ZABBIX_VERSION = getenv("ZABBIX_VERSION", "6.2")
+
+
+@pytest.fixture(scope="session", autouse=True)
+def wait_for_zabbix() -> None:
+ max_attempts = 30
+ while max_attempts > 0:
+ try:
+ ZabbixAPI(ZABBIX_SERVER).login("Admin", "zabbix")
+ except (ConnectionError, ZabbixAPIException):
+ sleep(2)
+ max_attempts -= 1
+ continue
+ break
+
+ if max_attempts <= 0:
+ pytest.exit("waiting for zabbix failed!", 1)
+
+ # extra sleep if zabbix wasn't ready on first attempt
+ if max_attempts < 30:
+ sleep(5)
+
+
+@pytest.fixture()
+def zapi() -> ZabbixAPI:
+ api = ZabbixAPI(ZABBIX_SERVER)
+ api.login("Admin", "zabbix")
+ return api
diff --git a/examples/add_item.py b/examples/add_item.py
index 4b6b3bf..fdc8b37 100644
--- a/examples/add_item.py
+++ b/examples/add_item.py
@@ -2,37 +2,38 @@
Looks up a host based on its name, and then adds an item to it
"""
-from pyzabbix import ZabbixAPI, ZabbixAPIException
import sys
+from pyzabbix import ZabbixAPI, ZabbixAPIException
+
# The hostname at which the Zabbix web interface is available
-ZABBIX_SERVER = 'https://zabbix.example.com'
+ZABBIX_SERVER = "https://zabbix.example.com"
zapi = ZabbixAPI(ZABBIX_SERVER)
# Login to the Zabbix API
-zapi.login('Admin', 'zabbix')
+zapi.login("Admin", "zabbix")
-host_name = 'example.com'
+host_name = "example.com"
hosts = zapi.host.get(filter={"host": host_name}, selectInterfaces=["interfaceid"])
if hosts:
host_id = hosts[0]["hostid"]
- print("Found host id {0}".format(host_id))
+ print(f"Found host id {host_id}")
try:
item = zapi.item.create(
hostid=host_id,
- name='Used disk space on $1 in %',
- key_='vfs.fs.size[/,pused]',
+ name="Used disk space on $1 in %",
+ key_="vfs.fs.size[/,pused]",
type=0,
value_type=3,
interfaceid=hosts[0]["interfaces"][0]["interfaceid"],
- delay=30
+ delay=30,
)
except ZabbixAPIException as e:
print(e)
sys.exit()
- print("Added item with itemid {0} to host: {1}".format(item["itemids"][0], host_name))
+ print("Added item with itemid {} to host: {}".format(item["itemids"][0], host_name))
else:
print("No hosts found")
diff --git a/examples/additemcsv.py b/examples/additemcsv.py
new file mode 100644
index 0000000..5e024c8
--- /dev/null
+++ b/examples/additemcsv.py
@@ -0,0 +1,81 @@
+"""
+Desenvolvimento: WagPTech
+
+Programa para incluir Item no Zabbix em um Host especĂfico.
+
+Sintaxe: python ZBXadditem.py host_name arquivo.csv
+
+Entrar com login e senha do zabbix (necessário ser ADM)
+
+Formato do arquivo:
+key
+item1
+item2
+...
+"""
+
+import csv
+import getpass
+import os
+import sys
+
+from pyzabbix import ZabbixAPI, ZabbixAPIException
+
+host_name = sys.argv[1]
+arquivo = sys.argv[2]
+open(arquivo, newline="", encoding="utf-8")
+
+# Zabbix server
+
+zapi = ZabbixAPI("http://30.0.0.47/zabbix")
+
+# Login com o Zabbix API
+
+login = input("Insira seu login: ")
+passwd = getpass.getpass("Digite sua senha: ")
+
+zapi.login(login, passwd)
+
+add = int(0)
+nadd = int(0)
+
+hosts = zapi.host.get(filter={"host": host_name}, selectInterfaces=["interfaceid"])
+if hosts:
+ host_id = hosts[0]["hostid"]
+ print("host_name " + host_name + f" @ host id {host_id}")
+ with open(
+ arquivo, newline="", encoding="utf-8"
+ ) as csvfile: # sys.argv[2]/'zbx_l15_k10.csv'
+ reader = csv.DictReader(csvfile)
+ for row in reader:
+ try:
+ # print ("Add Item: " + row['key'])
+ item = zapi.item.create(
+ hostid=host_id,
+ name=row["key"],
+ key_=row["key"],
+ type=2, # 0-Zabbix agent; 2-Zabbix trapper; 3-simple check; 5-Zabbix internal;
+ # 7-Zabbix agent (active); 8-Zabbix aggregate; 9-web item;
+ # 10-external check; 11-database monitor; 12-IPMI agent;
+ # 13-SSH agent; 14-TELNET agent; 15-calculated;
+ # 16-JMX agent; 17-SNMP trap; 18-Dependent item; 19-HTTP agent; 20-SNMP agent; 21-Script.
+ value_type=3, # 0-numeric float; 1-character; 2-log; 3-numeric unsigned; 4-text.
+ interfaceid=hosts[0]["interfaces"][0]["interfaceid"],
+ delay=60,
+ status=1, # 0-enabled item; 1-disabled item.
+ )
+ add = add + 1
+ if add > 0:
+ print("Adicionados: " + str(add))
+ except ZabbixAPIException as error:
+ nadd = nadd + 1
+ if nadd > 0:
+ print("Recusados: " + str(nadd))
+ # print(error)
+ if add > 0:
+ print("Total de itens adicionados: " + str(add) + " itens.")
+ if nadd > 0:
+ print("Total de itens recusados: " + str(nadd) + " itens.")
+ sys.exit()
+else:
+ print("No hosts found")
diff --git a/examples/current_issues.py b/examples/current_issues.py
index 5798f57..3e3f104 100644
--- a/examples/current_issues.py
+++ b/examples/current_issues.py
@@ -5,42 +5,46 @@
from pyzabbix import ZabbixAPI
# The hostname at which the Zabbix web interface is available
-ZABBIX_SERVER = 'https://zabbix.example.com'
+ZABBIX_SERVER = "https://zabbix.example.com"
zapi = ZabbixAPI(ZABBIX_SERVER)
# Login to the Zabbix API
-zapi.login('api_username', 'api_password')
+zapi.login("api_username", "api_password")
# Get a list of all issues (AKA tripped triggers)
-triggers = zapi.trigger.get(only_true=1,
- skipDependent=1,
- monitored=1,
- active=1,
- output='extend',
- expandDescription=1,
- selectHosts=['host'],
- )
+triggers = zapi.trigger.get(
+ only_true=1,
+ skipDependent=1,
+ monitored=1,
+ active=1,
+ output="extend",
+ expandDescription=1,
+ selectHosts=["host"],
+)
# Do another query to find out which issues are Unacknowledged
-unack_triggers = zapi.trigger.get(only_true=1,
- skipDependent=1,
- monitored=1,
- active=1,
- output='extend',
- expandDescription=1,
- selectHosts=['host'],
- withLastEventUnacknowledged=1,
- )
-unack_trigger_ids = [t['triggerid'] for t in unack_triggers]
+unack_triggers = zapi.trigger.get(
+ only_true=1,
+ skipDependent=1,
+ monitored=1,
+ active=1,
+ output="extend",
+ expandDescription=1,
+ selectHosts=["host"],
+ withLastEventUnacknowledged=1,
+)
+unack_trigger_ids = [t["triggerid"] for t in unack_triggers]
for t in triggers:
- t['unacknowledged'] = True if t['triggerid'] in unack_trigger_ids \
- else False
+ t["unacknowledged"] = True if t["triggerid"] in unack_trigger_ids else False
# Print a list containing only "tripped" triggers
for t in triggers:
- if int(t['value']) == 1:
- print("{0} - {1} {2}".format(t['hosts'][0]['host'],
- t['description'],
- '(Unack)' if t['unacknowledged'] else '')
- )
+ if int(t["value"]) == 1:
+ print(
+ "{} - {} {}".format(
+ t["hosts"][0]["host"],
+ t["description"],
+ "(Unack)" if t["unacknowledged"] else "",
+ )
+ )
diff --git a/examples/export_history_csv.py b/examples/export_history_csv.py
new file mode 100644
index 0000000..564d066
--- /dev/null
+++ b/examples/export_history_csv.py
@@ -0,0 +1,196 @@
+# The research leading to these results has received funding from the
+# European Commission's Seventh Framework Programme (FP7/2007-13)
+# under grant agreement no 257386.
+# http://www.bonfire-project.eu/
+# Copyright 2012 Yahya Al-Hazmi, TU Berlin
+# Licensed 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
+
+import argparse
+import datetime
+import sys
+import time
+
+from pyzabbix import ZabbixAPI
+
+
+def login(zapi, username, password):
+ try:
+ zapi.login(username, password)
+ print("login succeed.")
+ except:
+ print("zabbix server is not reachable: ")
+ sys.exit()
+
+
+def getHostId(zapi, hostname, server):
+ if hostname == "":
+ print("hostname is missed")
+ sys.exit()
+ host = zapi.host.get(filter={"host": hostname}, output="extend")
+ if len(host) == 0:
+ print(f"hostname: {hostname} not found in zabbix server: {server}, exit")
+ sys.exit()
+ else:
+ return host[0]["hostid"]
+
+
+def getItems(zapi, key, hostid, hostname):
+ items = zapi.item.get(search={"key_": key}, hostids=hostid, output="extend")
+ if len(items) == 0:
+ print(f"item key: {key} not found in hostname: {hostname}")
+ sys.exit()
+ else:
+ return items
+
+
+def convertTimeStamp(inputTime):
+ if inputTime == "":
+ return ""
+ try:
+ tmpDate = datetime.datetime.strptime(inputTime, "%Y-%m-%d %H:%M:%S")
+ timestamp = int(time.mktime(tmpDate.timetuple()))
+ except:
+ print("time data %s does not match format Y-m-d H:M:S, exit" % (datetime))
+ sys.exit()
+
+ return timestamp
+
+
+def generateOutputFilename(output, hostname, key):
+ if output == "":
+ return hostname + "_" + key + ".csv"
+ else:
+ return output
+
+
+def exportToCSV(historys, key, output):
+ f = open(output, "w")
+ inc = 0
+ f.write("key;timestamp;valuei\n") # csv header
+ for history in historys:
+ f.write("{};{};{}\n".format(key, history["clock"], history["value"]))
+ inc = inc + 1
+ print("exported %i history to %s" % (inc, output))
+ f.close()
+
+
+def assignTimeRange(inputParameters, datetime1, datetime2):
+ timestamp1 = convertTimeStamp(datetime1)
+ timestamp2 = convertTimeStamp(datetime2)
+
+ # only timestamp1
+ if timestamp1 and not timestamp2:
+ inputParameters["time_from"] = timestamp1
+ inputParameters["time_till"] = convertTimeStamp(time.time()) # current time
+ # only timestamp2
+ elif not timestamp1 and timestamp2:
+ inputParameters["time_from"] = timestamp2
+ inputParameters["time_till"] = timestamp2
+ # no inserted both timestamps
+ elif not timestamp1 and not timestamp2:
+ inputParameters["time_from"] = convertTimeStamp(time.time()) # current time
+ inputParameters["time_till"] = convertTimeStamp(time.time()) # current time
+ # inserted both timestamps
+ else:
+ inputParameters["time_from"] = timestamp1
+ inputParameters["time_till"] = timestamp2
+
+
+def fetch_to_csv(
+ username, password, server, hostname, key, output, datetime1, datetime2, debuglevel
+):
+ print("login to zabbix server %s" % server)
+ zapi = ZabbixAPI(server + "/zabbix")
+ login(zapi, username, password)
+ hostid = getHostId(zapi, hostname, server)
+
+ # find itemid using key
+ print("key is: %s" % (key))
+ items = getItems(zapi, key, hostid, hostname)
+ item = items[0]
+
+ # parameter validation
+ inputParameters = {}
+ inputParameters["history"] = item["value_type"]
+ inputParameters["output"] = "extend"
+ inputParameters["itemids"] = [item["itemid"]]
+
+ assignTimeRange(inputParameters, datetime1, datetime2)
+
+ # get history
+ print("get history using this parameter")
+ print(inputParameters)
+ history = zapi.history.get(**inputParameters)
+
+ # export to File
+ output = generateOutputFilename(output, hostname, key)
+ exportToCSV(history, key, output)
+
+
+# Parsing Parameters
+parser = argparse.ArgumentParser(
+ description="Fetch history from aggregator and save it into CSV file"
+)
+parser.add_argument("-s", dest="server_IP", required=True, help="aggregator IP address")
+parser.add_argument(
+ "-n", dest="hostname", required=True, help="name of the monitored host"
+)
+parser.add_argument(
+ "-u",
+ dest="username",
+ default="Admin",
+ required=True,
+ help="zabbix username, default Admin",
+)
+parser.add_argument(
+ "-p", dest="password", default="zabbix", required=True, help="zabbix password"
+)
+parser.add_argument(
+ "-k",
+ dest="key",
+ default="",
+ required=True,
+ help="zabbix item key, if not specified the script will fetch all keys for the specified hostname",
+)
+parser.add_argument(
+ "-o", dest="output", default="", help="output file name, default hostname.csv"
+)
+parser.add_argument(
+ "-t1",
+ dest="datetime1",
+ default="",
+ help="begin date-time, use this pattern '2011-11-08 14:49:43' if only t1 specified then time period will be t1-now ",
+)
+parser.add_argument(
+ "-t2",
+ dest="datetime2",
+ default="",
+ help="end date-time, use this pattern '2011-11-08 14:49:43'",
+)
+parser.add_argument(
+ "-v", dest="debuglevel", default=0, type=int, help="log level, default 0"
+)
+args = parser.parse_args()
+
+# Calling fetching function
+fetch_to_csv(
+ args.username,
+ args.password,
+ args.server_IP,
+ args.hostname,
+ args.key,
+ args.output,
+ args.datetime1,
+ args.datetime2,
+ args.debuglevel,
+)
diff --git a/examples/fix_host_ips.py b/examples/fix_host_ips.py
index cbc5220..17f1b0c 100644
--- a/examples/fix_host_ips.py
+++ b/examples/fix_host_ips.py
@@ -7,10 +7,11 @@
"""
import socket
+
from pyzabbix import ZabbixAPI, ZabbixAPIException
# The hostname at which the Zabbix web interface is available
-ZABBIX_SERVER = 'https://zabbix.example.com'
+ZABBIX_SERVER = "https://zabbix.example.com"
zapi = ZabbixAPI(ZABBIX_SERVER)
@@ -18,37 +19,40 @@
zapi.session.verify = False
# Login to the Zabbix API
-zapi.login('Admin', 'zabbix')
+zapi.login("Admin", "zabbix")
# Loop through all hosts interfaces, getting only "main" interfaces of type "agent"
-for h in zapi.hostinterface.get(output=["dns", "ip", "useip"], selectHosts=["host"], filter={"main": 1, "type": 1}):
+for h in zapi.hostinterface.get(
+ output=["dns", "ip", "useip"], selectHosts=["host"], filter={"main": 1, "type": 1}
+):
# Make sure the hosts are named according to their FQDN
- if h['dns'] != h['hosts'][0]['host']:
- print('Warning: %s has dns "%s"' % (h['hosts'][0]['host'], h['dns']))
+ if h["dns"] != h["hosts"][0]["host"]:
+ print('Warning: {} has dns "{}"'.format(h["hosts"][0]["host"], h["dns"]))
# Make sure they are using hostnames to connect rather than IPs (could be also filtered in the get request)
- if h['useip'] == '1':
- print('%s is using IP instead of hostname. Skipping.' % h['hosts'][0]['host'])
+ if h["useip"] == "1":
+ print("%s is using IP instead of hostname. Skipping." % h["hosts"][0]["host"])
continue
# Do a DNS lookup for the host's DNS name
try:
- lookup = socket.gethostbyaddr(h['dns'])
+ lookup = socket.gethostbyaddr(h["dns"])
except socket.gaierror as e:
- print(h['dns'], e)
+ print(h["dns"], e)
continue
actual_ip = lookup[2][0]
# Check whether the looked-up IP matches the one stored in the host's IP
# field
- if actual_ip != h['ip']:
- print("%s has the wrong IP: %s. Changing it to: %s" % (h['hosts'][0]['host'],
- h['ip'],
- actual_ip))
+ if actual_ip != h["ip"]:
+ print(
+ "%s has the wrong IP: %s. Changing it to: %s"
+ % (h["hosts"][0]["host"], h["ip"], actual_ip)
+ )
# Set the host's IP field to match what the DNS lookup said it should
# be
try:
- zapi.hostinterface.update(interfaceid=h['interfaceid'], ip=actual_ip)
+ zapi.hostinterface.update(interfaceid=h["interfaceid"], ip=actual_ip)
except ZabbixAPIException as e:
print(e)
diff --git a/examples/history_data.py b/examples/history_data.py
index b4f6cac..b0b2e44 100644
--- a/examples/history_data.py
+++ b/examples/history_data.py
@@ -2,43 +2,50 @@
Retrieves history data for a given numeric (either int or float) item_id
"""
-from pyzabbix import ZabbixAPI
-from datetime import datetime
import time
+from datetime import datetime
+
+from pyzabbix import ZabbixAPI
# The hostname at which the Zabbix web interface is available
-ZABBIX_SERVER = 'http://localhost/zabbix'
+ZABBIX_SERVER = "http://localhost/zabbix"
zapi = ZabbixAPI(ZABBIX_SERVER)
# Login to the Zabbix API
-zapi.login('Admin', 'zabbix')
+zapi.login("Admin", "zabbix")
-item_id = 'item_id'
+item_id = "item_id"
# Create a time range
time_till = time.mktime(datetime.now().timetuple())
time_from = time_till - 60 * 60 * 4 # 4 hours
# Query item's history (integer) data
-history = zapi.history.get(itemids=[item_id],
- time_from=time_from,
- time_till=time_till,
- output='extend',
- limit='5000',
- )
+history = zapi.history.get(
+ itemids=[item_id],
+ time_from=time_from,
+ time_till=time_till,
+ output="extend",
+ limit="5000",
+)
# If nothing was found, try getting it from history (float) data
if not len(history):
- history = zapi.history.get(itemids=[item_id],
- time_from=time_from,
- time_till=time_till,
- output='extend',
- limit='5000',
- history=0,
- )
+ history = zapi.history.get(
+ itemids=[item_id],
+ time_from=time_from,
+ time_till=time_till,
+ output="extend",
+ limit="5000",
+ history=0,
+ )
# Print out each datapoint
for point in history:
- print("{0}: {1}".format(datetime.fromtimestamp(int(point['clock']))
- .strftime("%x %X"), point['value']))
+ print(
+ "{}: {}".format(
+ datetime.fromtimestamp(int(point["clock"])).strftime("%x %X"),
+ point["value"],
+ )
+ )
diff --git a/examples/import_templates.py b/examples/import_templates.py
index cf095e5..9cf6022 100644
--- a/examples/import_templates.py
+++ b/examples/import_templates.py
@@ -2,78 +2,67 @@
Import Zabbix XML templates
"""
+import os
+import sys
+
from pyzabbix import ZabbixAPI, ZabbixAPIException
-import glob
+
+if len(sys.argv) <= 1:
+ print(
+ "Please provide directory with templates as first ARG or the XML file with template."
+ )
+ exit(1)
+
+path = sys.argv[1]
# The hostname at which the Zabbix web interface is available
-ZABBIX_SERVER = 'https://zabbix.example.com'
+ZABBIX_SERVER = "https://zabbix.example.org"
zapi = ZabbixAPI(ZABBIX_SERVER)
# Login to the Zabbix API
-zapi.login("admin", "zabbix")
+# zapi.session.verify = False
+zapi.login("Admin", "zabbix")
rules = {
- 'applications': {
- 'createMissing': 'true',
- 'updateExisting': 'true'
- },
- 'discoveryRules': {
- 'createMissing': 'true',
- 'updateExisting': 'true'
- },
- 'graphs': {
- 'createMissing': 'true',
- 'updateExisting': 'true'
- },
- 'groups': {
- 'createMissing': 'true'
- },
- 'hosts': {
- 'createMissing': 'true',
- 'updateExisting': 'true'
- },
- 'images': {
- 'createMissing': 'true',
- 'updateExisting': 'true'
- },
- 'items': {
- 'createMissing': 'true',
- 'updateExisting': 'true'
- },
- 'maps': {
- 'createMissing': 'true',
- 'updateExisting': 'true'
- },
- 'screens': {
- 'createMissing': 'true',
- 'updateExisting': 'true'
- },
- 'templateLinkage': {
- 'createMissing': 'true',
- 'updateExisting': 'true'
- },
- 'templates': {
- 'createMissing': 'true',
- 'updateExisting': 'true'
- },
- 'templateScreens': {
- 'createMissing': 'true',
- 'updateExisting': 'true'
- },
- 'triggers': {
- 'createMissing': 'true',
- 'updateExisting': 'true'
+ "applications": {
+ "createMissing": True,
},
+ "discoveryRules": {"createMissing": True, "updateExisting": True},
+ "graphs": {"createMissing": True, "updateExisting": True},
+ "groups": {"createMissing": True},
+ "hosts": {"createMissing": True, "updateExisting": True},
+ "images": {"createMissing": True, "updateExisting": True},
+ "items": {"createMissing": True, "updateExisting": True},
+ "maps": {"createMissing": True, "updateExisting": True},
+ "screens": {"createMissing": True, "updateExisting": True},
+ "templateLinkage": {"createMissing": True},
+ "templates": {"createMissing": True, "updateExisting": True},
+ "templateScreens": {"createMissing": True, "updateExisting": True},
+ "triggers": {"createMissing": True, "updateExisting": True},
+ "valueMaps": {"createMissing": True, "updateExisting": True},
}
-path = './templates/*.xml'
-files = glob.glob(path)
-
-for file in files:
- with open(file, 'r') as f:
- template = f.read()
- try:
- zapi.confimport('xml', template, rules)
- except ZabbixAPIException as e:
- print(e)
+if os.path.isdir(path):
+ # path = path/*.xml
+ files = glob.glob(path + "/*.xml")
+ for file in files:
+ print(file)
+ with open(file) as f:
+ template = f.read()
+ try:
+ zapi.confimport("xml", template, rules)
+ except ZabbixAPIException as e:
+ print(e)
+ print("")
+elif os.path.isfile(path):
+ files = glob.glob(path)
+ for file in files:
+ with open(file) as f:
+ template = f.read()
+ try:
+ zapi.confimport("xml", template, rules)
+ except ZabbixAPIException as e:
+ print(e)
+else:
+ print("I need a xml file")
diff --git a/examples/timeout.py b/examples/timeout.py
index 7b30b6b..58617ab 100644
--- a/examples/timeout.py
+++ b/examples/timeout.py
@@ -5,7 +5,7 @@
from pyzabbix import ZabbixAPI
# The hostname at which the Zabbix web interface is available
-ZABBIX_SERVER = 'https://zabbix.example.com'
+ZABBIX_SERVER = "https://zabbix.example.com"
# Timeout (float) in seconds
# By default this timeout affects both the "connect" and "read", but
@@ -17,7 +17,7 @@
zapi = ZabbixAPI(ZABBIX_SERVER, timeout=TIMEOUT)
# Login to the Zabbix API
-zapi.login('api_username', 'api_password')
+zapi.login("api_username", "api_password")
# Or you can re-define it after
zapi.timeout = TIMEOUT
diff --git a/examples/with_context.py b/examples/with_context.py
new file mode 100644
index 0000000..97f4fde
--- /dev/null
+++ b/examples/with_context.py
@@ -0,0 +1,14 @@
+"""
+Prints hostnames for all known hosts.
+"""
+
+from pyzabbix import ZabbixAPI
+
+ZABBIX_SERVER = "https://zabbix.example.com"
+
+# Use context manager to auto-logout after request is done.
+with ZabbixAPI(ZABBIX_SERVER) as zapi:
+ zapi.login("api_username", "api_password")
+ hosts = zapi.host.get(output=["name"])
+ for host in hosts:
+ print(host["name"])
diff --git a/packaging/rpm/python-pyzabbix.spec b/packaging/rpm/python-pyzabbix.spec
index 81865b8..26a7692 100644
--- a/packaging/rpm/python-pyzabbix.spec
+++ b/packaging/rpm/python-pyzabbix.spec
@@ -17,7 +17,7 @@
Name: python-pyzabbix
-Version: 0.7.4
+Version: 0.8.2
Release: 1
Url: http://github.com/lukecyca/pyzabbix
Summary: Zabbix API Python interface
@@ -32,7 +32,7 @@ Requires: python-requests >= 1.0
%description
PyZabbix is a Python module for working with the Zabbix API.
-Tested against Zabbix 1.8 through 3.0.
+Tested against Zabbix 1.8 through 5.0.
%prep
%setup -q -n pyzabbix-%{version}
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..dd8c9ef
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,25 @@
+[tool.isort]
+profile = "black"
+combine_as_imports = true
+
+[tool.pylint.messages_control]
+disable = [
+ "missing-class-docstring",
+ "missing-function-docstring",
+ "missing-module-docstring",
+]
+
+[tool.mypy]
+allow_redefinition = true
+disallow_incomplete_defs = true
+
+[tool.pytest.ini_options]
+log_cli = true
+log_cli_level = "DEBUG"
+
+[tool.coverage.run]
+omit = ["tests/*", "e2e/*"]
+
+[build-system]
+requires = ["setuptools", "wheel"]
+build-backend = "setuptools.build_meta"
diff --git a/pyzabbix/__init__.py b/pyzabbix/__init__.py
index 27142b6..5603ad6 100644
--- a/pyzabbix/__init__.py
+++ b/pyzabbix/__init__.py
@@ -1,166 +1,7 @@
-from __future__ import unicode_literals
-import logging
-import requests
-import json
-
-
-class _NullHandler(logging.Handler):
- def emit(self, record):
- pass
-
-logger = logging.getLogger(__name__)
-logger.addHandler(_NullHandler())
-
-
-class ZabbixAPIException(Exception):
- """ generic zabbix api exception
- code list:
- -32602 - Invalid params (eg already exists)
- -32500 - no permissions
- """
- pass
-
-
-class ZabbixAPI(object):
- def __init__(self,
- server='http://localhost/zabbix',
- session=None,
- use_authenticate=False,
- timeout=None):
- """
- Parameters:
- server: Base URI for zabbix web interface (omitting /api_jsonrpc.php)
- session: optional pre-configured requests.Session instance
- use_authenticate: Use old (Zabbix 1.8) style authentication
- timeout: optional connect and read timeout in seconds, default: None (if you're using Requests >= 2.4 you can set it as tuple: "(connect, read)" which is used to set individual connect and read timeouts.)
- """
-
- if session:
- self.session = session
- else:
- self.session = requests.Session()
-
- # Default headers for all requests
- self.session.headers.update({
- 'Content-Type': 'application/json-rpc',
- 'User-Agent': 'python/pyzabbix',
- 'Cache-Control': 'no-cache'
- })
-
- self.use_authenticate = use_authenticate
- self.auth = ''
- self.id = 0
-
- self.timeout = timeout
-
- self.url = server + '/api_jsonrpc.php'
- logger.info("JSON-RPC Server Endpoint: %s", self.url)
-
- def login(self, user='', password=''):
- """Convenience method for calling user.authenticate and storing the resulting auth token
- for further commands.
- If use_authenticate is set, it uses the older (Zabbix 1.8) authentication command
- :param password: Password used to login into Zabbix
- :param user: Username used to login into Zabbix
- """
-
- # If we have an invalid auth token, we are not allowed to send a login
- # request. Clear it before trying.
- self.auth = ''
- if self.use_authenticate:
- self.auth = self.user.authenticate(user=user, password=password)
- else:
- self.auth = self.user.login(user=user, password=password)
-
- def confimport(self, confformat='', source='', rules=''):
- """Alias for configuration.import because it clashes with
- Python's import reserved keyword
- :param rules:
- :param source:
- :param confformat:
- """
-
- return self.do_request(
- method="configuration.import",
- params={"format": confformat, "source": source, "rules": rules}
- )['result']
-
- def api_version(self):
- return self.apiinfo.version()
-
- def do_request(self, method, params=None):
- request_json = {
- 'jsonrpc': '2.0',
- 'method': method,
- 'params': params or {},
- 'id': self.id,
- }
-
- # We don't have to pass the auth token if asking for the apiinfo.version
- if self.auth and method != 'apiinfo.version':
- request_json['auth'] = self.auth
-
- logger.debug("Sending: %s", json.dumps(request_json,
- indent=4,
- separators=(',', ': ')))
- response = self.session.post(
- self.url,
- data=json.dumps(request_json),
- timeout=self.timeout
- )
- logger.debug("Response Code: %s", str(response.status_code))
-
- # NOTE: Getting a 412 response code means the headers are not in the
- # list of allowed headers.
- response.raise_for_status()
-
- if not len(response.text):
- raise ZabbixAPIException("Received empty response")
-
- try:
- response_json = json.loads(response.text)
- except ValueError:
- raise ZabbixAPIException(
- "Unable to parse json: %s" % response.text
- )
- logger.debug("Response Body: %s", json.dumps(response_json,
- indent=4,
- separators=(',', ': ')))
-
- self.id += 1
-
- if 'error' in response_json: # some exception
- if 'data' not in response_json['error']: # some errors don't contain 'data': workaround for ZBX-9340
- response_json['error']['data'] = "No data"
- msg = "Error {code}: {message}, {data}".format(
- code=response_json['error']['code'],
- message=response_json['error']['message'],
- data=response_json['error']['data']
- )
- raise ZabbixAPIException(msg, response_json['error']['code'])
-
- return response_json
-
- def __getattr__(self, attr):
- """Dynamically create an object class (ie: host)"""
- return ZabbixAPIObjectClass(attr, self)
-
-
-class ZabbixAPIObjectClass(object):
- def __init__(self, name, parent):
- self.name = name
- self.parent = parent
-
- def __getattr__(self, attr):
- """Dynamically create a method (ie: get)"""
-
- def fn(*args, **kwargs):
- if args and kwargs:
- raise TypeError("Found both args and kwargs")
-
- return self.parent.do_request(
- '{0}.{1}'.format(self.name, attr),
- args or kwargs
- )['result']
-
- return fn
+from .api import (
+ ZabbixAPI,
+ ZabbixAPIException,
+ ZabbixAPIMethod,
+ ZabbixAPIObject,
+ ZabbixAPIObjectClass,
+)
diff --git a/pyzabbix/api.py b/pyzabbix/api.py
new file mode 100644
index 0000000..4e2deea
--- /dev/null
+++ b/pyzabbix/api.py
@@ -0,0 +1,305 @@
+# pylint: disable=wrong-import-order
+
+import logging
+from typing import Mapping, Optional, Sequence, Tuple, Union
+from warnings import warn
+
+from packaging.version import Version
+from requests import Session
+
+__all__ = [
+ "ZabbixAPI",
+ "ZabbixAPIException",
+ "ZabbixAPIMethod",
+ "ZabbixAPIObject",
+ "ZabbixAPIObjectClass",
+]
+
+logger = logging.getLogger(__name__)
+logger.addHandler(logging.NullHandler())
+
+ZABBIX_5_4_0 = Version("5.4.0")
+ZABBIX_6_4_0 = Version("6.4.0")
+
+
+class ZabbixAPIException(Exception):
+ """Generic Zabbix API exception
+
+ Codes:
+ -32700: invalid JSON. An error occurred on the server while
+ parsing the JSON text (typo, wrong quotes, etc.)
+ -32600: received JSON is not a valid JSON-RPC Request
+ -32601: requested remote-procedure does not exist
+ -32602: invalid method parameters
+ -32603: Internal JSON-RPC error
+ -32400: System error
+ -32300: Transport error
+ -32500: Application error
+ """
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args)
+
+ self.error = kwargs.get("error", None)
+
+
+# pylint: disable=too-many-instance-attributes
+class ZabbixAPI:
+ # pylint: disable=too-many-arguments
+ def __init__(
+ self,
+ server: str = "http://localhost/zabbix",
+ session: Optional[Session] = None,
+ use_authenticate: bool = False,
+ timeout: Optional[Union[float, int, Tuple[int, int]]] = None,
+ detect_version: bool = True,
+ ):
+ """
+ :param server: Base URI for zabbix web interface (omitting /api_jsonrpc.php)
+ :param session: optional pre-configured requests.Session instance
+ :param use_authenticate: Use old (Zabbix 1.8) style authentication
+ :param timeout: optional connect and read timeout in seconds, default: None
+ If you're using Requests >= 2.4 you can set it as
+ tuple: "(connect, read)" which is used to set individual
+ connect and read timeouts.
+ :param detect_version: autodetect Zabbix API version
+ """
+ self.session = session or Session()
+
+ # Default headers for all requests
+ self.session.headers.update(
+ {
+ "Content-Type": "application/json-rpc",
+ "User-Agent": "python/pyzabbix",
+ "Cache-Control": "no-cache",
+ }
+ )
+
+ self.use_authenticate = use_authenticate
+ self.use_api_token = False
+ self.auth = ""
+ self.id = 0 # pylint: disable=invalid-name
+
+ self.timeout = timeout
+
+ if not server.endswith("/api_jsonrpc.php"):
+ server = server.rstrip("/") + "/api_jsonrpc.php"
+ self.url = server
+ logger.info("JSON-RPC Server Endpoint: %s", self.url)
+
+ self.version: Optional[Version] = None
+ self._detect_version = detect_version
+
+ def __enter__(self) -> "ZabbixAPI":
+ return self
+
+ # pylint: disable=inconsistent-return-statements
+ def __exit__(self, exception_type, exception_value, traceback):
+ if isinstance(exception_value, (ZabbixAPIException, type(None))):
+ if self.is_authenticated and not self.use_api_token:
+ # Logout the user if they are authenticated using username + password.
+ self.user.logout()
+ return True
+ return None
+
+ def login(
+ self,
+ user: str = "",
+ password: str = "",
+ api_token: Optional[str] = None,
+ ) -> None:
+ """Convenience method for calling user.authenticate
+ and storing the resulting auth token for further commands.
+
+ If use_authenticate is set, it uses the older (Zabbix 1.8)
+ authentication command
+
+ :param password: Password used to login into Zabbix
+ :param user: Username used to login into Zabbix
+ :param api_token: API Token to authenticate with
+ """
+
+ if self._detect_version:
+ self.version = Version(self.api_version())
+ logger.info("Zabbix API version is: %s", self.version)
+
+ # If the API token is explicitly provided, use this instead.
+ if api_token is not None:
+ self.use_api_token = True
+ self.auth = api_token
+ return
+
+ # If we have an invalid auth token, we are not allowed to send a login
+ # request. Clear it before trying.
+ self.auth = ""
+ if self.use_authenticate:
+ self.auth = self.user.authenticate(user=user, password=password)
+ elif self.version and self.version >= ZABBIX_5_4_0:
+ self.auth = self.user.login(username=user, password=password)
+ else:
+ self.auth = self.user.login(user=user, password=password)
+
+ def check_authentication(self):
+ if self.use_api_token:
+ # We cannot use this call using an API Token
+ return True
+ # Convenience method for calling user.checkAuthentication of the current session
+ return self.user.checkAuthentication(sessionid=self.auth)
+
+ @property
+ def is_authenticated(self) -> bool:
+ if self.use_api_token:
+ # We cannot use this call using an API Token
+ return True
+
+ try:
+ self.user.checkAuthentication(sessionid=self.auth)
+ except ZabbixAPIException:
+ return False
+ return True
+
+ def confimport(
+ self,
+ confformat: str = "",
+ source: str = "",
+ rules: str = "",
+ ) -> dict:
+ """Alias for configuration.import because it clashes with
+ Python's import reserved keyword
+ :param rules:
+ :param source:
+ :param confformat:
+ """
+ warn(
+ "ZabbixAPI.confimport(format, source, rules) has been deprecated, please use "
+ "ZabbixAPI.configuration['import'](format=format, source=source, rules=rules) instead",
+ DeprecationWarning,
+ 2,
+ )
+
+ return self.configuration["import"](
+ format=confformat,
+ source=source,
+ rules=rules,
+ )
+
+ def api_version(self) -> str:
+ return self.apiinfo.version()
+
+ def do_request(
+ self,
+ method: str,
+ params: Optional[Union[Mapping, Sequence]] = None,
+ ) -> dict:
+ payload = {
+ "jsonrpc": "2.0",
+ "method": method,
+ "params": params or {},
+ "id": self.id,
+ }
+ headers = {}
+
+ # We don't have to pass the auth token if asking for
+ # the apiinfo.version or user.checkAuthentication
+ anonymous_methods = {
+ "apiinfo.version",
+ "user.checkAuthentication",
+ "user.login",
+ }
+ if self.auth and method not in anonymous_methods:
+ if self.version and self.version >= ZABBIX_6_4_0:
+ headers["Authorization"] = f"Bearer {self.auth}"
+ else:
+ payload["auth"] = self.auth
+
+ logger.debug("Sending: %s", payload)
+ resp = self.session.post(
+ self.url,
+ json=payload,
+ headers=headers,
+ timeout=self.timeout,
+ )
+ logger.debug("Response Code: %s", resp.status_code)
+
+ # NOTE: Getting a 412 response code means the headers are not in the
+ # list of allowed headers.
+ resp.raise_for_status()
+
+ if not resp.text:
+ raise ZabbixAPIException("Received empty response")
+
+ try:
+ response = resp.json()
+ except ValueError as exception:
+ raise ZabbixAPIException(
+ f"Unable to parse json: {resp.text}"
+ ) from exception
+
+ logger.debug("Response Body: %s", response)
+
+ self.id += 1
+
+ if "error" in response: # some exception
+ error = response["error"]
+
+ # some errors don't contain 'data': workaround for ZBX-9340
+ if "data" not in error:
+ error["data"] = "No data"
+
+ raise ZabbixAPIException(
+ f"Error {error['code']}: {error['message']}, {error['data']}",
+ error["code"],
+ error=error,
+ )
+
+ return response
+
+ def _object(self, attr: str) -> "ZabbixAPIObject":
+ """Dynamically create an object class (ie: host)"""
+ return ZabbixAPIObject(attr, self)
+
+ def __getattr__(self, attr: str) -> "ZabbixAPIObject":
+ return self._object(attr)
+
+ def __getitem__(self, attr: str) -> "ZabbixAPIObject":
+ return self._object(attr)
+
+
+# pylint: disable=too-few-public-methods
+class ZabbixAPIMethod:
+ def __init__(self, method: str, parent: ZabbixAPI):
+ self._method = method
+ self._parent = parent
+
+ def __call__(self, *args, **kwargs):
+ if args and kwargs:
+ raise TypeError("Found both args and kwargs")
+
+ return self._parent.do_request(self._method, args or kwargs)["result"]
+
+
+# pylint: disable=too-few-public-methods
+class ZabbixAPIObject:
+ def __init__(self, name: str, parent: ZabbixAPI):
+ self._name = name
+ self._parent = parent
+
+ def _method(self, attr: str) -> ZabbixAPIMethod:
+ """Dynamically create a method (ie: get)"""
+ return ZabbixAPIMethod(f"{self._name}.{attr}", self._parent)
+
+ def __getattr__(self, attr: str) -> ZabbixAPIMethod:
+ return self._method(attr)
+
+ def __getitem__(self, attr: str) -> ZabbixAPIMethod:
+ return self._method(attr)
+
+
+class ZabbixAPIObjectClass(ZabbixAPIObject):
+ def __init__(self, *args, **kwargs):
+ warn(
+ "ZabbixAPIObjectClass has been renamed to ZabbixAPIObject",
+ DeprecationWarning,
+ 2,
+ )
+ super().__init__(*args, **kwargs)
diff --git a/pyzabbix/py.typed b/pyzabbix/py.typed
new file mode 100644
index 0000000..1242d43
--- /dev/null
+++ b/pyzabbix/py.typed
@@ -0,0 +1 @@
+# Marker file for PEP 561.
diff --git a/setup.py b/setup.py
index ccc53a3..c562900 100644
--- a/setup.py
+++ b/setup.py
@@ -1,12 +1,14 @@
from setuptools import setup
+with open("README.md", encoding="utf-8") as fh:
+ long_description = fh.read()
+
setup(
name="pyzabbix",
- version="0.7.4",
- install_requires=[
- "requests>=1.0",
- ],
+ version="1.3.1",
description="Zabbix API Python interface",
+ long_description=long_description,
+ long_description_content_type="text/markdown",
author="Luke Cyca",
author_email="me@lukecyca.com",
license="LGPL",
@@ -14,9 +16,12 @@
url="http://github.com/lukecyca/pyzabbix",
classifiers=[
"Programming Language :: Python",
- "Programming Language :: Python :: 2.6",
- "Programming Language :: Python :: 2.7",
- "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.6",
+ "Programming Language :: Python :: 3.7",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
"License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)",
"Operating System :: OS Independent",
"Development Status :: 4 - Beta",
@@ -26,7 +31,23 @@
"Topic :: System :: Systems Administration",
],
packages=["pyzabbix"],
- tests_require=[
- "httpretty<0.8.7",
+ package_data={"": ["py.typed"]},
+ python_requires=">=3.6",
+ install_requires=[
+ "requests>=1.0",
+ "packaging",
],
+ extras_require={
+ "dev": [
+ "black",
+ "isort",
+ "mypy",
+ "pylint",
+ "pytest-cov",
+ "pytest-xdist",
+ "pytest",
+ "requests-mock",
+ "types-requests",
+ ],
+ },
)
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/api_test.py b/tests/api_test.py
new file mode 100644
index 0000000..1effe85
--- /dev/null
+++ b/tests/api_test.py
@@ -0,0 +1,285 @@
+import pytest
+from packaging.version import Version
+
+from pyzabbix import ZabbixAPI, ZabbixAPIException
+
+
+@pytest.mark.parametrize(
+ "server, expected",
+ [
+ ("http://example.com", "http://example.com/api_jsonrpc.php"),
+ ("http://example.com/", "http://example.com/api_jsonrpc.php"),
+ ("http://example.com/base", "http://example.com/base/api_jsonrpc.php"),
+ ("http://example.com/base/", "http://example.com/base/api_jsonrpc.php"),
+ ],
+)
+def test_server_url_correction(server, expected):
+ assert ZabbixAPI(server).url == expected
+
+
+def _zabbix_requests_mock_factory(requests_mock, *args, **kwargs):
+ requests_mock.post(
+ "http://example.com/api_jsonrpc.php",
+ request_headers={
+ "Content-Type": "application/json-rpc",
+ "User-Agent": "python/pyzabbix",
+ "Cache-Control": "no-cache",
+ },
+ *args,
+ **kwargs,
+ )
+
+
+def test_login(requests_mock):
+ _zabbix_requests_mock_factory(
+ requests_mock,
+ json={
+ "jsonrpc": "2.0",
+ "result": "0424bd59b807674191e7d77572075f33",
+ "id": 0,
+ },
+ )
+
+ zapi = ZabbixAPI("http://example.com", detect_version=False)
+ zapi.login("mylogin", "mypass")
+
+ # Check request
+ assert requests_mock.last_request.json() == {
+ "jsonrpc": "2.0",
+ "method": "user.login",
+ "params": {"user": "mylogin", "password": "mypass"},
+ "id": 0,
+ }
+
+ # Check login
+ assert zapi.auth == "0424bd59b807674191e7d77572075f33"
+
+
+def test_login_with_context(requests_mock):
+ _zabbix_requests_mock_factory(
+ requests_mock,
+ json={
+ "jsonrpc": "2.0",
+ "result": "0424bd59b807674191e7d77572075f33",
+ "id": 0,
+ },
+ )
+
+ with ZabbixAPI("http://example.com", detect_version=False) as zapi:
+ zapi.login("mylogin", "mypass")
+ assert zapi.auth == "0424bd59b807674191e7d77572075f33"
+
+
+@pytest.mark.parametrize(
+ "version",
+ [
+ ("4.0.0"),
+ ("5.4.0"),
+ ("6.2.0"),
+ ("6.2.0beta1"),
+ ("6.2.2alpha1"),
+ ],
+)
+def test_login_with_version_detect(requests_mock, version):
+ _zabbix_requests_mock_factory(
+ requests_mock,
+ [
+ {
+ "json": {
+ "jsonrpc": "2.0",
+ "result": version,
+ "id": 0,
+ }
+ },
+ {
+ "json": {
+ "jsonrpc": "2.0",
+ "result": "0424bd59b807674191e7d77572075f33",
+ "id": 0,
+ }
+ },
+ ],
+ )
+
+ with ZabbixAPI("http://example.com") as zapi:
+ zapi.login("mylogin", "mypass")
+ assert zapi.auth == "0424bd59b807674191e7d77572075f33"
+
+
+def test_attr_syntax_kwargs(requests_mock):
+ _zabbix_requests_mock_factory(
+ requests_mock,
+ json={
+ "jsonrpc": "2.0",
+ "result": [{"hostid": 1234}],
+ "id": 0,
+ },
+ )
+
+ zapi = ZabbixAPI("http://example.com", detect_version=False)
+ zapi.auth = "some_auth_key"
+ result = zapi.host.get(hostids=5)
+
+ # Check request
+ assert requests_mock.last_request.json() == {
+ "jsonrpc": "2.0",
+ "method": "host.get",
+ "params": {"hostids": 5},
+ "auth": "some_auth_key",
+ "id": 0,
+ }
+
+ # Check response
+ assert result == [{"hostid": 1234}]
+
+
+def test_attr_syntax_args(requests_mock):
+ _zabbix_requests_mock_factory(
+ requests_mock,
+ json={
+ "jsonrpc": "2.0",
+ "result": {"itemids": ["22982", "22986"]},
+ "id": 0,
+ },
+ )
+
+ zapi = ZabbixAPI("http://example.com", detect_version=False)
+ zapi.auth = "some_auth_key"
+ result = zapi.host.delete("22982", "22986")
+
+ # Check request
+ assert requests_mock.last_request.json() == {
+ "jsonrpc": "2.0",
+ "method": "host.delete",
+ "params": ["22982", "22986"],
+ "auth": "some_auth_key",
+ "id": 0,
+ }
+
+ # Check response
+ assert result == {"itemids": ["22982", "22986"]}
+
+
+def test_attr_syntax_args_and_kwargs_raises():
+ with pytest.raises(
+ TypeError,
+ match="Found both args and kwargs",
+ ):
+ zapi = ZabbixAPI("http://example.com")
+ zapi.host.delete("22982", hostids=5)
+
+
+@pytest.mark.parametrize(
+ "version",
+ [
+ ("4.0.0"),
+ ("4.0.0rc1"),
+ ("6.2.0beta1"),
+ ("6.2.2alpha1"),
+ ],
+)
+def test_detecting_version(requests_mock, version):
+ _zabbix_requests_mock_factory(
+ requests_mock,
+ json={
+ "jsonrpc": "2.0",
+ "result": version,
+ "id": 0,
+ },
+ )
+
+ zapi = ZabbixAPI("http://example.com")
+ zapi.login("mylogin", "mypass")
+
+ assert zapi.api_version() == version
+
+
+@pytest.mark.parametrize(
+ "data",
+ [
+ (None),
+ ('No groups for host "Linux server".'),
+ ],
+)
+def test_error_response(requests_mock, data):
+ _zabbix_requests_mock_factory(
+ requests_mock,
+ json={
+ "jsonrpc": "2.0",
+ "error": {
+ "code": -32602,
+ "message": "Invalid params.",
+ **({} if data is None else {"data": data}),
+ },
+ "id": 0,
+ },
+ )
+
+ with pytest.raises(
+ ZabbixAPIException,
+ match="Error -32602: Invalid params., No data."
+ if data is None
+ else f"Error -32602: Invalid params., {data}",
+ ):
+ zapi = ZabbixAPI("http://example.com")
+ zapi.host.get()
+
+
+def test_empty_response(requests_mock):
+ _zabbix_requests_mock_factory(
+ requests_mock,
+ body="",
+ )
+
+ with pytest.raises(ZabbixAPIException, match="Received empty response"):
+ zapi = ZabbixAPI("http://example.com")
+ zapi.login("mylogin", "mypass")
+
+
+@pytest.mark.parametrize(
+ "version",
+ [
+ ("4.0.0"),
+ ("5.4.0"),
+ ("6.2.0"),
+ ],
+)
+def test_do_request(requests_mock, version):
+ _zabbix_requests_mock_factory(
+ requests_mock,
+ json={
+ "jsonrpc": "2.0",
+ "result": [{"hostid": 1234}],
+ "id": 0,
+ },
+ )
+
+ zapi = ZabbixAPI("http://example.com", detect_version=False)
+ zapi.version = Version(version)
+ zapi.auth = "some_auth_key"
+ result = zapi["host"]["get"]()
+
+ # Check response
+ assert result == [{"hostid": 1234}]
+
+ # Check request
+ found = requests_mock.last_request
+ expect_json = {
+ "jsonrpc": "2.0",
+ "method": "host.get",
+ "params": {},
+ "id": 0,
+ }
+ expect_headers = {
+ "Cache-Control": "no-cache",
+ "Content-Type": "application/json-rpc",
+ "User-Agent": "python/pyzabbix",
+ }
+
+ if zapi.version < Version("6.4.0"):
+ expect_json["auth"] = "some_auth_key"
+ else:
+ expect_headers["Authorization"] = "Bearer some_auth_key"
+
+ assert found.json() == expect_json
+ assert found.headers.items() >= expect_headers.items()
diff --git a/tests/test_api.py b/tests/test_api.py
deleted file mode 100644
index c9f860f..0000000
--- a/tests/test_api.py
+++ /dev/null
@@ -1,111 +0,0 @@
-import unittest
-import httpretty
-import json
-from pyzabbix import ZabbixAPI
-
-
-class TestPyZabbix(unittest.TestCase):
-
- @httpretty.activate
- def test_login(self):
- httpretty.register_uri(
- httpretty.POST,
- "http://example.com/api_jsonrpc.php",
- body=json.dumps({
- "jsonrpc": "2.0",
- "result": "0424bd59b807674191e7d77572075f33",
- "id": 0
- }),
- )
-
- zapi = ZabbixAPI('http://example.com')
- zapi.login('mylogin', 'mypass')
-
- # Check request
- self.assertEqual(
- json.loads(httpretty.last_request().body.decode('utf-8')),
- {
- 'jsonrpc': '2.0',
- 'method': 'user.login',
- 'params': {'user': 'mylogin', 'password': 'mypass'},
- 'id': 0,
- }
- )
- self.assertEqual(
- httpretty.last_request().headers['content-type'],
- 'application/json-rpc'
- )
- self.assertEqual(
- httpretty.last_request().headers['user-agent'],
- 'python/pyzabbix'
- )
-
- # Check response
- self.assertEqual(zapi.auth, "0424bd59b807674191e7d77572075f33")
-
- @httpretty.activate
- def test_host_get(self):
- httpretty.register_uri(
- httpretty.POST,
- "http://example.com/api_jsonrpc.php",
- body=json.dumps({
- "jsonrpc": "2.0",
- "result": [{"hostid": 1234}],
- "id": 0
- }),
- )
-
- zapi = ZabbixAPI('http://example.com')
- zapi.auth = "123"
- result = zapi.host.get()
-
- # Check request
- self.assertEqual(
- json.loads(httpretty.last_request().body.decode('utf-8')),
- {
- 'jsonrpc': '2.0',
- 'method': 'host.get',
- 'params': {},
- 'auth': '123',
- 'id': 0,
- }
- )
-
- # Check response
- self.assertEqual(result, [{"hostid": 1234}])
-
- @httpretty.activate
- def test_host_delete(self):
- httpretty.register_uri(
- httpretty.POST,
- "http://example.com/api_jsonrpc.php",
- body=json.dumps({
- "jsonrpc": "2.0",
- "result": {
- "itemids": [
- "22982",
- "22986"
- ]
- },
- "id": 0
- }),
- )
-
- zapi = ZabbixAPI('http://example.com')
- zapi.auth = "123"
- result = zapi.host.delete("22982", "22986")
-
- # Check request
- self.assertEqual(
- json.loads(httpretty.last_request().body.decode('utf-8')),
- {
- 'jsonrpc': '2.0',
- 'method': 'host.delete',
- 'params': ["22982", "22986"],
- 'auth': '123',
- 'id': 0,
- }
- )
-
- # Check response
- self.assertEqual(set(result["itemids"]), set(["22982", "22986"]))